mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-15 21:08:01 +02:00
2066 lines
94 KiB
JavaScript
2066 lines
94 KiB
JavaScript
// 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 下拉选项 HTML(value / 文本已转义) */
|
||
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 插值把日期里的「/」转成 /,与 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()">×</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()">×</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()">×</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()">×</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()">×</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()">×</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 /cdn/health /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);
|
||
}
|
||
});
|
||
|
||
})();
|