fix: restore listener edit actions and validate required fields

This commit is contained in:
Ed1s0nZ committed 2026-10-06 02:54:27 +08:00
1 parent ae395f5bf7
commit 7d6d3e77dc
10 files changed
+186 -17

No files matched your search

+4
View File
@@ -4144,6 +4144,10 @@
"failed": "Failed"
},
"listeners": {
"nameRequired": "Enter a listener name",
"typeRequired": "Select a listener type",
"portInvalid": "Port must be an integer from 1 to 65535",
"saveFailed": "Failed to save listener",
"title": "Listener Management",
"create": "Create Listener",
"name": "Name",
+4
View File
@@ -4156,6 +4156,10 @@
"failed": "Ошибка"
},
"listeners": {
"nameRequired": "Введите имя слушателя",
"typeRequired": "Выберите тип слушателя",
"portInvalid": "Порт должен быть целым числом от 1 до 65535",
"saveFailed": "Не удалось сохранить слушатель",
"title": "Управление слушателями",
"create": "Создать слушатель",
"name": "Имя",
+4
View File
@@ -4132,6 +4132,10 @@
"failed": "失败"
},
"listeners": {
"nameRequired": "请输入监听器名称",
"typeRequired": "请选择监听器类型",
"portInvalid": "端口必须为 1–65535 的整数",
"saveFailed": "监听器保存失败",
"title": "监听器管理",
"create": "创建监听器",
"name": "名称",
+66
View File
@@ -0,0 +1,66 @@
const fs = require('node:fs');
const vm = require('node:vm');
const test = require('node:test');
const assert = require('node:assert/strict');
const source = fs.readFileSync('web/static/js/c2.js', 'utf8');
function harness(overrides = {}, failure) {
const fields = Object.fromEntries(Object.entries({ name: 'listener', type: 'http', host: '127.0.0.1', port: '8080', ...overrides }).map(([key, value]) => ['c2-listener-' + key, { value, setAttribute(k, v) { this[k] = v; }, focus() { this.focused = true; }, setCustomValidity(v) { this.message = v; }, reportValidity() {} }]));
const requests = [], messages = [];
let closed = false;
const c = vm.createContext({ document: { getElementById: id => fields[id] }, C2: { listeners: [], closeModal() { closed = true; }, loadListeners() {} }, API_BASE: '/api/c2', c2t: key => key, showToast: (message, type) => messages.push({ message, type }), apiRequest: async (...args) => { requests.push(args); if (failure) throw Error(failure); return {}; } });
vm.runInContext(source.slice(source.indexOf(' function validateListenerForm('), source.indexOf(' C2.startListener =')), c);
vm.runInContext(source.slice(source.indexOf(' C2.saveListener ='), source.indexOf(' // ============================================================================\n // 会话管理')), c);
return { c, fields, requests, messages, closed: () => closed };
}
test('creation and editing reject invalid fields and identify the field', async () => {
for (const method of ['createListener', 'saveListener']) for (const values of [{ name: '' }, { port: '0' }, { port: '65536' }, { port: '1.5' }, { port: '80abc' }]) {
const h = harness(values);
await h.c.C2[method]('id');
assert.equal(h.requests.length, 0);
assert.equal(h.messages.length, 1);
assert.ok(Object.values(h.fields).some(field => field.focused && field['aria-invalid'] === 'true'));
}
});
test('valid edit sends PUT and closes only after success', async () => {
const h = harness();
await h.c.C2.saveListener('id');
assert.equal(h.requests[0][0], 'PUT');
assert.equal(h.requests[0][1], '/api/c2/listeners/id');
assert.equal(h.requests[0][2].bind_port, 8080);
assert.equal(h.closed(), true);
});
test('transport failure is visible and keeps editor open', async () => {
for (const method of ['createListener', 'saveListener']) {
const h = harness({}, 'offline');
await h.c.C2[method]('id');
assert.equal(h.messages.at(-1).message, 'offline');
assert.equal(h.messages.at(-1).type, 'error');
assert.equal(h.closed(), false);
}
});
test('modal allows click delegation while background close is target guarded', () => {
const html = fs.readFileSync('web/templates/index.html', 'utf8');
const modal = html.slice(html.indexOf('<div id="c2-modal"'), html.indexOf('<div id="c2-modal"') + 360);
assert.match(modal, /if\(event.target===this\)C2.closeModal\(\)/);
assert.doesNotMatch(modal, /stopPropagation/);
assert.match(source, /case 'listener-save': C2.saveListener\(id\)/);
});
test('delegated edit-save button reaches the actual save function', async () => {
const h = harness();
const handlers = {};
h.c.document.documentElement = { dataset: {} };
h.c.document.addEventListener = (type, callback) => { handlers[type] = callback; };
const start = source.indexOf(' function bindC2SafeActionDelegation(');
const end = source.indexOf(' function runC2SafeAction(', start);
vm.runInContext(source.slice(start, end), h.c);
// Use the original action dispatcher, trimming at its next function boundary.
const dispatchStart = source.indexOf(' function runC2SafeAction(');
const dispatchEnd = source.indexOf('\n function ', dispatchStart + 10);
vm.runInContext(source.slice(dispatchStart, dispatchEnd), h.c);
const button = { getAttribute: key => ({ 'data-c2-action': 'listener-save', 'data-c2-id': 'listener-id' })[key] };
handlers.click({ target: { closest: selector => selector === '[data-c2-action]' ? button : null }, preventDefault() {}, stopPropagation() {} });
await new Promise(resolve => setImmediate(resolve));
assert.equal(h.requests.length, 1);
assert.equal(h.requests[0][1], '/api/c2/listeners/listener-id');
assert.equal(h.closed(), true);
});
+35 -14
View File
@@ -1035,11 +1035,11 @@
<div class="c2-modal-body">
<div class="c2-form-row">
<div class="c2-form-group">
<label>${escapeHtml(c2t('c2.listeners.name'))}</label>
<label>${escapeHtml(c2t('c2.listeners.name'))} <span aria-hidden="true">*</span></label>
<input type="text" id="c2-listener-name" class="form-control" placeholder="${escapeAttr(c2t('c2.listeners.placeholderNameExample'))}">
</div>
<div class="c2-form-group">
<label>${escapeHtml(c2t('c2.listeners.type'))}</label>
<label>${escapeHtml(c2t('c2.listeners.type'))} <span aria-hidden="true">*</span></label>
<select id="c2-listener-type" class="form-control c2-form-select-native">
<option value="http_beacon">HTTP Beacon</option>
<option value="https_beacon">HTTPS Beacon</option>
@@ -1059,7 +1059,7 @@
<div class="form-hint">${escapeHtml(c2t('c2.listeners.bindHintExternal'))}</div>
</div>
<div class="c2-form-group">
<label>${escapeHtml(c2t('c2.listeners.bindPort'))}</label>
<label>${escapeHtml(c2t('c2.listeners.bindPort'))} <span aria-hidden="true">*</span></label>
<input type="number" id="c2-listener-port" class="form-control" placeholder="8443">
</div>
</div>
@@ -1121,7 +1121,32 @@
syncC2FormSelect('c2-listener-profile-id');
};
function validateListenerForm(creating) {
const checks = [
['c2-listener-name', 'c2.listeners.nameRequired', value => !!value.trim()],
...(creating ? [['c2-listener-type', 'c2.listeners.typeRequired', value => !!value]] : []),
['c2-listener-port', 'c2.listeners.portInvalid', value => /^\d+$/.test(value) && Number(value) >= 1 && Number(value) <= 65535]
];
let firstInvalid;
checks.forEach(([id, key, valid]) => {
const input = document.getElementById(id);
if (!input) return;
const invalid = !valid(String(input.value || ''));
input.setAttribute('aria-invalid', String(invalid));
input.setCustomValidity?.(invalid ? c2t(key) : '');
if (invalid && !firstInvalid) firstInvalid = { input, key };
});
if (firstInvalid) {
showToast(c2t(firstInvalid.key), 'error');
firstInvalid.input.focus();
firstInvalid.input.reportValidity?.();
return false;
}
return true;
}
C2.createListener = function() {
if (!validateListenerForm(true)) return;
const name = document.getElementById('c2-listener-name')?.value.trim();
const type = document.getElementById('c2-listener-type')?.value;
const bindHost = document.getElementById('c2-listener-host')?.value || '127.0.0.1';
@@ -1129,11 +1154,6 @@
const callbackHost = document.getElementById('c2-listener-callback-host')?.value?.trim() || '';
const remark = document.getElementById('c2-listener-remark')?.value;
if (!name || !type || !bindPort) {
showToast(c2t('c2.listeners.toastFillRequired'), 'error');
return;
}
const profileId = (document.getElementById('c2-listener-profile-id')?.value || '').trim();
const legacyShell = document.getElementById('c2-listener-legacy-shell')?.checked === true;
const body = {
@@ -1146,7 +1166,7 @@
body.config = { allow_legacy_shell: true };
}
apiRequest('POST', `${API_BASE}/listeners`, body).then(data => {
return apiRequest('POST', `${API_BASE}/listeners`, body).then(data => {
if (data.error) {
showToast(data.error, 'error');
} else {
@@ -1154,7 +1174,7 @@
C2.closeModal();
C2.loadListeners();
}
});
}).catch(error => showToast(error.message || c2t('c2.listeners.saveFailed'), 'error'));
};
C2.startListener = function(id) {
@@ -1263,7 +1283,7 @@
</div>
<div class="c2-modal-body">
<div class="c2-form-group">
<label>${escapeHtml(c2t('c2.listeners.name'))}</label>
<label>${escapeHtml(c2t('c2.listeners.name'))} <span aria-hidden="true">*</span></label>
<input type="text" id="c2-listener-name" class="form-control" value="${escapeAttr(l.name)}">
</div>
<div class="c2-form-group">
@@ -1276,7 +1296,7 @@
<input type="text" id="c2-listener-host" class="form-control" value="${escapeAttr(String(l.bindHost))}">
</div>
<div class="c2-form-group">
<label>${escapeHtml(c2t('c2.listeners.bindPort'))}</label>
<label>${escapeHtml(c2t('c2.listeners.bindPort'))} <span aria-hidden="true">*</span></label>
<input type="number" id="c2-listener-port" class="form-control" value="${l.bindPort}">
</div>
</div>
@@ -1317,6 +1337,7 @@
};
C2.saveListener = function(id) {
if (!validateListenerForm(false)) return;
const name = document.getElementById('c2-listener-name')?.value.trim();
const bindHost = document.getElementById('c2-listener-host')?.value;
const bindPort = parseInt(document.getElementById('c2-listener-port')?.value);
@@ -1339,14 +1360,14 @@
body.config = merged;
}
apiRequest('PUT', `${API_BASE}/listeners/${id}`, body).then(data => {
return apiRequest('PUT', `${API_BASE}/listeners/${id}`, body).then(data => {
if (data.error) showToast(data.error, 'error');
else {
showToast(c2t('c2.listeners.toastUpdated'), 'success');
C2.closeModal();
C2.loadListeners();
}
});
}).catch(error => showToast(error.message || c2t('c2.listeners.saveFailed'), 'error'));
};
// ============================================================================
+1 -1
View File
@@ -3249,7 +3249,7 @@
<!-- C2 模态框 -->
<div id="c2-modal" class="c2-modal-overlay" style="display:none;" onclick="if(event.target===this)C2.closeModal()">
<div class="c2-modal" onclick="event.stopPropagation()">
<div class="c2-modal">
<div id="c2-modal-content"></div>
</div>
</div>