mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-10-20 06:27:53 +02:00
fix: restore listener edit actions and validate required fields
This commit is contained in:
1 parent
ae395f5bf7
commit
7d6d3e77dc
10 files changed
+186
-17
No files matched your search
@@ -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",
|
||||
|
||||
@@ -4156,6 +4156,10 @@
|
||||
"failed": "Ошибка"
|
||||
},
|
||||
"listeners": {
|
||||
"nameRequired": "Введите имя слушателя",
|
||||
"typeRequired": "Выберите тип слушателя",
|
||||
"portInvalid": "Порт должен быть целым числом от 1 до 65535",
|
||||
"saveFailed": "Не удалось сохранить слушатель",
|
||||
"title": "Управление слушателями",
|
||||
"create": "Создать слушатель",
|
||||
"name": "Имя",
|
||||
|
||||
@@ -4132,6 +4132,10 @@
|
||||
"failed": "失败"
|
||||
},
|
||||
"listeners": {
|
||||
"nameRequired": "请输入监听器名称",
|
||||
"typeRequired": "请选择监听器类型",
|
||||
"portInvalid": "端口必须为 1–65535 的整数",
|
||||
"saveFailed": "监听器保存失败",
|
||||
"title": "监听器管理",
|
||||
"create": "创建监听器",
|
||||
"name": "名称",
|
||||
|
||||
@@ -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
@@ -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'));
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
|
||||
Reference in new issue
Block a user