Files
CyberStrikeAI/web/static/js/c2.js
T
2026-05-04 03:42:24 +08:00

1876 lines
84 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;
}
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() {
apiRequest('GET', `${API_BASE}/listeners`).then(data => {
C2.listeners = data.listeners || [];
C2.renderListeners();
C2.updateDashboardStats();
});
};
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(l => `
<div class="c2-listener-card ${l.status}">
<div class="c2-listener-header">
<div>
<div class="c2-listener-name">${escapeHtml(l.name)}</div>
<div class="c2-listener-id">${l.id.substring(0, 12)}...</div>
</div>
<span class="c2-listener-type">${escapeHtml(listenerTypeLabel(l.type))}</span>
</div>
<div class="c2-listener-info">
<div class="c2-listener-address">
<span class="c2-status-dot ${l.status}"></span>
<strong>${l.bindHost}:${l.bindPort}</strong>
</div>
${l.startedAt ? `<div style="font-size:12px;margin-top:4px;">${escapeHtml(c2t('c2.listeners.startedAt', { time: formatTime(l.startedAt) }))}</div>` : ''}
${l.remark ? `<div style="font-size:12px;margin-top:2px;opacity:0.7;">${escapeHtml(l.remark)}</div>` : ''}
</div>
<div class="c2-listener-actions">
${l.status === 'stopped'
? `<button class="btn-primary btn-sm" onclick="C2.startListener('${l.id}')">▶ ${escapeHtml(c2t('c2.listeners.start'))}</button>`
: `<button class="btn-secondary btn-sm" onclick="C2.stopListener('${l.id}')">⏹ ${escapeHtml(c2t('c2.listeners.stop'))}</button>`
}
<button class="btn-ghost btn-sm" onclick="C2.editListener('${l.id}')">${escapeHtml(c2t('c2.listeners.edit'))}</button>
<button class="btn-danger btn-sm" onclick="C2.deleteListener('${l.id}')">${escapeHtml(c2t('c2.listeners.delete'))}</button>
</div>
</div>
`).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) return;
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">
<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">
<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>
`;
modal.style.display = 'flex';
};
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;
}
apiRequest('POST', `${API_BASE}/listeners`, {
name, type, bind_host: bindHost, bind_port: bindPort, remark,
callback_host: callbackHost
}).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) return;
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="${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">
<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>
`;
modal.style.display = 'flex';
};
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;
apiRequest('PUT', `${API_BASE}/listeners/${id}`, {
name, bind_host: bindHost, bind_port: bindPort, remark,
callback_host: callbackHost
}).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);
}
});
})();