Files
CyberStrikeAI/web/static/js/c2.js
T
2026-05-04 04:50:53 +08:00

2066 lines
94 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// C2 模块前端逻辑 - 完整实现
// 支持: xterm 终端、文件管理、监听器/会话/任务/事件/Payload/Profile 管理
(function() {
'use strict';
// C2 模块命名空间
const C2 = {
currentPage: '',
listeners: [],
sessions: [],
tasks: [],
tasksPage: 1,
tasksPageSize: 10,
tasksTotal: 0,
tasksPendingQueuedCount: null,
events: [],
eventsPage: 1,
eventsPageSize: 10,
eventsTotal: 0,
profiles: [],
selectedSessionId: null,
selectedListenerId: null,
eventSource: null,
// xterm 相关
terminalInstance: null,
terminalFitAddon: null,
terminalResizeObserver: null,
terminalContainer: null,
terminalSessionId: 'main',
// 文件管理
currentPath: '/',
fileList: [],
// 任务轮询
taskPollInterval: null,
};
// API 基础路径
const API_BASE = '/api/c2';
window.__c2DownloadPayload = function(filename) {
const url = `${API_BASE}/payloads/${filename}/download`;
const fetchFn = (typeof apiFetch === 'function') ? apiFetch : fetch;
fetchFn(url).then(resp => {
if (!resp.ok) throw new Error('download failed: ' + resp.status);
return resp.blob();
}).then(blob => {
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(a.href);
}).catch(err => {
if (window.showToast) window.showToast(err.message, 'error');
});
};
function c2t(key, opts) {
try {
if (typeof window.t === 'function') return window.t(key, opts || {});
} catch (e) {}
return key;
}
function listenerTypeLabel(type) {
if (!type) return '';
const k = 'c2.listeners.typeLabels.' + String(type).toLowerCase();
const tr = c2t(k);
if (tr !== k) return tr;
return String(type).replace(/_/g, ' ');
}
function sessionStatusLabel(status) {
const s = String(status || '').toLowerCase();
if (!s) return '';
const k = 'c2.sessions.' + s;
const tr = c2t(k);
if (tr !== k) return tr;
return status;
}
function taskStatusLabel(status) {
const s = String(status || '').toLowerCase();
if (!s) return '';
const k = 'c2.tasks.' + s;
const tr = c2t(k);
if (tr !== k) return tr;
return status;
}
// ============================================================================
// 工具函数
// ============================================================================
function apiRequest(method, url, data) {
const options = {
method: method,
headers: { 'Content-Type': 'application/json' }
};
if (data && (method === 'POST' || method === 'PUT' || method === 'PATCH' || method === 'DELETE')) {
options.body = JSON.stringify(data);
}
if (typeof apiFetch === 'function') {
return apiFetch(url, options).then(r => r.json());
}
return fetch(url, options).then(r => r.json());
}
function showToast(message, type = 'info') {
if (window.showToast) {
window.showToast(message, type);
return;
}
const container = document.getElementById('c2-toast-container') || (() => {
const div = document.createElement('div');
div.id = 'c2-toast-container';
div.style.cssText = 'position:fixed;top:20px;right:20px;z-index:10000;display:flex;flex-direction:column;gap:8px;';
document.body.appendChild(div);
return div;
})();
const toast = document.createElement('div');
const colors = { error: '#e53e3e', success: '#38a169', info: '#3182ce', warn: '#d69e2e' };
toast.style.cssText = `background:${colors[type] || colors.info};color:#fff;padding:10px 18px;border-radius:6px;font-size:0.875rem;box-shadow:0 4px 12px rgba(0,0,0,0.2);opacity:0;transition:opacity .3s;max-width:400px;word-break:break-word;`;
toast.textContent = message;
container.appendChild(toast);
requestAnimationFrame(() => { toast.style.opacity = '1'; });
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 300);
}, 3500);
}
function formatTime(dateStr) {
if (!dateStr) return '-';
return new Date(dateStr).toLocaleString();
}
function formatDuration(ms) {
if (!ms || ms <= 0) return '-';
if (ms < 1000) return c2t('c2.fmt.durationMs', { n: ms });
if (ms < 60000) return c2t('c2.fmt.durationSec', { n: (ms / 1000).toFixed(1) });
return c2t('c2.fmt.durationMin', { n: (ms / 60000).toFixed(1) });
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/** 监听器表单:Malleable Profile 下拉选项 HTMLvalue / 文本已转义) */
function listenerProfileSelectHtml(selectedProfileId) {
const sel = selectedProfileId ? String(selectedProfileId) : '';
let opts = `<option value="">${escapeHtml(c2t('c2.listeners.malleableProfileNone'))}</option>`;
for (const p of (C2.profiles || [])) {
if (!p) continue;
const pid = p.id || p.ID;
if (!pid) continue;
const idEsc = escapeHtml(String(pid));
const nameEsc = escapeHtml(p.name || pid);
const selected = sel && String(pid) === sel ? ' selected' : '';
opts += `<option value="${idEsc}"${selected}>${nameEsc}</option>`;
}
return opts;
}
function listenerResolvedProfileId(l) {
if (!l) return '';
const v = l.profileId != null && l.profileId !== '' ? l.profileId : l.profile_id;
return v != null ? String(v).trim() : '';
}
/** 监听器卡片展示用 Profile 名称(依赖 C2.profiles,由 loadListeners 一并拉取) */
function listenerProfileDisplayName(l) {
const pid = listenerResolvedProfileId(l);
if (!pid) return '';
const list = C2.profiles || [];
for (let i = 0; i < list.length; i++) {
const p = list[i];
if (p && (p.id === pid || p.ID === pid)) return String(p.name || p.id || pid).trim() || pid;
}
return pid.length > 18 ? pid.substring(0, 16) + '…' : pid;
}
function listenerTypeVisualClass(type) {
const t = String(type || '').toLowerCase();
if (t === 'https_beacon') return 'c2-ltype-mark--https';
if (t === 'http_beacon') return 'c2-ltype-mark--http';
if (t === 'tcp_reverse') return 'c2-ltype-mark--tcp';
if (t === 'websocket') return 'c2-ltype-mark--ws';
return 'c2-ltype-mark--def';
}
function listenerTypeShortLabel(type) {
const t = String(type || '').toLowerCase();
if (t === 'https_beacon') return 'HTTPS';
if (t === 'http_beacon') return 'HTTP';
if (t === 'tcp_reverse') return 'TCP';
if (t === 'websocket') return 'WS';
return '?';
}
function listenerCardStatusPillLabel(status) {
const s = String(status || '').toLowerCase();
if (s === 'running') return c2t('c2.listeners.running');
if (s === 'stopped') return c2t('c2.listeners.stopped');
if (s === 'error') return c2t('c2.listeners.statusError');
return c2t('c2.listeners.stopped');
}
/** 避免 i18n 插值把日期里的「/」转成 &#x2F;,与 formatTime 拼接后整体转义 */
function formatListenerStartedHtml(dateStr) {
if (!dateStr) return '';
const prefix = c2t('c2.listeners.startedAtPrefix');
const time = formatTime(dateStr);
return '<div class="c2-listener-meta-row"><span class="c2-listener-meta-label">' + escapeHtml(prefix) + '</span> <span class="c2-listener-meta-time">' + escapeHtml(time) + '</span></div>';
}
function copyToClipboard(text) {
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(() => showToast(c2t('c2.clipboardCopied'), 'success'));
} else {
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
showToast(c2t('c2.clipboardCopied'), 'success');
}
}
// ============================================================================
// 页面初始化
// ============================================================================
C2.init = function() {
const pageId = window.currentPageId || '';
if (pageId.startsWith('c2')) {
C2.connectEventStream();
}
switch(pageId) {
case 'c2':
case 'c2-listeners':
C2.loadListeners();
break;
case 'c2-sessions':
C2.loadSessions();
break;
case 'c2-tasks':
C2.loadTasks();
break;
case 'c2-payloads':
C2.loadListenersForPayload();
break;
case 'c2-events':
C2.loadEvents();
break;
case 'c2-profiles':
C2.loadProfiles();
break;
}
};
// ============================================================================
// 监听器管理
// ============================================================================
C2.loadListeners = function() {
Promise.all([
apiRequest('GET', `${API_BASE}/listeners`),
apiRequest('GET', `${API_BASE}/profiles`).catch(function() { return {}; })
]).then(function(results) {
var ldata = results[0];
var pdata = results[1];
C2.listeners = (ldata && ldata.listeners) || [];
if (pdata && pdata.profiles && !pdata.error) {
C2.profiles = pdata.profiles;
}
C2.renderListeners();
C2.updateDashboardStats();
});
};
/** 拉取 Profile 列表(监听器表单用);失败时置空列表不阻断弹窗 */
C2.ensureProfilesLoaded = function() {
return apiRequest('GET', `${API_BASE}/profiles`).then(data => {
if (data && data.error) {
C2.profiles = [];
return C2.profiles;
}
C2.profiles = (data && data.profiles) || [];
return C2.profiles;
});
};
C2.renderListeners = function() {
const container = document.getElementById('c2-listener-grid');
if (!container) return;
if (C2.listeners.length === 0) {
container.innerHTML = `
<div class="c2-empty">
<svg width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="1.2" style="margin-bottom:16px;opacity:0.6;">
<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"></path>
<path d="M12 15l-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"></path>
<path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"></path>
<path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"></path>
</svg>
<h3 style="margin-bottom:8px;font-size:18px;font-weight:700;">${escapeHtml(c2t('c2.listeners.emptyTitle'))}</h3>
<p style="font-size:14px;">${escapeHtml(c2t('c2.listeners.emptyHint'))}</p>
<button class="btn-primary" onclick="C2.showCreateListenerModal()" style="margin-top:20px;">
${escapeHtml(c2t('c2.listeners.headerCreateBtn'))}
</button>
</div>`;
return;
}
container.innerHTML = C2.listeners.map(function(l) {
const st = String(l.status || 'stopped').toLowerCase();
const stUi = st === 'running' || st === 'stopped' || st === 'error' ? st : 'stopped';
const profilePid = listenerResolvedProfileId(l);
const profileName = listenerProfileDisplayName(l);
const profileBadge = profilePid
? '<div class="c2-listener-profile-badge" title="' + escapeHtml(c2t('c2.listeners.profileBadgeTitle')) + '"><span class="c2-listener-profile-dot" aria-hidden="true"></span><span>' + escapeHtml(profileName) + '</span></div>'
: '';
const cb = C2.getListenerCallbackHost(l);
const cbRow = cb
? '<div class="c2-listener-kv"><span class="c2-listener-kv-label">' + escapeHtml(c2t('c2.listeners.callbackShort')) + '</span><span class="c2-listener-kv-val c2-listener-mono">' + escapeHtml(cb) + '</span></div>'
: '';
const remarkRow = l.remark ? '<div class="c2-listener-remark">' + escapeHtml(l.remark) + '</div>' : '';
const startedHtml = formatListenerStartedHtml(l.startedAt);
const pillLabel = escapeHtml(listenerCardStatusPillLabel(st));
const typeMark = escapeHtml(listenerTypeShortLabel(l.type));
const typeVis = listenerTypeVisualClass(l.type);
const fullType = escapeHtml(listenerTypeLabel(l.type));
const bindVal = escapeHtml(String(l.bindHost)) + ':' + escapeHtml(String(l.bindPort));
return `
<article class="c2-listener-card c2-listener-card--${stUi}" data-listener-id="${escapeHtml(l.id)}">
<div class="c2-listener-card-head">
<div class="c2-ltype-mark ${typeVis}" title="${fullType}"><span>${typeMark}</span></div>
<div class="c2-listener-card-head-main">
<div class="c2-listener-card-title-row">
<h3 class="c2-listener-name">${escapeHtml(l.name)}</h3>
<span class="c2-listener-pill c2-listener-pill--${stUi}">${pillLabel}</span>
</div>
<div class="c2-listener-id-row">
<code class="c2-listener-id-full" title="${escapeHtml(l.id)}">${escapeHtml(l.id)}</code>
</div>
</div>
</div>
<div class="c2-listener-card-body">
<div class="c2-listener-kv">
<span class="c2-listener-kv-label">${escapeHtml(c2t('c2.listeners.bindEndpoint'))}</span>
<span class="c2-listener-kv-val c2-listener-mono"><span class="c2-status-dot ${escapeHtml(st)}"></span>${bindVal}</span>
</div>
${cbRow}
${profileBadge}
${remarkRow}
${startedHtml}
</div>
<div class="c2-listener-card-actions">
${l.status === 'stopped'
? `<button type="button" class="btn-primary btn-sm" onclick="C2.startListener('${l.id}')">▶ ${escapeHtml(c2t('c2.listeners.start'))}</button>`
: `<button type="button" class="btn-secondary btn-sm" onclick="C2.stopListener('${l.id}')">⏹ ${escapeHtml(c2t('c2.listeners.stop'))}</button>`
}
<button type="button" class="btn-secondary btn-sm" onclick="C2.editListener('${l.id}')">${escapeHtml(c2t('c2.listeners.edit'))}</button>
<button type="button" class="btn-danger btn-sm" onclick="C2.deleteListener('${l.id}')">${escapeHtml(c2t('c2.listeners.delete'))}</button>
</div>
</article>`;
}).join('');
};
C2.getListenerCallbackHost = function(l) {
if (!l) return '';
try {
var raw = l.configJson != null ? l.configJson : '{}';
var j = typeof raw === 'string' ? JSON.parse(raw || '{}') : (raw || {});
return String(j.callback_host || '').trim();
} catch (e) {
return '';
}
};
C2.showCreateListenerModal = function() {
const modal = document.getElementById('c2-modal');
const content = document.getElementById('c2-modal-content');
if (!content || !modal) return;
modal.style.display = 'flex';
content.innerHTML = `
<div class="c2-modal-header">
<h3>${escapeHtml(c2t('c2.listeners.modalCreateTitle'))}</h3>
<button class="c2-modal-close" onclick="C2.closeModal()">&times;</button>
</div>
<div class="c2-modal-body">
<p class="form-hint" style="margin-top:0;">${escapeHtml(c2t('c2.listeners.loadingProfiles'))}</p>
</div>
`;
C2.ensureProfilesLoaded().then(() => {
const profileOpts = listenerProfileSelectHtml('');
const emptyProfHintCreate = (C2.profiles && C2.profiles.length > 0)
? ''
: `<div class="form-hint" style="margin-bottom:6px;color:#b45309;">${escapeHtml(c2t('c2.listeners.malleableProfileEmptyListHint'))}</div>`;
content.innerHTML = `
<div class="c2-modal-header">
<h3>${escapeHtml(c2t('c2.listeners.modalCreateTitle'))}</h3>
<button class="c2-modal-close" onclick="C2.closeModal()">&times;</button>
</div>
<div class="c2-modal-body">
<div class="c2-form-row">
<div class="c2-form-group">
<label>${escapeHtml(c2t('c2.listeners.name'))}</label>
<input type="text" id="c2-listener-name" class="form-control" placeholder="${escapeHtml(c2t('c2.listeners.placeholderNameExample'))}">
</div>
<div class="c2-form-group">
<label>${escapeHtml(c2t('c2.listeners.type'))}</label>
<select id="c2-listener-type" class="form-control c2-native-select" onchange="C2.syncListenerProfileRowForType()">
<option value="http_beacon">HTTP Beacon</option>
<option value="https_beacon">HTTPS Beacon</option>
<option value="tcp_reverse">TCP Reverse</option>
<option value="websocket">WebSocket</option>
</select>
</div>
</div>
<div class="c2-form-row">
<div class="c2-form-group">
<label>${escapeHtml(c2t('c2.listeners.bindHost'))}</label>
<input type="text" id="c2-listener-host" class="form-control" value="127.0.0.1">
<div class="form-hint">${escapeHtml(c2t('c2.listeners.bindHintExternal'))}</div>
</div>
<div class="c2-form-group">
<label>${escapeHtml(c2t('c2.listeners.bindPort'))}</label>
<input type="number" id="c2-listener-port" class="form-control" placeholder="8443">
</div>
</div>
<div class="c2-form-group" id="c2-listener-profile-group">
<label>${escapeHtml(c2t('c2.listeners.malleableProfile'))}</label>
${emptyProfHintCreate}
<select id="c2-listener-profile-id" class="form-control c2-native-select">${profileOpts}</select>
<div class="form-hint">${escapeHtml(c2t('c2.listeners.malleableProfileHint'))}</div>
</div>
<div class="c2-form-group">
<label>${escapeHtml(c2t('c2.listeners.callbackHost'))}</label>
<input type="text" id="c2-listener-callback-host" class="form-control" placeholder="">
<div class="form-hint">${escapeHtml(c2t('c2.listeners.callbackHostHint'))}</div>
</div>
<div class="c2-form-group">
<label>${escapeHtml(c2t('c2.listeners.remark'))}</label>
<input type="text" id="c2-listener-remark" class="form-control" placeholder="${escapeHtml(c2t('c2.listeners.placeholderRemarkLong'))}">
</div>
</div>
<div class="c2-modal-footer">
<button class="btn-secondary" onclick="C2.closeModal()">${escapeHtml(c2t('common.cancel'))}</button>
<button class="btn-primary" onclick="C2.createListener()">${escapeHtml(c2t('c2.listeners.submitCreate'))}</button>
</div>
`;
C2.syncListenerProfileRowForType();
}).catch(() => {
showToast(c2t('c2.listeners.toastProfilesLoadFailed'), 'error');
C2.closeModal();
});
};
/** 非 HTTP/HTTPS Beacon 时隐藏 Profile 行(避免误以为 TCP 等也会用) */
C2.syncListenerProfileRowForType = function() {
const typeEl = document.getElementById('c2-listener-type');
const row = document.getElementById('c2-listener-profile-group');
if (!typeEl || !row) return;
const t = String(typeEl.value || '').toLowerCase();
const show = t === 'http_beacon' || t === 'https_beacon';
row.style.display = show ? '' : 'none';
if (!show) {
const sel = document.getElementById('c2-listener-profile-id');
if (sel) sel.value = '';
}
};
C2.createListener = function() {
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';
const bindPort = parseInt(document.getElementById('c2-listener-port')?.value);
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();
apiRequest('POST', `${API_BASE}/listeners`, {
name, type, bind_host: bindHost, bind_port: bindPort, remark,
callback_host: callbackHost,
profile_id: profileId
}).then(data => {
if (data.error) {
showToast(data.error, 'error');
} else {
showToast(c2t('c2.listeners.toastCreated'), 'success');
C2.closeModal();
C2.loadListeners();
}
});
};
C2.startListener = function(id) {
apiRequest('POST', `${API_BASE}/listeners/${id}/start`, {}).then(data => {
if (data.error) showToast(data.error, 'error');
else {
showToast(c2t('c2.listeners.toastStarted'), 'success');
C2.loadListeners();
}
});
};
C2.stopListener = function(id) {
apiRequest('POST', `${API_BASE}/listeners/${id}/stop`, {}).then(data => {
if (data.error) showToast(data.error, 'error');
else {
showToast(c2t('c2.listeners.toastStopped'), 'success');
C2.loadListeners();
}
});
};
C2.deleteListener = function(id) {
if (!confirm(c2t('c2.listeners.confirmDelete'))) return;
apiRequest('DELETE', `${API_BASE}/listeners/${id}`, {}).then(data => {
showToast(c2t('c2.listeners.toastDeleted'), 'success');
C2.loadListeners();
});
};
C2.editListener = function(id) {
const l = C2.listeners.find(x => x.id === id);
if (!l) return;
const cbHost = C2.getListenerCallbackHost(l);
const modal = document.getElementById('c2-modal');
const content = document.getElementById('c2-modal-content');
if (!content || !modal) return;
modal.style.display = 'flex';
content.innerHTML = `
<div class="c2-modal-header">
<h3>${escapeHtml(c2t('c2.listeners.editTitle'))}</h3>
<button class="c2-modal-close" onclick="C2.closeModal()">&times;</button>
</div>
<div class="c2-modal-body">
<p class="form-hint" style="margin-top:0;">${escapeHtml(c2t('c2.listeners.loadingProfiles'))}</p>
</div>
`;
C2.ensureProfilesLoaded().then(() => {
const resolvedPid = listenerResolvedProfileId(l);
const profileOpts = listenerProfileSelectHtml(resolvedPid);
const lt = String(l.type || '').toLowerCase();
const httpHint = (lt === 'http_beacon' || lt === 'https_beacon')
? ''
: `<div class="form-hint" style="margin-bottom:6px;">${escapeHtml(c2t('c2.listeners.malleableProfileNonHttpHint'))}</div>`;
const emptyProfHint = (C2.profiles && C2.profiles.length > 0)
? ''
: `<div class="form-hint" style="margin-bottom:6px;color:#b45309;">${escapeHtml(c2t('c2.listeners.malleableProfileEmptyListHint'))}</div>`;
content.innerHTML = `
<div class="c2-modal-header">
<h3>${escapeHtml(c2t('c2.listeners.editTitle'))}</h3>
<button class="c2-modal-close" onclick="C2.closeModal()">&times;</button>
</div>
<div class="c2-modal-body">
<div class="c2-form-group">
<label>${escapeHtml(c2t('c2.listeners.name'))}</label>
<input type="text" id="c2-listener-name" class="form-control" value="${escapeHtml(l.name)}">
</div>
<div class="c2-form-row">
<div class="c2-form-group">
<label>${escapeHtml(c2t('c2.listeners.bindHost'))}</label>
<input type="text" id="c2-listener-host" class="form-control" value="${escapeHtml(String(l.bindHost))}">
</div>
<div class="c2-form-group">
<label>${escapeHtml(c2t('c2.listeners.bindPort'))}</label>
<input type="number" id="c2-listener-port" class="form-control" value="${l.bindPort}">
</div>
</div>
<div class="c2-form-group" id="c2-listener-profile-group">
<label>${escapeHtml(c2t('c2.listeners.malleableProfile'))}</label>
${httpHint}${emptyProfHint}
<select id="c2-listener-profile-id" class="form-control c2-native-select">${profileOpts}</select>
<div class="form-hint">${escapeHtml(c2t('c2.listeners.malleableProfileHint'))}</div>
</div>
<div class="c2-form-group">
<label>${escapeHtml(c2t('c2.listeners.callbackHost'))}</label>
<input type="text" id="c2-listener-callback-host" class="form-control" value="${escapeHtml(cbHost)}">
<div class="form-hint">${escapeHtml(c2t('c2.listeners.callbackHostHint'))}</div>
</div>
<div class="c2-form-group">
<label>${escapeHtml(c2t('c2.listeners.remark'))}</label>
<input type="text" id="c2-listener-remark" class="form-control" value="${escapeHtml(l.remark || '')}">
</div>
</div>
<div class="c2-modal-footer">
<button class="btn-secondary" onclick="C2.closeModal()">${escapeHtml(c2t('common.cancel'))}</button>
<button class="btn-primary" onclick="C2.saveListener('${l.id}')">${escapeHtml(c2t('common.save'))}</button>
</div>
`;
}).catch(() => {
showToast(c2t('c2.listeners.toastProfilesLoadFailed'), 'error');
C2.closeModal();
});
};
C2.saveListener = function(id) {
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);
const callbackHost = document.getElementById('c2-listener-callback-host')?.value?.trim() ?? '';
const remark = document.getElementById('c2-listener-remark')?.value;
const profileEl = document.getElementById('c2-listener-profile-id');
const profileId = profileEl ? String(profileEl.value || '').trim() : '';
apiRequest('PUT', `${API_BASE}/listeners/${id}`, {
name, bind_host: bindHost, bind_port: bindPort, remark,
callback_host: callbackHost,
profile_id: profileId
}).then(data => {
if (data.error) showToast(data.error, 'error');
else {
showToast(c2t('c2.listeners.toastUpdated'), 'success');
C2.closeModal();
C2.loadListeners();
}
});
};
// ============================================================================
// 会话管理
// ============================================================================
C2.loadSessions = function() {
return apiRequest('GET', `${API_BASE}/sessions`).then(data => {
C2.sessions = data.sessions || [];
C2.renderSessions();
C2.updateDashboardStats();
});
};
C2.renderSessions = function() {
const list = document.getElementById('c2-session-list');
const main = document.getElementById('c2-session-main');
if (!list) return;
if (C2.sessions.length === 0) {
list.innerHTML = `
<div class="c2-empty" style="padding:40px 20px;">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="1.2" style="margin-bottom:16px;opacity:0.5;">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
<line x1="8" y1="21" x2="16" y2="21"></line>
<line x1="12" y1="17" x2="12" y2="21"></line>
</svg>
<h3 style="font-size:16px;font-weight:700;margin-bottom:6px;">${escapeHtml(c2t('c2.sessions.emptyTitle'))}</h3>
<p style="font-size:13px;">${escapeHtml(c2t('c2.sessions.emptyHint'))}</p>
</div>`;
if (main) main.innerHTML = '';
return;
}
list.innerHTML = C2.sessions.map(s => `
<div class="c2-session-item ${s.id === C2.selectedSessionId ? 'active' : ''}"
onclick="C2.selectSession('${s.id}')">
<div class="c2-session-header">
<span class="c2-session-host">${escapeHtml(s.hostname || c2t('c2.sessions.unknownHost'))}</span>
<span class="c2-session-status ${s.status}">${escapeHtml(sessionStatusLabel(s.status))}</span>
</div>
<div class="c2-session-meta">
${escapeHtml(s.username)} · ${s.os}/${s.arch}
${s.isAdmin ? '<span style="color:#f59e0b;font-weight:700;margin-left:4px;">' + escapeHtml(c2t('c2.sessions.rootBadge')) + '</span>' : ''}
</div>
<div class="c2-session-meta" style="font-size:11px;margin-top:2px;">
${s.internalIp || '-'} · PID ${s.pid}
</div>
<div class="c2-session-item-footer">
<span class="c2-session-meta c2-session-item-time">${formatTime(s.lastCheckIn)}</span>
<button type="button" class="c2-session-card-delete" onclick="event.stopPropagation(); C2.deleteSessionRecord('${s.id}');">${escapeHtml(c2t('c2.sessions.cardDeleteSession'))}</button>
</div>
</div>
`).join('');
if (C2.selectedSessionId && !C2.sessions.find(s => s.id === C2.selectedSessionId)) {
C2.selectedSessionId = null;
}
if (!C2.selectedSessionId && C2.sessions.length > 0) {
C2.selectSession(C2.sessions[0].id);
}
};
C2.selectSession = function(id) {
C2.selectedSessionId = id;
C2.renderSessions();
C2.renderSessionDetail(id);
C2.initTerminal();
};
C2.renderSessionDetail = function(id) {
const container = document.getElementById('c2-session-main');
if (!container) return;
const s = C2.sessions.find(x => x.id === id);
if (!s) return;
const adminVal = s.isAdmin ? c2t('c2.sessions.adminYes') : c2t('c2.sessions.adminNo');
const sleepLine = c2t('c2.sessions.infoSleepLine', { sec: s.sleepSeconds, jitter: s.jitterPercent });
container.innerHTML = `
<div class="c2-session-detail">
<div class="c2-session-header-bar">
<div class="c2-session-title">
<h3>${escapeHtml(s.hostname)} <span class="c2-session-badge ${s.status}">${escapeHtml(sessionStatusLabel(s.status))}</span></h3>
<div class="c2-session-subtitle">${s.id} | ${escapeHtml(s.username)}@${s.os}/${s.arch}</div>
</div>
<div class="c2-session-actions">
<button class="btn-secondary btn-sm" onclick="C2.setSessionSleep('${s.id}')">${escapeHtml(c2t('c2.sessions.btnSleep'))}</button>
<button class="btn-danger btn-sm" onclick="C2.killSession('${s.id}')">${escapeHtml(c2t('c2.sessions.kill'))}</button>
</div>
</div>
<div class="c2-session-tabs">
<div class="c2-session-tab active" data-tab="terminal" onclick="C2.switchTab('terminal')">${escapeHtml(c2t('c2.sessions.terminal'))}</div>
<div class="c2-session-tab" data-tab="files" onclick="C2.switchTab('files')">${escapeHtml(c2t('c2.sessions.files'))}</div>
<div class="c2-session-tab" data-tab="tasks" onclick="C2.switchTab('tasks')">${escapeHtml(c2t('c2.sessions.tasks'))}</div>
<div class="c2-session-tab" data-tab="info" onclick="C2.switchTab('info')">${escapeHtml(c2t('c2.sessions.info'))}</div>
</div>
<div class="c2-session-tab-content">
<div id="c2-tab-terminal" class="c2-tab-panel active">
<div id="c2-terminal-container" class="c2-terminal-container"></div>
<div class="c2-terminal-toolbar">
<button class="btn-ghost btn-sm" onclick="C2.clearTerminal()">${escapeHtml(c2t('c2.sessions.clearTerminal'))}</button>
<button class="btn-ghost btn-sm" onclick="C2.copyTerminal()">${escapeHtml(c2t('common.copy'))}</button>
<span class="c2-terminal-status" id="c2-terminal-status">${escapeHtml(c2t('c2.sessions.termStatusReady'))}</span>
</div>
</div>
<div id="c2-tab-files" class="c2-tab-panel" style="display:none;">
<div class="c2-file-toolbar">
<button class="btn-ghost btn-sm" onclick="C2.loadFileList('..')">⬆ ${escapeHtml(c2t('c2.files.parent'))}</button>
<button class="btn-ghost btn-sm" onclick="C2.refreshFiles()">${escapeHtml(c2t('c2.files.refresh'))}</button>
<span id="c2-current-path" class="c2-path-breadcrumb">/</span>
</div>
<div id="c2-file-list" class="c2-file-list"></div>
</div>
<div id="c2-tab-tasks" class="c2-tab-panel" style="display:none;">
<div id="c2-session-tasks-list" class="c2-task-list-compact"></div>
</div>
<div id="c2-tab-info" class="c2-tab-panel" style="display:none;">
<div class="c2-info-grid">
<div><strong>${escapeHtml(c2t('c2.sessions.infoSessionId'))}:</strong> ${s.id}</div>
<div><strong>${escapeHtml(c2t('c2.sessions.infoImplantUuid'))}:</strong> ${s.implantUuid}</div>
<div><strong>${escapeHtml(c2t('c2.sessions.infoHostname'))}:</strong> ${escapeHtml(s.hostname)}</div>
<div><strong>${escapeHtml(c2t('c2.sessions.infoUsername'))}:</strong> ${escapeHtml(s.username)}</div>
<div><strong>${escapeHtml(c2t('c2.sessions.infoOs'))}:</strong> ${s.os}</div>
<div><strong>${escapeHtml(c2t('c2.sessions.infoArch'))}:</strong> ${s.arch}</div>
<div><strong>${escapeHtml(c2t('c2.sessions.infoPid'))}:</strong> ${s.pid}</div>
<div><strong>${escapeHtml(c2t('c2.sessions.infoProcess'))}:</strong> ${escapeHtml(s.processName || '-')}</div>
<div><strong>${escapeHtml(c2t('c2.sessions.infoAdmin'))}:</strong> ${escapeHtml(adminVal)}</div>
<div><strong>${escapeHtml(c2t('c2.sessions.infoInternalIp'))}:</strong> ${s.internalIp || '-'}</div>
<div><strong>${escapeHtml(c2t('c2.sessions.infoSleep'))}:</strong> ${escapeHtml(sleepLine)}</div>
<div><strong>${escapeHtml(c2t('c2.sessions.infoFirstSeen'))}:</strong> ${formatTime(s.firstSeenAt)}</div>
<div><strong>${escapeHtml(c2t('c2.sessions.infoLastCheckin'))}:</strong> ${formatTime(s.lastCheckIn)}</div>
<div><strong>${escapeHtml(c2t('c2.sessions.infoNote'))}:</strong> ${escapeHtml(s.note || '-')}</div>
</div>
</div>
</div>
</div>
`;
var isCurlBeacon = s.implantUuid && s.implantUuid.startsWith('curl_');
if (isCurlBeacon) {
var termContainer = container.querySelector('#c2-terminal-container');
if (termContainer) {
termContainer.innerHTML =
'<div style="padding:24px;color:#94a3b8;text-align:center;line-height:1.8;">' +
'<div style="font-size:32px;margin-bottom:12px;">📡</div>' +
'<div style="font-size:14px;font-weight:600;color:#e2e8f0;margin-bottom:8px;">' + escapeHtml(c2t('c2.sessions.curlBeaconTitle')) + '</div>' +
'<div style="font-size:12px;">' + c2t('c2.sessions.curlBeaconBody').split('\n').map(function (ln) { return escapeHtml(ln); }).join('<br>') + '</div>' +
'</div>';
}
}
setTimeout(() => {
if (!isCurlBeacon) C2.initTerminal();
C2.loadFileList(s.id, '.');
C2.loadSessionTasks(s.id);
}, 50);
};
C2.switchTab = function(tab) {
document.querySelectorAll('.c2-session-tab').forEach(el => el.classList.remove('active'));
document.querySelectorAll('.c2-tab-panel').forEach(el => el.style.display = 'none');
const tabEl = document.querySelector(`.c2-session-tab[data-tab="${tab}"]`);
if (tabEl) tabEl.classList.add('active');
const panel = document.getElementById(`c2-tab-${tab}`);
if (panel) panel.style.display = 'block';
if (tab === 'terminal') {
setTimeout(() => C2.fitTerminal(), 50);
}
};
C2.setSessionSleep = function(id) {
const sleep = prompt(c2t('c2.sessions.promptSleepSeconds'), '5');
if (!sleep) return;
const jitter = prompt(c2t('c2.sessions.promptJitterPercent'), '0') || '0';
apiRequest('PUT', `${API_BASE}/sessions/${id}/sleep`, {
sleep_seconds: parseInt(sleep),
jitter_percent: parseInt(jitter)
}).then(data => {
if (data.error) showToast(data.error, 'error');
else showToast(c2t('c2.sessions.toastSleepUpdated'), 'success');
});
};
C2.killSession = function(id) {
if (!confirm(c2t('c2.sessions.confirmExitSession'))) return;
apiRequest('POST', `${API_BASE}/tasks`, {
session_id: id,
task_type: 'exit',
payload: {}
}).then(data => {
showToast(c2t('c2.sessions.toastExitSent'), 'success');
});
};
C2.deleteSessionRecord = function(id) {
if (!confirm(c2t('c2.sessions.confirmDeleteSession'))) return;
apiRequest('DELETE', `${API_BASE}/sessions/${id}`, {}).then(data => {
if (data.error) {
showToast(data.error, 'error');
return;
}
showToast(c2t('c2.sessions.toastSessionDeleted'), 'success');
if (C2.selectedSessionId === id) C2.selectedSessionId = null;
C2.loadSessions();
});
};
// ============================================================================
// xterm 终端
// ============================================================================
C2.initTerminal = function() {
const container = document.getElementById('c2-terminal-container');
if (!container || typeof Terminal === 'undefined') return;
if (C2.terminalInstance) {
C2.terminalInstance.dispose();
}
const term = new Terminal({
cursorBlink: true,
cursorStyle: 'block',
fontSize: 14,
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
lineHeight: 1.3,
scrollback: 5000,
theme: {
background: '#0d1117',
foreground: '#e6edf3',
cursor: '#58a6ff',
selection: 'rgba(88, 166, 255, 0.3)'
}
});
if (typeof FitAddon !== 'undefined') {
const FitCtor = FitAddon.FitAddon || FitAddon;
C2.terminalFitAddon = new FitCtor();
term.loadAddon(C2.terminalFitAddon);
}
term.open(container);
try {
if (C2.terminalFitAddon) C2.terminalFitAddon.fit();
} catch (e) {}
let lineBuffer = '';
const prompt = '$ ';
term.writeln('\x1b[36m' + c2t('c2.sessions.terminalWelcome') + '\x1b[0m');
term.writeln('');
term.write(prompt);
term.onData(e => {
const code = e.charCodeAt(0);
if (code === 13) { // Enter
term.writeln('');
const cmd = lineBuffer.trim();
lineBuffer = '';
if (cmd) {
C2.executeInTerminal(cmd, term);
} else {
term.write(prompt);
}
} else if (code === 127) { // Backspace
if (lineBuffer.length > 0) {
lineBuffer = lineBuffer.slice(0, -1);
term.write('\b \b');
}
} else if (code >= 32) { // Printable
lineBuffer += e;
term.write(e);
}
});
C2.terminalInstance = term;
// Resize observer
if (C2.terminalResizeObserver) {
C2.terminalResizeObserver.disconnect();
}
C2.terminalResizeObserver = new ResizeObserver(() => {
C2.fitTerminal();
});
C2.terminalResizeObserver.observe(container);
};
C2.fitTerminal = function() {
if (C2.terminalFitAddon && C2.terminalInstance) {
try {
C2.terminalFitAddon.fit();
} catch (e) {}
}
};
C2.executeInTerminal = function(cmd, term) {
if (!C2.selectedSessionId) {
term.writeln('\x1b[31m' + c2t('c2.sessions.termNoSession') + '\x1b[0m');
term.write('$ ');
return;
}
const statusEl = document.getElementById('c2-terminal-status');
if (statusEl) statusEl.textContent = c2t('c2.sessions.termStatusExec');
apiRequest('POST', `${API_BASE}/tasks`, {
session_id: C2.selectedSessionId,
task_type: 'shell',
payload: { command: cmd, timeout_seconds: 60 }
}).then(data => {
if (data.error) {
term.writeln(`\x1b[31mError: ${data.error}\x1b[0m`);
term.write('$ ');
if (statusEl) statusEl.textContent = c2t('c2.sessions.termStatusErr');
} else {
C2.waitForTaskResult(data.task?.id || data.task_id, term);
}
});
};
C2.waitForTaskResult = function(taskId, term) {
let attempts = 0;
const maxAttempts = 60;
let delay = 500;
const maxDelay = 5000;
const check = () => {
if (++attempts > maxAttempts) {
term.writeln('\x1b[33m' + c2t('c2.sessions.termWaitTimeout') + '\x1b[0m');
term.write('$ ');
const statusEl = document.getElementById('c2-terminal-status');
if (statusEl) statusEl.textContent = c2t('c2.sessions.termStatusTimeout');
return;
}
apiRequest('GET', `${API_BASE}/tasks/${taskId}`).then(data => {
const task = data.task;
if (task && (task.status === 'success' || task.status === 'failed')) {
if (task.resultText) {
const lines = task.resultText.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
lines.forEach(line => term.writeln(line));
}
if (task.error) {
term.writeln(`\x1b[31m${task.error}\x1b[0m`);
}
term.write('$ ');
const statusEl = document.getElementById('c2-terminal-status');
if (statusEl) statusEl.textContent = c2t('c2.sessions.termStatusReady');
} else {
delay = Math.min(delay * 1.5, maxDelay);
setTimeout(check, delay);
}
});
};
check();
};
C2.clearTerminal = function() {
if (C2.terminalInstance) {
C2.terminalInstance.clear();
C2.terminalInstance.writeln('\x1b[36m' + c2t('c2.sessions.termCleared') + '\x1b[0m');
C2.terminalInstance.write('$ ');
}
};
C2.copyTerminal = function() {
if (!C2.terminalInstance) return;
const text = C2.terminalInstance.getSelection();
if (text) copyToClipboard(text);
else showToast(c2t('c2.sessions.termNoSelection'), 'warning');
};
// ============================================================================
// 文件管理
// ============================================================================
C2.loadFileList = function(sessionId, path) {
if (!sessionId) sessionId = C2.selectedSessionId;
if (!sessionId) return;
if (!path) path = C2.currentPath || '.';
const container = document.getElementById('c2-file-list');
const breadcrumb = document.getElementById('c2-current-path');
if (container) container.innerHTML = '<div class="c2-loading">' + escapeHtml(c2t('c2.files.loading')) + '</div>';
apiRequest('POST', `${API_BASE}/tasks`, {
session_id: sessionId,
task_type: 'ls',
payload: { path: path }
}).then(data => {
if (data.error) {
if (container) container.innerHTML = `<div class="c2-error">${data.error}</div>`;
return;
}
C2.waitForFileList(data.task?.id || data.task_id, sessionId, path);
});
};
C2.waitForFileList = function(taskId, sessionId, path) {
let attempts = 0;
const container = document.getElementById('c2-file-list');
const check = () => {
if (++attempts > 60) {
if (container) container.innerHTML = '<div class="c2-error">' + escapeHtml(c2t('c2.files.timeout')) + '</div>';
return;
}
apiRequest('GET', `${API_BASE}/tasks/${taskId}`).then(data => {
const task = data.task;
if (task && task.status === 'success') {
C2.currentPath = path;
const breadcrumb = document.getElementById('c2-current-path');
if (breadcrumb) breadcrumb.textContent = path;
C2.renderFileList(task.resultText || '');
} else if (task && task.status === 'failed') {
if (container) container.innerHTML = `<div class="c2-error">${escapeHtml(task.error || c2t('c2.files.failed'))}</div>`;
} else {
setTimeout(check, 500);
}
});
};
check();
};
C2.renderFileList = function(output) {
const container = document.getElementById('c2-file-list');
if (!container) return;
const lines = output.split('\n').filter(l => l.trim());
if (lines.length === 0) {
container.innerHTML = '<div class="c2-empty">' + escapeHtml(c2t('c2.files.emptyDir')) + '</div>';
return;
}
container.innerHTML = `
<table class="c2-file-table">
<thead>
<tr>
<th>${escapeHtml(c2t('c2.files.colName'))}</th>
<th>${escapeHtml(c2t('c2.files.colSize'))}</th>
<th>${escapeHtml(c2t('c2.files.colMode'))}</th>
<th>${escapeHtml(c2t('c2.files.colActions'))}</th>
</tr>
</thead>
<tbody>
${lines.map(line => {
const parts = line.split(/\s+/);
const name = parts[parts.length - 1] || line;
const isDir = line.startsWith('d') || parts[0]?.startsWith?.('d');
return `
<tr>
<td class="c2-file-name">
<span class="c2-file-icon">${isDir ? '📁' : '📄'}</span>
${escapeHtml(name)}
</td>
<td>${parts[parts.length - 5] || '-'}</td>
<td>${parts[parts.length - 4] || '-'}</td>
<td>
${isDir
? `<button class="btn-ghost btn-sm" onclick="C2.loadFileList(null, '${escapeHtml(name)}')">${escapeHtml(c2t('c2.files.open'))}</button>`
: `<button class="btn-ghost btn-sm" onclick="C2.downloadFile('${escapeHtml(name)}')">${escapeHtml(c2t('c2.files.download'))}</button>`
}
</td>
</tr>
`;
}).join('')}
</tbody>
</table>
`;
};
C2.refreshFiles = function() {
C2.loadFileList(null, C2.currentPath);
};
C2.downloadFile = function(filename) {
if (!C2.selectedSessionId) return;
const remotePath = C2.currentPath === '/' ? '/' + filename : C2.currentPath + '/' + filename;
apiRequest('POST', `${API_BASE}/tasks`, {
session_id: C2.selectedSessionId,
task_type: 'download',
payload: { remote_path: remotePath }
}).then(data => {
if (data.error) showToast(data.error, 'error');
else showToast(c2t('c2.payloads.toastDownloadQueued'), 'success');
});
};
// ============================================================================
// 任务管理
// ============================================================================
C2.loadTasks = function(page) {
const p = page != null ? page : (C2.tasksPage || 1);
C2.tasksPage = p;
const ps = C2.tasksPageSize || 10;
apiRequest('GET', `${API_BASE}/tasks?page=${encodeURIComponent(String(p))}&page_size=${encodeURIComponent(String(ps))}`).then(data => {
if (data.error) {
showToast(String(data.error), 'error');
return;
}
C2.tasks = data.tasks || [];
C2.tasksTotal = typeof data.total === 'number' ? data.total : (C2.tasks.length || 0);
if (typeof data.pending_queued_count === 'number') {
C2.tasksPendingQueuedCount = data.pending_queued_count;
}
const maxPage = Math.max(1, Math.ceil(C2.tasksTotal / ps));
if (p > maxPage) {
C2.loadTasks(maxPage);
return;
}
C2.renderTasks();
C2.renderTasksPagination();
C2.syncTasksToolbar();
C2.updateDashboardStats();
}).catch(err => {
showToast(err.message || String(err), 'error');
});
};
C2.goTasksPage = function(targetPage) {
const totalPages = Math.max(1, Math.ceil((C2.tasksTotal || 0) / (C2.tasksPageSize || 10)));
if (targetPage < 1 || targetPage > totalPages) return;
C2.loadTasks(targetPage);
const list = document.getElementById('c2-task-list');
if (list) list.scrollIntoView({ behavior: 'smooth', block: 'start' });
};
C2.changeTasksPageSize = function() {
const sel = document.getElementById('c2-tasks-page-size-pagination');
if (!sel) return;
const n = parseInt(sel.value, 10);
if (n > 0) {
C2.tasksPageSize = n;
C2.loadTasks(1);
}
};
C2.renderTasksPagination = function() {
const paginationContainer = document.getElementById('c2-tasks-pagination');
if (!paginationContainer) return;
const total = C2.tasksTotal || 0;
const currentPage = C2.tasksPage || 1;
const pageSize = C2.tasksPageSize || 10;
const totalPages = Math.max(1, Math.ceil(total / pageSize));
if (total === 0) {
paginationContainer.innerHTML = '';
return;
}
const start = total === 0 ? 0 : (currentPage - 1) * pageSize + 1;
const end = Math.min(currentPage * pageSize, total);
let html = '<div class="monitor-pagination">';
html += `
<div class="pagination-info">
<span>${escapeHtml(c2t('c2.tasks.paginationShow', { start, end, total }))}</span>
<label class="pagination-page-size">
${escapeHtml(c2t('c2.tasks.paginationPerPage'))}
<select id="c2-tasks-page-size-pagination" onchange="C2.changeTasksPageSize()">
<option value="10" ${pageSize === 10 ? 'selected' : ''}>10</option>
<option value="20" ${pageSize === 20 ? 'selected' : ''}>20</option>
<option value="50" ${pageSize === 50 ? 'selected' : ''}>50</option>
<option value="100" ${pageSize === 100 ? 'selected' : ''}>100</option>
</select>
</label>
</div>
<div class="pagination-controls">
<button type="button" class="btn-secondary" onclick="C2.goTasksPage(1)" ${currentPage === 1 ? 'disabled' : ''}>${escapeHtml(c2t('c2.tasks.paginationFirst'))}</button>
<button type="button" class="btn-secondary" onclick="C2.goTasksPage(${currentPage - 1})" ${currentPage === 1 ? 'disabled' : ''}>${escapeHtml(c2t('c2.tasks.paginationPrev'))}</button>
<span class="pagination-page">${escapeHtml(c2t('c2.tasks.paginationPage', { current: currentPage, total: totalPages }))}</span>
<button type="button" class="btn-secondary" onclick="C2.goTasksPage(${currentPage + 1})" ${currentPage >= totalPages ? 'disabled' : ''}>${escapeHtml(c2t('c2.tasks.paginationNext'))}</button>
<button type="button" class="btn-secondary" onclick="C2.goTasksPage(${totalPages})" ${currentPage >= totalPages ? 'disabled' : ''}>${escapeHtml(c2t('c2.tasks.paginationLast'))}</button>
</div>
`;
html += '</div>';
paginationContainer.innerHTML = html;
if (typeof applyTranslations === 'function') applyTranslations(paginationContainer);
};
C2.collectCheckedTaskIds = function() {
return Array.from(document.querySelectorAll('.c2-task-row-check:checked')).map(cb => cb.getAttribute('data-id')).filter(Boolean);
};
C2.syncTasksToolbar = function() {
const batchBtn = document.getElementById('c2-tasks-batch-delete');
const ids = C2.collectCheckedTaskIds();
if (batchBtn) batchBtn.disabled = ids.length === 0;
const all = document.querySelectorAll('.c2-task-row-check');
const selAll = document.getElementById('c2-tasks-select-all');
if (selAll && all.length) {
const nChecked = document.querySelectorAll('.c2-task-row-check:checked').length;
selAll.checked = nChecked === all.length;
selAll.indeterminate = nChecked > 0 && nChecked < all.length;
} else if (selAll) {
selAll.checked = false;
selAll.indeterminate = false;
}
};
C2.onTasksSelectAll = function(checked) {
document.querySelectorAll('.c2-task-row-check').forEach(cb => { cb.checked = checked; });
C2.syncTasksToolbar();
};
C2.deleteTaskById = function(id) {
if (!id) return;
if (!confirm(c2t('c2.tasks.confirmDeleteOne'))) return;
apiRequest('DELETE', `${API_BASE}/tasks`, { ids: [id] }).then(data => {
if (data.error) {
showToast(String(data.error), 'error');
return;
}
showToast(c2t('c2.tasks.toastDeleted', { n: data.deleted != null ? data.deleted : 1 }), 'success');
C2.loadTasks(C2.tasksPage || 1);
}).catch(err => showToast(err.message || String(err), 'error'));
};
C2.deleteSelectedTasks = function() {
const ids = C2.collectCheckedTaskIds();
if (!ids.length) {
showToast(c2t('c2.tasks.toastSelectFirst'), 'warn');
return;
}
if (!confirm(c2t('c2.tasks.confirmBatchDelete', { n: ids.length }))) return;
apiRequest('DELETE', `${API_BASE}/tasks`, { ids }).then(data => {
if (data.error) {
showToast(String(data.error), 'error');
return;
}
const deleted = data.deleted != null ? data.deleted : ids.length;
showToast(c2t('c2.tasks.toastDeleted', { n: deleted }), 'success');
C2.loadTasks(C2.tasksPage || 1);
}).catch(err => showToast(err.message || String(err), 'error'));
};
C2.loadSessionTasks = function(sessionId) {
apiRequest('GET', `${API_BASE}/tasks?session_id=${encodeURIComponent(sessionId)}&limit=50`).then(data => {
const container = document.getElementById('c2-session-tasks-list');
const tasks = data.tasks || [];
if (typeof data.pending_queued_count === 'number') {
C2.tasksPendingQueuedCount = data.pending_queued_count;
C2.updateDashboardStats();
}
if (!container) return;
if (tasks.length === 0) {
container.innerHTML = '<div class="c2-empty">' + escapeHtml(c2t('c2.tasks.emptySession')) + '</div>';
return;
}
container.innerHTML = tasks.map(t => `
<div class="c2-task-item-compact">
<span class="c2-task-status-dot ${t.status}"></span>
<span class="c2-task-type">${t.taskType}</span>
<span class="c2-task-meta">${escapeHtml(taskStatusLabel(t.status))} | ${formatDuration(t.durationMs)}</span>
<button class="btn-ghost btn-sm" onclick="C2.viewTask('${t.id}')">${escapeHtml(c2t('c2.tasks.view'))}</button>
</div>
`).join('');
});
};
C2.renderTasks = function() {
const container = document.getElementById('c2-task-list');
if (!container) return;
const selAll = document.getElementById('c2-tasks-select-all');
if (selAll) {
selAll.checked = false;
selAll.indeterminate = false;
}
if (C2.tasks.length === 0) {
container.innerHTML = '<div class="c2-empty">' + escapeHtml(c2t('c2.tasks.emptyAll')) + '</div>';
if (selAll) selAll.disabled = true;
C2.syncTasksToolbar();
return;
}
if (selAll) selAll.disabled = false;
const delTitle = escapeHtml(c2t('c2.tasks.deleteOne'));
container.innerHTML = `
<table class="c2-task-table">
<thead>
<tr>
<th class="c2-task-table-col-check"></th>
<th>${escapeHtml(c2t('c2.tasks.colTask'))}</th>
<th>${escapeHtml(c2t('c2.tasks.colSession'))}</th>
<th>${escapeHtml(c2t('c2.tasks.colType'))}</th>
<th>${escapeHtml(c2t('c2.tasks.colStatus'))}</th>
<th>${escapeHtml(c2t('c2.tasks.colDuration'))}</th>
<th>${escapeHtml(c2t('c2.tasks.colCreated'))}</th>
<th>${escapeHtml(c2t('c2.tasks.colActions'))}</th>
</tr>
</thead>
<tbody>
${C2.tasks.map(t => {
const rawId = t.id || '';
const idJson = JSON.stringify(rawId);
const shortTaskId = rawId.length > 14 ? escapeHtml(rawId.substring(0, 12)) + '\u2026' : escapeHtml(rawId);
const sid = t.sessionId ? escapeHtml(String(t.sessionId).substring(0, 8)) + '\u2026' : '-';
return `
<tr>
<td class="c2-task-table-col-check">
<label class="c2-task-check-label" onclick="event.stopPropagation();">
<input type="checkbox" class="c2-task-row-check" data-id="${escapeHtml(rawId)}" onchange="C2.syncTasksToolbar()">
</label>
</td>
<td>${shortTaskId}</td>
<td>${sid}</td>
<td>${escapeHtml(t.taskType || '')}</td>
<td><span class="c2-status-badge ${escapeHtml(t.status || '')}">${escapeHtml(taskStatusLabel(t.status))}</span></td>
<td>${formatDuration(t.durationMs)}</td>
<td>${formatTime(t.createdAt)}</td>
<td>
<button type="button" class="btn-ghost btn-sm" onclick="C2.viewTask(${idJson})">${escapeHtml(c2t('c2.tasks.view'))}</button>
${t.status === 'queued' || t.status === 'sent'
? `<button type="button" class="btn-danger btn-sm" onclick="C2.cancelTask(${idJson})">${escapeHtml(c2t('c2.tasks.cancelBtn'))}</button>`
: ''}
<button type="button" class="btn-secondary btn-sm c2-task-row-delete" onclick="C2.deleteTaskById(${idJson})" title="${delTitle}" aria-label="${delTitle}">${escapeHtml(c2t('c2.tasks.deleteBtn'))}</button>
</td>
</tr>
`;
}).join('')}
</tbody>
</table>
`;
C2.syncTasksToolbar();
if (typeof applyTranslations === 'function') applyTranslations(container);
};
C2.viewTask = function(id) {
const modal = document.getElementById('c2-modal');
const content = document.getElementById('c2-modal-content');
if (!content) return;
const renderTaskModal = function(t) {
if (!t || !modal) return;
content.innerHTML = `
<div class="c2-modal-header">
<h3>${escapeHtml(c2t('c2.tasks.modalTitle'))}</h3>
<button class="c2-modal-close" onclick="C2.closeModal()">&times;</button>
</div>
<div class="c2-modal-body">
<div class="c2-task-detail">
<div><strong>${escapeHtml(c2t('c2.tasks.labelId'))}:</strong> ${t.id}</div>
<div><strong>${escapeHtml(c2t('c2.tasks.labelSession'))}:</strong> ${t.sessionId}</div>
<div><strong>${escapeHtml(c2t('c2.tasks.labelType'))}:</strong> ${t.taskType}</div>
<div><strong>${escapeHtml(c2t('c2.tasks.labelStatus'))}:</strong> <span class="c2-status-badge ${t.status}">${escapeHtml(taskStatusLabel(t.status))}</span></div>
<div><strong>${escapeHtml(c2t('c2.tasks.labelCreated'))}:</strong> ${formatTime(t.createdAt)}</div>
<div><strong>${escapeHtml(c2t('c2.tasks.labelSent'))}:</strong> ${formatTime(t.sentAt)}</div>
<div><strong>${escapeHtml(c2t('c2.tasks.labelCompleted'))}:</strong> ${formatTime(t.completedAt)}</div>
<div><strong>${escapeHtml(c2t('c2.tasks.labelDuration'))}:</strong> ${formatDuration(t.durationMs)}</div>
${t.error ? `<div class="c2-task-error"><strong>${escapeHtml(c2t('c2.tasks.labelError'))}:</strong> ${escapeHtml(t.error)}</div>` : ''}
${t.resultText ? `
<div class="c2-task-result">
<strong>${escapeHtml(c2t('c2.tasks.labelResult'))}:</strong>
<pre>${escapeHtml(t.resultText)}</pre>
</div>
` : ''}
</div>
</div>
<div class="c2-modal-footer">
<button class="btn-secondary" onclick="C2.closeModal()">${escapeHtml(c2t('common.close'))}</button>
</div>
`;
modal.style.display = 'flex';
};
const local = C2.tasks.find(x => x.id === id);
if (local) {
renderTaskModal(local);
return;
}
apiRequest('GET', `${API_BASE}/tasks/${id}`).then(data => {
if (data.task) renderTaskModal(data.task);
});
};
C2.cancelTask = function(id) {
apiRequest('POST', `${API_BASE}/tasks/${id}/cancel`, {}).then(data => {
if (data.error) showToast(data.error, 'error');
else {
showToast(c2t('c2.tasks.toastCancelled'), 'success');
C2.loadTasks(C2.tasksPage || 1);
}
});
};
// ============================================================================
// Payload 生成
// ============================================================================
C2.loadListenersForPayload = function() {
apiRequest('GET', `${API_BASE}/listeners`).then(data => {
if (data.error) {
showToast(data.error, 'error');
return;
}
C2.listeners = data.listeners || [];
C2.renderPayloadPage();
}).catch(err => {
showToast(c2t('c2.payloads.toastLoadListenersFail', { msg: err.message || '' }), 'error');
});
};
var onelinerKindsByListenerType = {
'tcp_reverse': [
{ value: 'bash', label: 'Bash (/dev/tcp)' },
{ value: 'nc', label: 'Netcat (-e)' },
{ value: 'nc_mkfifo', label: 'Netcat (mkfifo)' },
{ value: 'python', label: 'Python' },
{ value: 'perl', label: 'Perl' },
{ value: 'powershell', label: 'PowerShell' }
],
'http_beacon': [
{ value: 'curl_beacon', label: 'Curl Beacon (HTTP)' }
],
'https_beacon': [
{ value: 'curl_beacon', label: 'Curl Beacon (HTTP)' }
],
'websocket': [
{ value: 'curl_beacon', label: 'Curl Beacon (HTTP)' }
]
};
C2.updateOnelinerKinds = function() {
var listenerSelect = document.getElementById('c2-payload-listener');
var kindSelect = document.getElementById('c2-payload-kind');
if (!listenerSelect || !kindSelect) return;
var listenerId = listenerSelect.value;
var listener = (C2.listeners || []).find(function(l) { return l.id === listenerId; });
var ltype = listener ? listener.type : '';
var kinds = onelinerKindsByListenerType[ltype] || [];
if (kinds.length === 0) {
kindSelect.innerHTML = '<option value="">' + escapeHtml(c2t('c2.payloads.noKindOption')) + '</option>';
} else {
kindSelect.innerHTML = kinds.map(function(k) {
return '<option value="' + k.value + '">' + k.label + '</option>';
}).join('');
}
};
C2.updateLoopbackBuildHint = function() {
const sel = document.getElementById('c2-build-listener');
const hint = document.getElementById('c2-build-loopback-hint');
if (!hint) return;
const override = document.getElementById('c2-build-host') && String(document.getElementById('c2-build-host').value || '').trim();
if (override) {
hint.style.display = 'none';
return;
}
const id = sel && sel.value;
if (!id) {
hint.style.display = 'none';
return;
}
const l = (C2.listeners || []).find(function(x) { return x.id === id; });
const h = (l && l.bindHost ? String(l.bindHost) : '').toLowerCase().trim();
if (h === '127.0.0.1' || h === 'localhost' || h === '::1') {
hint.textContent = c2t('c2.payloads.loopbackBeaconWarning');
hint.style.display = 'block';
} else {
hint.style.display = 'none';
}
};
C2.renderPayloadPage = function() {
const optionsHtml = C2.listeners.length > 0
? C2.listeners.map(l =>
`<option value="${l.id}">${escapeHtml(l.name)} (${l.type} ${l.bindHost}:${l.bindPort})</option>`
).join('')
: '<option value="">' + escapeHtml(c2t('c2.payloads.noListenersOption')) + '</option>';
const listenerSelect = document.getElementById('c2-payload-listener');
if (listenerSelect) {
listenerSelect.innerHTML = optionsHtml;
listenerSelect.removeEventListener('change', C2.updateOnelinerKinds);
listenerSelect.addEventListener('change', C2.updateOnelinerKinds);
}
const buildSelect = document.getElementById('c2-build-listener');
if (buildSelect) {
const listeners = C2.listeners || [];
let buildOptionsHtml;
if (listeners.length > 0) {
buildOptionsHtml = listeners.map(l =>
`<option value="${l.id}">${escapeHtml(l.name)} (${l.type} ${l.bindHost}:${l.bindPort})</option>`
).join('');
} else {
buildOptionsHtml = '<option value="">' + escapeHtml(c2t('c2.payloads.noListenersOption')) + '</option>';
}
buildSelect.innerHTML = buildOptionsHtml;
buildSelect.removeEventListener('change', C2.updateLoopbackBuildHint);
buildSelect.addEventListener('change', C2.updateLoopbackBuildHint);
C2.updateLoopbackBuildHint();
}
const buildHostInput = document.getElementById('c2-build-host');
if (buildHostInput) {
buildHostInput.removeEventListener('input', C2.updateLoopbackBuildHint);
buildHostInput.addEventListener('input', C2.updateLoopbackBuildHint);
}
C2.updateOnelinerKinds();
const buildBtn = document.getElementById('c2-build-btn');
if (buildBtn && !buildBtn.disabled) buildBtn.textContent = c2t('c2.payloads.buildBeaconBtn');
const genBtn = document.getElementById('c2-generate-oneliner-btn');
if (genBtn) genBtn.textContent = c2t('c2.payloads.generateOnelinerBtn');
};
C2.generateOneliner = function() {
const listenerId = document.getElementById('c2-payload-listener')?.value;
const kind = document.getElementById('c2-payload-kind')?.value || 'bash';
const host = document.getElementById('c2-payload-host')?.value;
if (!listenerId) {
showToast(c2t('c2.payloads.toastPickListener'), 'error');
return;
}
apiRequest('POST', `${API_BASE}/payloads/oneliner`, {
listener_id: listenerId,
kind: kind,
host: host
}).then(data => {
if (data.error) {
showToast(data.error, 'error');
} else {
const output = document.getElementById('c2-oneliner-output');
if (output) {
output.textContent = data.oneliner;
output.style.display = 'block';
}
}
}).catch(err => {
showToast(c2t('c2.payloads.toastOnelinerFail', { msg: err.message || '' }), 'error');
});
};
C2.copyOneliner = function() {
const el = document.getElementById('c2-oneliner-output');
if (el && el.textContent) copyToClipboard(el.textContent);
};
C2.buildBeacon = function() {
const listenerId = document.getElementById('c2-build-listener')?.value;
const os = document.getElementById('c2-build-os')?.value || 'linux';
const arch = document.getElementById('c2-build-arch')?.value || 'amd64';
const host = document.getElementById('c2-build-host')?.value;
if (!listenerId) {
showToast(c2t('c2.payloads.toastPickListener'), 'error');
return;
}
const btn = document.getElementById('c2-build-btn');
if (btn) {
btn.disabled = true;
btn.textContent = c2t('c2.payloads.building');
}
apiRequest('POST', `${API_BASE}/payloads/build`, {
listener_id: listenerId,
os: os,
arch: arch,
host: host
}).then(data => {
if (btn) {
btn.disabled = false;
btn.textContent = c2t('c2.payloads.buildBeaconBtn');
}
if (data.error) {
showToast(data.error, 'error');
} else {
showToast(c2t('c2.payloads.toastBuildSuccess', { bytes: data.payload?.size_bytes }), 'success');
const result = document.getElementById('c2-build-result');
if (result) {
result.innerHTML = `
<div class="c2-build-success">
<div>✓ ${escapeHtml(c2t('c2.payloads.buildSuccessTitle'))}</div>
<div>${escapeHtml(c2t('c2.payloads.buildMetaOsArch', { os: data.payload?.os, arch: data.payload?.arch }))}</div>
<div>${escapeHtml(c2t('c2.payloads.buildSize', { bytes: data.payload?.size_bytes }))}</div>
<button onclick="window.__c2DownloadPayload('${data.payload?.download_path?.split('/').pop()}')"
class="btn-primary" style="margin-top:8px;display:inline-block;cursor:pointer;">${escapeHtml(c2t('c2.payloads.download'))}</button>
</div>
`;
}
}
}).catch(err => {
if (btn) {
btn.disabled = false;
btn.textContent = c2t('c2.payloads.buildBeaconBtn');
}
showToast(c2t('c2.payloads.toastBuildFail', { msg: err.message || '' }), 'error');
});
};
// ============================================================================
// 事件审计
// ============================================================================
C2.loadEvents = function(page) {
const p = page != null ? page : (C2.eventsPage || 1);
C2.eventsPage = p;
const ps = C2.eventsPageSize || 10;
apiRequest('GET', `${API_BASE}/events?page=${encodeURIComponent(String(p))}&page_size=${encodeURIComponent(String(ps))}`).then(data => {
if (data.error) {
showToast(String(data.error), 'error');
return;
}
C2.events = data.events || [];
C2.eventsTotal = typeof data.total === 'number' ? data.total : (C2.events.length || 0);
const maxPage = Math.max(1, Math.ceil(C2.eventsTotal / ps));
if (p > maxPage) {
C2.loadEvents(maxPage);
return;
}
C2.renderEvents();
C2.renderEventsPagination();
C2.syncEventsToolbar();
}).catch(err => {
showToast(err.message || String(err), 'error');
});
};
C2.goEventsPage = function(targetPage) {
const totalPages = Math.max(1, Math.ceil((C2.eventsTotal || 0) / (C2.eventsPageSize || 10)));
if (targetPage < 1 || targetPage > totalPages) return;
C2.loadEvents(targetPage);
const list = document.getElementById('c2-event-list');
if (list) list.scrollIntoView({ behavior: 'smooth', block: 'start' });
};
C2.changeEventsPageSize = function() {
const sel = document.getElementById('c2-events-page-size-pagination');
if (!sel) return;
const n = parseInt(sel.value, 10);
if (n > 0) {
C2.eventsPageSize = n;
C2.loadEvents(1);
}
};
C2.renderEventsPagination = function() {
const paginationContainer = document.getElementById('c2-events-pagination');
if (!paginationContainer) return;
const total = C2.eventsTotal || 0;
const currentPage = C2.eventsPage || 1;
const pageSize = C2.eventsPageSize || 10;
const totalPages = Math.max(1, Math.ceil(total / pageSize));
if (total === 0) {
paginationContainer.innerHTML = '';
return;
}
const start = total === 0 ? 0 : (currentPage - 1) * pageSize + 1;
const end = Math.min(currentPage * pageSize, total);
let html = '<div class="monitor-pagination">';
html += `
<div class="pagination-info">
<span>${escapeHtml(c2t('c2.events.paginationShow', { start, end, total }))}</span>
<label class="pagination-page-size">
${escapeHtml(c2t('c2.events.paginationPerPage'))}
<select id="c2-events-page-size-pagination" onchange="C2.changeEventsPageSize()">
<option value="10" ${pageSize === 10 ? 'selected' : ''}>10</option>
<option value="20" ${pageSize === 20 ? 'selected' : ''}>20</option>
<option value="50" ${pageSize === 50 ? 'selected' : ''}>50</option>
<option value="100" ${pageSize === 100 ? 'selected' : ''}>100</option>
</select>
</label>
</div>
<div class="pagination-controls">
<button type="button" class="btn-secondary" onclick="C2.goEventsPage(1)" ${currentPage === 1 ? 'disabled' : ''}>${escapeHtml(c2t('c2.events.paginationFirst'))}</button>
<button type="button" class="btn-secondary" onclick="C2.goEventsPage(${currentPage - 1})" ${currentPage === 1 ? 'disabled' : ''}>${escapeHtml(c2t('c2.events.paginationPrev'))}</button>
<span class="pagination-page">${escapeHtml(c2t('c2.events.paginationPage', { current: currentPage, total: totalPages }))}</span>
<button type="button" class="btn-secondary" onclick="C2.goEventsPage(${currentPage + 1})" ${currentPage >= totalPages ? 'disabled' : ''}>${escapeHtml(c2t('c2.events.paginationNext'))}</button>
<button type="button" class="btn-secondary" onclick="C2.goEventsPage(${totalPages})" ${currentPage >= totalPages ? 'disabled' : ''}>${escapeHtml(c2t('c2.events.paginationLast'))}</button>
</div>
`;
html += '</div>';
paginationContainer.innerHTML = html;
if (typeof applyTranslations === 'function') applyTranslations(paginationContainer);
};
C2.collectCheckedEventIds = function() {
return Array.from(document.querySelectorAll('.c2-event-check:checked')).map(cb => cb.getAttribute('data-id')).filter(Boolean);
};
C2.syncEventsToolbar = function() {
const batchBtn = document.getElementById('c2-events-batch-delete');
const ids = C2.collectCheckedEventIds();
if (batchBtn) batchBtn.disabled = ids.length === 0;
const all = document.querySelectorAll('.c2-event-check');
const selAll = document.getElementById('c2-events-select-all');
if (selAll && all.length) {
const nChecked = document.querySelectorAll('.c2-event-check:checked').length;
selAll.checked = nChecked === all.length;
selAll.indeterminate = nChecked > 0 && nChecked < all.length;
} else if (selAll) {
selAll.checked = false;
selAll.indeterminate = false;
}
};
C2.onEventsSelectAll = function(checked) {
document.querySelectorAll('.c2-event-check').forEach(cb => { cb.checked = checked; });
C2.syncEventsToolbar();
};
C2.deleteEventById = function(id) {
if (!id) return;
if (!confirm(c2t('c2.events.confirmDeleteOne'))) return;
apiRequest('DELETE', `${API_BASE}/events`, { ids: [id] }).then(data => {
if (data.error) {
showToast(String(data.error), 'error');
return;
}
showToast(c2t('c2.events.toastDeleted', { n: data.deleted != null ? data.deleted : 1 }), 'success');
C2.loadEvents(C2.eventsPage || 1);
}).catch(err => showToast(err.message || String(err), 'error'));
};
C2.deleteSelectedEvents = function() {
const ids = C2.collectCheckedEventIds();
if (!ids.length) {
showToast(c2t('c2.events.toastSelectFirst'), 'warn');
return;
}
if (!confirm(c2t('c2.events.confirmBatchDelete', { n: ids.length }))) return;
apiRequest('DELETE', `${API_BASE}/events`, { ids }).then(data => {
if (data.error) {
showToast(String(data.error), 'error');
return;
}
const deleted = data.deleted != null ? data.deleted : ids.length;
showToast(c2t('c2.events.toastDeleted', { n: deleted }), 'success');
C2.loadEvents(C2.eventsPage || 1);
}).catch(err => showToast(err.message || String(err), 'error'));
};
C2.renderEvents = function() {
const container = document.getElementById('c2-event-list');
if (!container) return;
const selAll = document.getElementById('c2-events-select-all');
if (selAll) {
selAll.checked = false;
selAll.indeterminate = false;
}
if (C2.events.length === 0) {
container.innerHTML = '<div class="c2-empty">' + escapeHtml(c2t('c2.events.empty')) + '</div>';
if (selAll) selAll.disabled = true;
C2.syncEventsToolbar();
return;
}
if (selAll) selAll.disabled = false;
const delTitle = escapeHtml(c2t('c2.events.deleteOne'));
container.innerHTML = C2.events.map(e => {
const eid = escapeHtml(e.id || '');
return `
<div class="c2-event-item">
<label class="c2-event-check-label" onclick="event.stopPropagation();">
<input type="checkbox" class="c2-event-check" data-id="${eid}" onchange="C2.syncEventsToolbar()">
</label>
<div class="c2-event-level ${escapeHtml(e.level || '')}"></div>
<div class="c2-event-content">
<div class="c2-event-message">${escapeHtml(e.message)}</div>
<div class="c2-event-meta">
${formatTime(e.createdAt)} · ${escapeHtml(e.category || '')}${e.sessionId ? ' · ' + escapeHtml(String(e.sessionId).substring(0, 8)) : ''}
</div>
</div>
<button type="button" class="btn-secondary c2-event-row-delete" onclick="event.stopPropagation();C2.deleteEventById('${eid}')" title="${delTitle}" aria-label="${delTitle}">🗑</button>
</div>
`;
}).join('');
C2.syncEventsToolbar();
if (typeof applyTranslations === 'function') applyTranslations(container);
};
C2.connectEventStream = function() {
if (C2.eventSource) C2.eventSource.close();
let streamUrl = `${API_BASE}/events/stream`;
if (typeof authToken !== 'undefined' && authToken) {
streamUrl += `?token=${encodeURIComponent(authToken)}`;
}
C2.eventSource = new EventSource(streamUrl);
C2.eventSource.onmessage = (e) => {
try {
const event = JSON.parse(e.data);
C2.onEvent(event);
} catch (err) {}
};
C2.eventSource.onerror = () => {
setTimeout(() => C2.connectEventStream(), 5000);
};
};
C2.onEvent = function(event) {
if (window.currentPageId === 'c2-events' && (C2.eventsPage || 1) === 1) {
C2.loadEvents(1);
}
const msg = event.message || '';
const sessionOnline = event.category === 'session' && (
msg.includes('上线') || msg.includes('新会话') || /new session/i.test(msg)
);
if (event.level === 'critical' || sessionOnline) {
showToast(`[${event.category}] ${event.message}`, event.level === 'critical' ? 'error' : 'info');
}
C2.updateDashboardStats();
};
// ============================================================================
// Profile 管理
// ============================================================================
C2.loadProfiles = function() {
apiRequest('GET', `${API_BASE}/profiles`).then(data => {
C2.profiles = data.profiles || [];
C2.renderProfiles();
});
};
C2.renderProfiles = function() {
const container = document.getElementById('c2-profile-list');
if (!container) return;
if (C2.profiles.length === 0) {
container.innerHTML = '<div class="c2-empty">' + escapeHtml(c2t('c2.profiles.empty')) + '</div>';
return;
}
const defVal = c2t('c2.profiles.defaultValue');
container.innerHTML = C2.profiles.map(p => `
<div class="c2-profile-card">
<div class="c2-profile-header">
<h4>${escapeHtml(p.name)}</h4>
<button class="btn-danger btn-sm" onclick="C2.deleteProfile('${p.id}')">${escapeHtml(c2t('common.delete'))}</button>
</div>
<div class="c2-profile-info">
<div><strong>UA:</strong> ${escapeHtml(p.userAgent || defVal)}</div>
<div><strong>URIs:</strong> ${escapeHtml((p.uris || []).join(', ') || defVal)}</div>
<div><strong>Jitter:</strong> ${p.jitterMinMs || 0}ms ${p.jitterMaxMs || 0}ms</div>
</div>
</div>
`).join('');
};
C2.showCreateProfileModal = function() {
const modal = document.getElementById('c2-modal');
const content = document.getElementById('c2-modal-content');
if (!content) return;
content.innerHTML = `
<div class="c2-modal-header">
<h3>${escapeHtml(c2t('c2.profiles.modalCreateTitle'))}</h3>
<button class="c2-modal-close" onclick="C2.closeModal()">&times;</button>
</div>
<div class="c2-modal-body">
<div class="c2-form-group">
<label>${escapeHtml(c2t('c2.profiles.profileNameLabel'))}</label>
<input type="text" id="c2-profile-name" class="form-control" placeholder="${escapeHtml(c2t('c2.profiles.placeholderProfileName'))}">
</div>
<div class="c2-form-group">
<label>${escapeHtml(c2t('c2.profiles.userAgent'))}</label>
<input type="text" id="c2-profile-ua" class="form-control" placeholder="Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...">
<div class="form-hint">${escapeHtml(c2t('c2.profiles.hintUa'))}</div>
</div>
<div class="c2-form-group">
<label>${escapeHtml(c2t('c2.profiles.labelBeaconUris'))}</label>
<textarea id="c2-profile-uris" class="form-control" rows="3" placeholder="/api/v1/status&#10;/cdn/health&#10;/assets/check">/api/v1/status</textarea>
<div class="form-hint">${escapeHtml(c2t('c2.profiles.hintUris'))}</div>
</div>
<div class="c2-form-row">
<div class="c2-form-group">
<label>${escapeHtml(c2t('c2.profiles.labelJitterMin'))}</label>
<input type="number" id="c2-profile-jmin" class="form-control" value="100" min="0">
</div>
<div class="c2-form-group">
<label>${escapeHtml(c2t('c2.profiles.labelJitterMax'))}</label>
<input type="number" id="c2-profile-jmax" class="form-control" value="500" min="0">
</div>
</div>
<div class="c2-form-group">
<label>${escapeHtml(c2t('c2.profiles.labelRespHeaders'))}</label>
<textarea id="c2-profile-headers" class="form-control" rows="3" placeholder='{"Server":"nginx","X-Powered-By":"ASP.NET"}'>{"Server":"nginx"}</textarea>
<div class="form-hint">${escapeHtml(c2t('c2.profiles.hintHeaders'))}</div>
</div>
</div>
<div class="c2-modal-footer">
<button class="btn-secondary" onclick="C2.closeModal()">${escapeHtml(c2t('common.cancel'))}</button>
<button class="btn-primary" onclick="C2.createProfile()">${escapeHtml(c2t('c2.profiles.submitCreate'))}</button>
</div>
`;
modal.style.display = 'flex';
};
C2.createProfile = function() {
const name = document.getElementById('c2-profile-name')?.value.trim();
if (!name) {
showToast(c2t('c2.profiles.toastNameRequired'), 'error');
return;
}
const userAgent = document.getElementById('c2-profile-ua')?.value.trim() || '';
const urisRaw = document.getElementById('c2-profile-uris')?.value.trim() || '';
const uris = urisRaw.split('\n').map(u => u.trim()).filter(u => u);
const jitterMinMs = parseInt(document.getElementById('c2-profile-jmin')?.value) || 100;
const jitterMaxMs = parseInt(document.getElementById('c2-profile-jmax')?.value) || 500;
let responseHeaders = {};
const headersRaw = document.getElementById('c2-profile-headers')?.value.trim();
if (headersRaw) {
try { responseHeaders = JSON.parse(headersRaw); }
catch (e) { showToast(c2t('c2.profiles.toastInvalidHeadersJson'), 'error'); return; }
}
apiRequest('POST', `${API_BASE}/profiles`, {
name,
user_agent: userAgent,
uris,
jitter_min_ms: jitterMinMs,
jitter_max_ms: jitterMaxMs,
response_headers: responseHeaders
}).then(data => {
if (data.error) {
showToast(data.error, 'error');
} else {
showToast(c2t('c2.profiles.toastCreated'), 'success');
C2.closeModal();
C2.loadProfiles();
}
});
};
C2.deleteProfile = function(id) {
if (!confirm(c2t('c2.profiles.confirmDelete'))) return;
apiRequest('DELETE', `${API_BASE}/profiles/${id}`, {}).then(data => {
showToast(c2t('c2.profiles.toastDeleted'), 'success');
C2.loadProfiles();
});
};
// ============================================================================
// 仪表盘
// ============================================================================
C2.updateDashboardStats = function() {
const runningListeners = C2.listeners.filter(l => l.status === 'running').length;
const activeSessions = C2.sessions.filter(s => s.status === 'active').length;
const pendingTasks = typeof C2.tasksPendingQueuedCount === 'number'
? C2.tasksPendingQueuedCount
: C2.tasks.filter(t => t.status === 'queued' || t.status === 'pending').length;
const elListeners = document.getElementById('c2-stat-listeners');
const elSessions = document.getElementById('c2-stat-sessions');
const elPending = document.getElementById('c2-stat-pending');
if (elListeners) elListeners.textContent = runningListeners;
if (elSessions) elSessions.textContent = activeSessions;
if (elPending) elPending.textContent = pendingTasks;
};
// ============================================================================
// 模态框
// ============================================================================
C2.closeModal = function() {
const modal = document.getElementById('c2-modal');
if (modal) modal.style.display = 'none';
};
// ============================================================================
// 暴露到全局
// ============================================================================
window.C2 = C2;
// 页面切换监听
window.addEventListener('pageChanged', function(e) {
if (e.detail?.pageId?.startsWith('c2')) {
C2.init();
}
});
// DOM 加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
if (window.currentPageId?.startsWith('c2')) C2.init();
});
} else {
if (window.currentPageId?.startsWith('c2')) C2.init();
}
document.addEventListener('languagechange', function () {
try {
if (!window.currentPageId || !String(window.currentPageId).startsWith('c2')) return;
if (typeof applyTranslations === 'function') applyTranslations(document);
C2.init();
if (C2.selectedSessionId && (window.currentPageId === 'c2-sessions')) {
C2.renderSessions();
C2.renderSessionDetail(C2.selectedSessionId);
}
} catch (e) {
console.warn('languagechange C2 refresh failed', e);
}
});
})();