// 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: null, terminalHistory: {}, terminalLogs: {}, terminalBusy: false, terminalQueue: [], // 文件管理 currentPath: '.', implantPwd: null, fileList: [], fileUploadBusy: false, // 任务轮询 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 formatTaskCommand(task) { if (!task) return ''; const type = String(task.taskType || '').toLowerCase(); const p = task.payload; if (!p || typeof p !== 'object' || Object.keys(p).length === 0) { if (type === 'pwd' || type === 'ps' || type === 'screenshot') return type; return ''; } switch (type) { case 'shell': case 'exec': return p.command != null ? String(p.command) : ''; case 'ls': case 'cd': return p.path != null ? String(p.path) : ''; case 'download': return p.remote_path != null ? String(p.remote_path) : ''; case 'upload': if (p.remote_path) return String(p.remote_path); if (p.file_id) return 'file:' + String(p.file_id); return ''; case 'kill_proc': return p.pid != null ? 'pid:' + String(p.pid) : ''; case 'sleep': let sleepStr = p.seconds != null ? 'sleep ' + p.seconds + 's' : ''; if (p.jitter != null) sleepStr += (sleepStr ? ', ' : '') + 'jitter ' + p.jitter + '%'; return sleepStr; case 'port_fwd': return [p.action, p.remote_host, p.remote_port, p.local_port].filter(v => v != null && v !== '').join(':'); case 'socks_start': case 'socks_stop': return p.port != null ? 'port:' + String(p.port) : type; case 'load_assembly': if (p.args) return String(p.args); if (p.file_id) return 'file:' + String(p.file_id); return ''; case 'persist': return p.method != null ? String(p.method) : ''; default: try { return JSON.stringify(p); } catch (e) { return ''; } } } function truncateCommand(cmd, maxLen) { if (!cmd) return ''; const s = String(cmd); if (!maxLen || s.length <= maxLen) return s; return s.substring(0, maxLen - 1) + '\u2026'; } // ============================================================================ // 工具函数 // ============================================================================ 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:10100;display:flex;flex-direction:column;gap:8px;'; document.body.appendChild(div); return div; })(); container.style.zIndex = '10100'; 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 bindC2TaskActionDelegation() { if (document.documentElement.dataset.c2TaskActionsBound === '1') return; document.documentElement.dataset.c2TaskActionsBound = '1'; document.addEventListener('click', function(e) { const btn = e.target.closest('[data-c2-task-action]'); if (!btn) return; e.preventDefault(); e.stopPropagation(); const action = btn.getAttribute('data-c2-task-action'); const id = btn.getAttribute('data-task-id'); if (!id) return; if (action === 'view') C2.viewTask(id); else if (action === 'cancel') C2.cancelTask(id); else if (action === 'delete') C2.deleteTaskById(id); }); } bindC2TaskActionDelegation(); /** 监听器表单:Malleable Profile 下拉选项 HTML(value / 文本已转义) */ function listenerProfileSelectHtml(selectedProfileId) { const sel = selectedProfileId ? String(selectedProfileId) : ''; let opts = ``; 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 += ``; } 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 '
' + escapeHtml(prefix) + ' ' + escapeHtml(time) + '
'; } 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(); C2.ensureListenersLoaded(); 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.ensureListenersLoaded = function() { if (C2.listeners && C2.listeners.length > 0) { return Promise.resolve(C2.listeners); } return apiRequest('GET', `${API_BASE}/listeners`).then(function(data) { C2.listeners = (data && data.listeners) || []; return C2.listeners; }); }; 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 = `

${escapeHtml(c2t('c2.listeners.emptyTitle'))}

${escapeHtml(c2t('c2.listeners.emptyHint'))}

`; 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 ? '
' + escapeHtml(profileName) + '
' : ''; const cb = C2.getListenerCallbackHost(l); const cbRow = cb ? '
' + escapeHtml(c2t('c2.listeners.callbackShort')) + '' + escapeHtml(cb) + '
' : ''; const remarkRow = l.remark ? '
' + escapeHtml(l.remark) + '
' : ''; 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 `
${typeMark}

${escapeHtml(l.name)}

${pillLabel}
${escapeHtml(l.id)}
${escapeHtml(c2t('c2.listeners.bindEndpoint'))} ${bindVal}
${cbRow} ${profileBadge} ${remarkRow} ${startedHtml}
${l.status === 'stopped' ? `` : `` }
`; }).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 = `

${escapeHtml(c2t('c2.listeners.modalCreateTitle'))}

${escapeHtml(c2t('c2.listeners.loadingProfiles'))}

`; C2.ensureProfilesLoaded().then(() => { const profileOpts = listenerProfileSelectHtml(''); const emptyProfHintCreate = (C2.profiles && C2.profiles.length > 0) ? '' : `
${escapeHtml(c2t('c2.listeners.malleableProfileEmptyListHint'))}
`; content.innerHTML = `

${escapeHtml(c2t('c2.listeners.modalCreateTitle'))}

${escapeHtml(c2t('c2.listeners.bindHintExternal'))}
${emptyProfHintCreate}
${escapeHtml(c2t('c2.listeners.malleableProfileHint'))}
${escapeHtml(c2t('c2.listeners.callbackHostHint'))}
`; 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 = `

${escapeHtml(c2t('c2.listeners.editTitle'))}

${escapeHtml(c2t('c2.listeners.loadingProfiles'))}

`; 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') ? '' : `
${escapeHtml(c2t('c2.listeners.malleableProfileNonHttpHint'))}
`; const emptyProfHint = (C2.profiles && C2.profiles.length > 0) ? '' : `
${escapeHtml(c2t('c2.listeners.malleableProfileEmptyListHint'))}
`; content.innerHTML = `

${escapeHtml(c2t('c2.listeners.editTitle'))}

${httpHint}${emptyProfHint}
${escapeHtml(c2t('c2.listeners.malleableProfileHint'))}
${escapeHtml(c2t('c2.listeners.callbackHostHint'))}
`; }).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 = `

${escapeHtml(c2t('c2.sessions.emptyTitle'))}

${escapeHtml(c2t('c2.sessions.emptyHint'))}

`; if (main) main.innerHTML = ''; return; } list.innerHTML = C2.sessions.map(s => `
${escapeHtml(s.hostname || c2t('c2.sessions.unknownHost'))} ${escapeHtml(sessionStatusLabel(s.status))}
${escapeHtml(s.username)} · ${s.os}/${s.arch} ${s.isAdmin ? '' + escapeHtml(c2t('c2.sessions.rootBadge')) + '' : ''}
${s.internalIp || '-'} · PID ${s.pid}
`).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.implantPwd = null; C2.currentPath = '.'; C2.renderSessions(); C2.renderSessionDetail(id); }; 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 = `

${escapeHtml(s.hostname)} ${escapeHtml(sessionStatusLabel(s.status))}

${s.id} | ${escapeHtml(s.username)}@${s.os}/${s.arch}
${escapeHtml(c2t('c2.sessions.terminal'))}
${escapeHtml(c2t('c2.sessions.files'))}
${escapeHtml(c2t('c2.sessions.tasks'))}
${escapeHtml(c2t('c2.sessions.info'))}
${escapeHtml(c2t('c2.sessions.termStatusReady'))}
`; var isCurlBeacon = s.implantUuid && s.implantUuid.startsWith('curl_'); if (isCurlBeacon) { var termContainer = container.querySelector('#c2-terminal-container'); if (termContainer) { termContainer.innerHTML = '
' + '
📡
' + '
' + escapeHtml(c2t('c2.sessions.curlBeaconTitle')) + '
' + '
' + c2t('c2.sessions.curlBeaconBody').split('\n').map(function (ln) { return escapeHtml(ln); }).join('
') + '
' + '
'; } } setTimeout(() => { if (!isCurlBeacon) C2.initTerminal(); C2.loadFileList(s.id, '.'); C2.loadSessionTasks(s.id); C2.updateFileUploadButton(s); C2.ensureListenersLoaded().then(function() { C2.updateFileUploadButton(s); }); }, 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(function () { C2.fitTerminal(); if (C2.terminalInstance) C2.terminalInstance.focus(); }, 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.serializeTerminalBuffer = function(term) { if (!term || !term.buffer || !term.buffer.active) return ''; const buf = term.buffer.active; const lines = []; for (let i = 0; i < buf.length; i++) { const line = buf.getLine(i); if (line) lines.push(line.translateToString(true)); } return lines.join('\n'); }; C2.pushTerminalHistory = function(cmd) { const sid = C2.selectedSessionId; if (!sid || !cmd) return; if (!C2.terminalHistory[sid]) C2.terminalHistory[sid] = []; const hist = C2.terminalHistory[sid]; if (hist.length === 0 || hist[hist.length - 1] !== cmd) { hist.push(cmd); if (hist.length > 200) hist.shift(); } }; C2.finishTerminalCommand = function(term, status) { C2.terminalBusy = false; const statusEl = document.getElementById('c2-terminal-status'); if (status === 'err' && statusEl) { statusEl.textContent = c2t('c2.sessions.termStatusErr'); } else if (status === 'timeout' && statusEl) { statusEl.textContent = c2t('c2.sessions.termStatusTimeout'); } else if (statusEl && C2.terminalQueue.length === 0) { statusEl.textContent = c2t('c2.sessions.termStatusReady'); } if (C2.terminalQueue.length > 0) { const next = C2.terminalQueue.shift(); C2.runTerminalCommand(next, term); return; } term.write('$ '); if (statusEl && status !== 'err' && status !== 'timeout') { statusEl.textContent = c2t('c2.sessions.termStatusReady'); } }; C2.runTerminalCommand = function(cmd, term) { if (!C2.selectedSessionId) { term.writeln('\x1b[31m' + c2t('c2.sessions.termNoSession') + '\x1b[0m'); term.write('$ '); return; } C2.terminalBusy = true; C2.pushTerminalHistory(cmd); 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`); C2.finishTerminalCommand(term, 'err'); } else { C2.waitForTaskResult(data.task?.id || data.task_id, term); } }).catch(function () { term.writeln('\x1b[31mError: request failed\x1b[0m'); C2.finishTerminalCommand(term, 'err'); }); }; C2.executeInTerminal = function(cmd, term) { if (!cmd) { term.write('$ '); return; } if (C2.terminalBusy) { C2.terminalQueue.push(cmd); term.writeln('\x1b[33m' + c2t('c2.sessions.termQueued') + '\x1b[0m'); return; } C2.runTerminalCommand(cmd, 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'); C2.finishTerminalCommand(term, 'timeout'); 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`); } C2.finishTerminalCommand(term, task.status === 'failed' ? 'err' : 'ready'); } else { delay = Math.min(delay * 1.5, maxDelay); setTimeout(check, delay); } }).catch(function () { C2.finishTerminalCommand(term, 'err'); }); }; check(); }; C2.initTerminal = function() { const container = document.getElementById('c2-terminal-container'); if (!container || typeof Terminal === 'undefined') return; if (C2.terminalInstance && C2.terminalSessionId) { C2.terminalLogs[C2.terminalSessionId] = C2.serializeTerminalBuffer(C2.terminalInstance); } if (C2.terminalInstance) { C2.terminalInstance.dispose(); } const sessionId = C2.selectedSessionId || '_none'; C2.terminalSessionId = sessionId; C2.terminalQueue = []; C2.terminalBusy = false; 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', cursorAccent: '#0d1117', 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 = ''; let cursorIndex = 0; let historyIndex = -1; let lastPasteAt = 0; let lastPasteText = ''; const prompt = '$ '; function redrawInputLine() { term.write('\x1b[2K\r' + prompt + lineBuffer); const tail = lineBuffer.length - cursorIndex; if (tail > 0) term.write('\x1b[' + tail + 'D'); } function resetInputLine() { lineBuffer = ''; cursorIndex = 0; historyIndex = -1; term.write('\x1b[2K\r' + prompt); } function insertPlainText(text) { const safe = String(text).replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, ''); if (!safe) return; lineBuffer = lineBuffer.slice(0, cursorIndex) + safe + lineBuffer.slice(cursorIndex); cursorIndex += safe.length; redrawInputLine(); } function deleteWordBeforeCursor() { if (cursorIndex === 0) return; let start = cursorIndex; while (start > 0 && /\s/.test(lineBuffer[start - 1])) start--; while (start > 0 && !/\s/.test(lineBuffer[start - 1])) start--; lineBuffer = lineBuffer.slice(0, start) + lineBuffer.slice(cursorIndex); cursorIndex = start; redrawInputLine(); } function moveWordLeft() { if (cursorIndex === 0) return; let pos = cursorIndex; while (pos > 0 && /\s/.test(lineBuffer[pos - 1])) pos--; while (pos > 0 && !/\s/.test(lineBuffer[pos - 1])) pos--; const delta = cursorIndex - pos; if (delta > 0) { term.write('\x1b[' + delta + 'D'); cursorIndex = pos; } } function moveWordRight() { if (cursorIndex >= lineBuffer.length) return; let pos = cursorIndex; while (pos < lineBuffer.length && /\s/.test(lineBuffer[pos])) pos++; while (pos < lineBuffer.length && !/\s/.test(lineBuffer[pos])) pos++; const delta = pos - cursorIndex; if (delta > 0) { term.write('\x1b[' + delta + 'C'); cursorIndex = pos; } } function showHistoryEntry(entry) { lineBuffer = entry || ''; cursorIndex = lineBuffer.length; term.write('\x1b[2K\r' + prompt + lineBuffer); } function submitCurrentLine() { if (C2.terminalBusy) { term.writeln(''); term.writeln('\x1b[33m' + c2t('c2.sessions.termWaitFinish') + '\x1b[0m'); term.write(prompt + lineBuffer); const tail = lineBuffer.length - cursorIndex; if (tail > 0) term.write('\x1b[' + tail + 'D'); return; } term.writeln(''); const cmd = lineBuffer.trim(); lineBuffer = ''; cursorIndex = 0; historyIndex = -1; if (cmd) { C2.executeInTerminal(cmd, term); } else { term.write(prompt); } } function handlePasteText(text) { const now = Date.now(); if (text === lastPasteText && now - lastPasteAt < 80) return; lastPasteAt = now; lastPasteText = text; const normalized = String(text).replace(/\r\n/g, '\n').replace(/\r/g, '\n'); if (normalized.indexOf('\n') === -1) { insertPlainText(normalized); return; } const endsWithNewline = normalized.endsWith('\n'); const parts = normalized.split('\n'); const tail = parts.pop() || ''; parts.forEach(function (part) { insertPlainText(part); submitCurrentLine(); }); if (tail) insertPlainText(tail); else if (endsWithNewline && parts.length === 0) submitCurrentLine(); } const savedLog = C2.terminalLogs[sessionId]; if (savedLog) { term.write(String(savedLog).replace(/\r\n/g, '\n').replace(/\r/g, '\n').replace(/\n/g, '\r\n')); if (!savedLog.endsWith('\n')) term.write('\r\n'); } else { term.writeln('\x1b[36m' + c2t('c2.sessions.terminalWelcome') + '\x1b[0m'); term.writeln(''); } term.write(prompt); term.onData(function (e) { if (e === '\x0c') { term.clear(); resetInputLine(); C2.terminalLogs[sessionId] = ''; return; } if (e === '\x03') { if (C2.terminalBusy) { term.writeln(''); term.writeln('\x1b[33m^C (' + c2t('c2.sessions.termCtrlC') + ')\x1b[0m'); } resetInputLine(); return; } if (e === '\x16') { if (navigator.clipboard && navigator.clipboard.readText) { navigator.clipboard.readText().then(handlePasteText).catch(function () {}); } return; } if (e.length > 1 && e.indexOf('\x1b') !== 0) { handlePasteText(e); return; } if (e === '\x1b[D' || e === '\x1bOD') { if (cursorIndex > 0) { cursorIndex--; term.write('\x1b[D'); } return; } if (e === '\x1b[C' || e === '\x1bOC') { if (cursorIndex < lineBuffer.length) { cursorIndex++; term.write('\x1b[C'); } return; } if (e === '\x1b[1;3D' || e === '\x1bb') { moveWordLeft(); return; } if (e === '\x1b[1;3C' || e === '\x1bf') { moveWordRight(); return; } if (e === '\x1b[A' || e === '\x1bOA') { const hist = C2.terminalHistory[sessionId] || []; if (hist.length === 0) return; historyIndex = historyIndex < 0 ? hist.length - 1 : Math.max(0, historyIndex - 1); showHistoryEntry(hist[historyIndex]); return; } if (e === '\x1b[B' || e === '\x1bOB') { const hist = C2.terminalHistory[sessionId] || []; if (hist.length === 0) return; historyIndex = historyIndex < 0 ? -1 : Math.min(hist.length - 1, historyIndex + 1); if (historyIndex < 0) showHistoryEntry(''); else showHistoryEntry(hist[historyIndex]); return; } if (e === '\x1b[H' || e === '\x1bOH' || e === '\x01') { if (cursorIndex > 0) { term.write('\x1b[' + cursorIndex + 'D'); cursorIndex = 0; } return; } if (e === '\x1b[F' || e === '\x1bOF' || e === '\x05') { const move = lineBuffer.length - cursorIndex; if (move > 0) { term.write('\x1b[' + move + 'C'); cursorIndex = lineBuffer.length; } return; } if (e === '\x1b[3~') { if (cursorIndex < lineBuffer.length) { lineBuffer = lineBuffer.slice(0, cursorIndex) + lineBuffer.slice(cursorIndex + 1); redrawInputLine(); } return; } if (e === '\x15') { resetInputLine(); return; } if (e === '\x0b') { lineBuffer = lineBuffer.slice(0, cursorIndex); redrawInputLine(); return; } if (e === '\x17') { deleteWordBeforeCursor(); return; } if (e === '\x1b\x7f') { deleteWordBeforeCursor(); return; } const code = e.charCodeAt(0); if (code === 13 || code === 10) { submitCurrentLine(); } else if (code === 127 || code === 8) { if (cursorIndex > 0) { lineBuffer = lineBuffer.slice(0, cursorIndex - 1) + lineBuffer.slice(cursorIndex); cursorIndex--; redrawInputLine(); } } else if (e.length === 1 && code >= 32) { historyIndex = -1; lineBuffer = lineBuffer.slice(0, cursorIndex) + e + lineBuffer.slice(cursorIndex); cursorIndex++; if (cursorIndex === lineBuffer.length) { term.write(e); } else { redrawInputLine(); } } }); const onTerminalPaste = function (ev) { const text = ev.clipboardData && ev.clipboardData.getData('text'); if (!text) return; ev.preventDefault(); handlePasteText(text); }; if (term.element) { term.element.addEventListener('paste', onTerminalPaste); } term.attachCustomKeyEventHandler(function (ev) { if (ev.type !== 'keydown') return true; if ((ev.ctrlKey || ev.metaKey) && !ev.shiftKey && (ev.key === 'c' || ev.key === 'C')) { if (term.getSelection()) return true; } const isPaste = (ev.ctrlKey || ev.metaKey) && !ev.shiftKey && !ev.altKey && (ev.key === 'v' || ev.key === 'V'); if (isPaste && navigator.clipboard && navigator.clipboard.readText) { ev.preventDefault(); navigator.clipboard.readText().then(handlePasteText).catch(function () {}); return false; } if (ev.shiftKey && ev.key === 'Insert' && navigator.clipboard && navigator.clipboard.readText) { ev.preventDefault(); navigator.clipboard.readText().then(handlePasteText).catch(function () {}); return false; } return true; }); container.addEventListener('click', function () { term.focus(); }); container.setAttribute('tabindex', '0'); C2.terminalInstance = term; if (C2.terminalResizeObserver) { C2.terminalResizeObserver.disconnect(); } C2.terminalResizeObserver = new ResizeObserver(function () { C2.fitTerminal(); }); C2.terminalResizeObserver.observe(container); setTimeout(function () { try { if (C2.terminalFitAddon) C2.terminalFitAddon.fit(); term.focus(); } catch (e) {} }, 100); }; C2.fitTerminal = function() { if (C2.terminalFitAddon && C2.terminalInstance) { try { C2.terminalFitAddon.fit(); } catch (e) {} } }; C2.clearTerminal = function() { if (C2.terminalInstance) { C2.terminalInstance.clear(); C2.terminalInstance.writeln('\x1b[36m' + c2t('c2.sessions.termCleared') + '\x1b[0m'); C2.terminalInstance.write('$ '); if (C2.terminalSessionId) { C2.terminalLogs[C2.terminalSessionId] = C2.serializeTerminalBuffer(C2.terminalInstance); } } C2.terminalQueue = []; }; C2.copyTerminal = function() { if (!C2.terminalInstance) return; const text = C2.terminalInstance.getSelection(); if (text) copyToClipboard(text); else showToast(c2t('c2.sessions.termNoSelection'), 'warning'); }; // ============================================================================ // 文件管理 // ============================================================================ C2.normalizeFilePath = function(path) { var p = path == null ? '.' : String(path).trim(); if (!p || p === '/') return '.'; p = p.replace(/\\/g, '/').replace(/\/+/g, '/').replace(/\/+$/, ''); return p || '.'; }; C2.joinFilePath = function(base, name) { var b = C2.normalizeFilePath(base); var n = String(name || '').trim().replace(/\\/g, '/').replace(/^\/+/, ''); if (!n) return b; if (b === '.' || b === '/') return n; return b + '/' + n; }; /** 将相对浏览路径解析为 implant 工作目录下的绝对路径 */ C2.resolvePathAgainstPwd = function(pwd, rel) { var base = String(pwd || '').trim().replace(/\\/g, '/').replace(/\/+$/, ''); if (!base) base = '/'; if (!base.startsWith('/')) base = '/' + base; var parts = String(rel || '.').replace(/\\/g, '/').split('/'); var stack = base === '/' ? [] : base.split('/').filter(Boolean); for (var i = 0; i < parts.length; i++) { var p = parts[i]; if (!p || p === '.') continue; if (p === '..') { if (stack.length) stack.pop(); } else { stack.push(p); } } return '/' + stack.join('/'); }; C2.resolveRemotePath = function(browsePath, filename) { var joined = C2.joinFilePath(browsePath || '.', filename); if (!C2.implantPwd) return joined; return C2.resolvePathAgainstPwd(C2.implantPwd, joined); }; C2.updateFileBreadcrumb = function(browsePath) { var breadcrumb = document.getElementById('c2-current-path'); if (!breadcrumb) return; var rel = C2.normalizeFilePath(browsePath || '.'); if (C2.implantPwd) { breadcrumb.textContent = C2.resolvePathAgainstPwd(C2.implantPwd, rel); breadcrumb.title = rel; } else { breadcrumb.textContent = rel; breadcrumb.title = ''; } }; C2.parseLsLine = function(line) { var trimmed = String(line || '').trim(); if (!trimmed || /^total\s+\d+/i.test(trimmed)) return null; // Beacon 结构化输出:type\tmode\tsize\tname var beaconParts = trimmed.split('\t'); if (beaconParts.length >= 4) { var bName = beaconParts.slice(3).join('\t').trim(); var bMode = beaconParts[1].trim(); var bType = beaconParts[0].trim(); if (bName && bName !== '.' && bName !== '..') { return { mode: bMode || bType, size: beaconParts[2].trim(), name: bName, isDir: bType.charAt(0) === 'd' || bMode.charAt(0) === 'd' }; } return null; } // 原生 ls -l 输出 var m = trimmed.match(/^(\S+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.+)$/); if (!m) return null; var name = m[9].trim(); var arrow = name.indexOf(' -> '); if (arrow > 0) name = name.slice(0, arrow).trim(); if (!name || name === '.' || name === '..') return null; return { mode: m[1], size: m[5], name: name, isDir: m[1].charAt(0) === 'd' }; }; C2.isDownloadShellError = function(text) { var lower = String(text || '').toLowerCase(); return lower.indexOf('c2_download_err:') >= 0 || lower.indexOf('no such file') >= 0 || lower.indexOf('permission denied') >= 0 || lower.indexOf('is a directory') >= 0 || lower.indexOf('cannot open') >= 0 || lower.indexOf('not a regular file') >= 0; }; C2.refreshImplantPwd = function(sessionId, callback) { if (!sessionId) { if (callback) callback(); return; } apiRequest('POST', `${API_BASE}/tasks`, { session_id: sessionId, task_type: 'pwd', payload: {} }).then(function(data) { if (data.error) { if (callback) callback(); return; } var taskId = data.task && data.task.id ? data.task.id : data.task_id; if (!taskId) { if (callback) callback(); return; } C2.waitForImplantPwd(taskId, callback); }).catch(function() { if (callback) callback(); }); }; C2.waitForImplantPwd = function(taskId, callback) { var attempts = 0; var poll = function() { if (++attempts > 30) { if (callback) callback(); return; } apiRequest('GET', `${API_BASE}/tasks/${taskId}`).then(function(data) { var task = data.task; if (task && task.status === 'success' && task.resultText) { C2.implantPwd = String(task.resultText).trim().split('\n').pop().trim(); C2.updateFileBreadcrumb(C2.currentPath); if (callback) callback(); } else if (task && task.status === 'failed') { if (callback) callback(); } else { setTimeout(poll, 300); } }); }; poll(); }; C2.getParentFilePath = function(path) { var p = C2.normalizeFilePath(path); if (p === '.' || p === '/') return '.'; var idx = p.lastIndexOf('/'); if (idx < 0) return '.'; var parent = p.slice(0, idx); return parent || '.'; }; C2.goToParentDirectory = function() { var parent = C2.getParentFilePath(C2.currentPath || '.'); C2.loadFileList(null, parent); }; C2.openDirectory = function(name) { var next = C2.joinFilePath(C2.currentPath || '.', name); C2.loadFileList(null, next); }; C2.loadFileList = function(sessionId, path) { // 兼容误传:仅传路径时(如旧版 loadFileList('..'))自动纠正 if (sessionId && path == null && typeof sessionId === 'string' && (sessionId === '..' || sessionId === '.' || sessionId.indexOf('/') >= 0)) { path = sessionId; sessionId = null; } if (!sessionId) sessionId = C2.selectedSessionId; if (!sessionId) return; if (!path) path = C2.currentPath || '.'; path = C2.normalizeFilePath(path); const container = document.getElementById('c2-file-list'); const breadcrumb = document.getElementById('c2-current-path'); if (container) container.innerHTML = '
' + escapeHtml(c2t('c2.files.loading')) + '
'; apiRequest('POST', `${API_BASE}/tasks`, { session_id: sessionId, task_type: 'ls', payload: { path: path } }).then(data => { if (data.error) { if (container) container.innerHTML = `
${data.error}
`; 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 = '
' + escapeHtml(c2t('c2.files.timeout')) + '
'; return; } apiRequest('GET', `${API_BASE}/tasks/${taskId}`).then(data => { const task = data.task; if (task && task.status === 'success') { C2.currentPath = path; C2.updateFileBreadcrumb(path); C2.renderFileList(task.resultText || ''); C2.refreshImplantPwd(sessionId); } else if (task && task.status === 'failed') { if (container) container.innerHTML = `
${escapeHtml(task.error || c2t('c2.files.failed'))}
`; } else { setTimeout(check, 500); } }); }; check(); }; C2.renderFileList = function(output) { const container = document.getElementById('c2-file-list'); if (!container) return; const entries = output.split('\n') .map(C2.parseLsLine) .filter(function(entry) { return entry != null; }); if (entries.length === 0) { container.innerHTML = '
' + escapeHtml(c2t('c2.files.emptyDir')) + '
'; return; } container.innerHTML = ` ${entries.map(function(entry) { return ` `; }).join('')}
${escapeHtml(c2t('c2.files.colName'))} ${escapeHtml(c2t('c2.files.colSize'))} ${escapeHtml(c2t('c2.files.colMode'))} ${escapeHtml(c2t('c2.files.colActions'))}
${entry.isDir ? '📁' : '📄'} ${escapeHtml(entry.name)} ${escapeHtml(entry.size)} ${escapeHtml(entry.mode)} ${entry.isDir ? `` : `` }
`; }; C2.refreshFiles = function() { C2.loadFileList(null, C2.currentPath); }; C2.sessionTransport = function(session) { if (!session || !session.metadata) return ''; return String(session.metadata.transport || '').toLowerCase(); }; C2.sessionSupportsUpload = function(session) { if (!session) { return { supported: false, reasonKey: 'c2.files.uploadUnsupported' }; } if (session.implantUuid && String(session.implantUuid).indexOf('curl_') === 0) { return { supported: false, reasonKey: 'c2.files.uploadCurlBeacon' }; } var transport = C2.sessionTransport(session); // 编译 Beacon:HTTP/HTTPS/TCP(CSB1) 均走二进制/结构化协议,支持 upload if (transport === 'tcp_beacon' || transport === 'http_beacon' || transport === 'https_beacon') { return { supported: true, reasonKey: '' }; } // 经典 TCP 反弹 Shell(bash/nc,metadata.transport=tcp_reverse) if (transport === 'tcp_reverse' || (session.hostname && String(session.hostname).indexOf('tcp_') === 0)) { return { supported: false, reasonKey: 'c2.files.uploadTcpShell' }; } return { supported: true, reasonKey: '' }; }; C2.updateFileUploadButton = function(session) { if (!session && C2.selectedSessionId) { session = C2.sessions.find(function(s) { return s.id === C2.selectedSessionId; }); } var btn = document.getElementById('c2-file-upload-btn'); if (!btn) return; var cap = C2.sessionSupportsUpload(session); btn.disabled = !cap.supported || !!C2.fileUploadBusy; btn.title = cap.supported ? c2t('c2.files.upload') : c2t(cap.reasonKey); if (!cap.supported) { btn.classList.add('is-disabled'); } else { btn.classList.remove('is-disabled'); } var hint = document.getElementById('c2-file-upload-hint'); if (hint) { if (!cap.supported) { hint.hidden = false; hint.textContent = c2t(cap.reasonKey); } else { hint.hidden = true; hint.textContent = ''; } } }; C2.setFileUploadProgress = function(visible, percent, filename) { var row = document.getElementById('c2-file-upload-progress'); if (!row) return; if (!visible) { row.hidden = true; return; } row.hidden = false; var fill = document.getElementById('c2-file-upload-progress-fill'); var label = document.getElementById('c2-file-upload-progress-label'); if (fill) fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%'; if (label) { label.textContent = c2t('c2.files.uploading', { name: filename || '', percent: percent || 0 }); } }; C2.openFileUploadPicker = function() { if (!C2.selectedSessionId || C2.fileUploadBusy) return; var session = C2.sessions.find(function(s) { return s.id === C2.selectedSessionId; }); var cap = C2.sessionSupportsUpload(session); if (!cap.supported) { showToast(c2t(cap.reasonKey), 'warn'); return; } var inp = document.getElementById('c2-file-upload-input'); if (inp) inp.click(); }; C2.onC2FileUploadPick = function(ev) { var input = ev && ev.target; var file = input && input.files && input.files[0]; if (!file) return; if (input) input.value = ''; C2.uploadFileToImplant(file); }; C2.uploadFileToImplant = function(file) { if (!C2.selectedSessionId || C2.fileUploadBusy || !file) return; var sessionId = C2.selectedSessionId; var remotePath = C2.resolveRemotePath(C2.currentPath || '.', file.name); var uploadUrl = API_BASE + '/files/upload'; C2.fileUploadBusy = true; C2.updateFileUploadButton(); C2.setFileUploadProgress(true, 0, file.name); var form = new FormData(); form.append('session_id', sessionId); form.append('remote_path', remotePath); form.append('file', file); var uploadPromise; if (typeof apiUploadWithProgress === 'function') { uploadPromise = apiUploadWithProgress(uploadUrl, form, { onProgress: function(p) { C2.setFileUploadProgress(true, Math.min(p.percent || 0, 50), file.name); } }); } else if (typeof apiFetch === 'function') { uploadPromise = apiFetch(uploadUrl, { method: 'POST', body: form }); } else { uploadPromise = fetch(uploadUrl, { method: 'POST', body: form }); } uploadPromise.then(function(res) { if (!res.ok) { return res.text().then(function(text) { throw new Error(text || c2t('c2.files.failed')); }); } return res.json(); }).then(function(uploadData) { var fileId = uploadData && uploadData.file_id; if (!fileId) throw new Error(c2t('c2.files.failed')); C2.setFileUploadProgress(true, 55, file.name); return apiRequest('POST', API_BASE + '/tasks', { session_id: sessionId, task_type: 'upload', payload: { remote_path: remotePath, file_id: fileId } }); }).then(function(taskData) { if (taskData && taskData.error) throw new Error(taskData.error); var taskId = taskData.task && taskData.task.id ? taskData.task.id : taskData.task_id; if (!taskId) { showToast(c2t('c2.files.uploadQueued'), 'success'); C2.fileUploadBusy = false; C2.setFileUploadProgress(false); C2.updateFileUploadButton(); return; } if (taskData.task && taskData.task.approvalStatus === 'pending') { showToast(c2t('c2.files.uploadPendingApproval'), 'info'); } C2.waitForFileUpload(taskId, file.name); }).catch(function(err) { showToast((err && err.message) || c2t('c2.files.failed'), 'error'); C2.fileUploadBusy = false; C2.setFileUploadProgress(false); C2.updateFileUploadButton(); }); }; C2.waitForFileUpload = function(taskId, filename) { var attempts = 0; var check = function() { if (++attempts > 120) { showToast(c2t('c2.files.timeout'), 'error'); C2.fileUploadBusy = false; C2.setFileUploadProgress(false); C2.updateFileUploadButton(); return; } apiRequest('GET', API_BASE + '/tasks/' + taskId).then(function(data) { var task = data.task; if (task && task.approvalStatus === 'pending' && task.status === 'queued') { C2.setFileUploadProgress(true, 60, filename); setTimeout(check, 1000); return; } if (task && task.status === 'success') { C2.setFileUploadProgress(true, 100, filename); showToast(c2t('c2.files.uploadOk'), 'success'); C2.fileUploadBusy = false; setTimeout(function() { C2.setFileUploadProgress(false); }, 400); C2.updateFileUploadButton(); C2.refreshFiles(); } else if (task && task.status === 'failed') { showToast(task.error || task.resultText || c2t('c2.files.failed'), 'error'); C2.fileUploadBusy = false; C2.setFileUploadProgress(false); C2.updateFileUploadButton(); } else { var pct = 60 + Math.min(35, Math.floor(attempts / 3)); C2.setFileUploadProgress(true, pct, filename); setTimeout(check, 500); } }).catch(function() { C2.fileUploadBusy = false; C2.setFileUploadProgress(false); C2.updateFileUploadButton(); }); }; check(); }; C2.saveDownloadBlob = function(blob, filename) { 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); }; C2.saveDownloadContent = function(content, filename) { const text = String(content || ''); if (C2.isDownloadShellError(text)) { throw new Error(text.trim() || c2t('c2.files.failed')); } const b64 = text.replace(/\s/g, ''); let bytes; try { if (/^[A-Za-z0-9+/=]+$/.test(b64) && b64.length > 0) { const binary = atob(b64); bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); } else { bytes = new TextEncoder().encode(text); } } catch (e) { bytes = new TextEncoder().encode(text); } C2.saveDownloadBlob(new Blob([bytes], { type: 'application/octet-stream' }), filename); }; C2.fetchTaskResultFile = function(taskId, filename) { const url = `${API_BASE}/tasks/${taskId}/result-file`; 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 => { C2.saveDownloadBlob(blob, filename); }).catch(err => { showToast((err && err.message) || c2t('c2.files.failed'), 'error'); }); }; C2.waitForFileDownload = function(taskId, filename) { let attempts = 0; const check = () => { if (++attempts > 120) { showToast(c2t('c2.files.timeout'), 'error'); return; } apiRequest('GET', `${API_BASE}/tasks/${taskId}`).then(data => { const task = data.task; if (task && task.status === 'success') { if (task.resultBlobPath) { C2.fetchTaskResultFile(taskId, filename); } else if (task.resultText != null) { try { C2.saveDownloadContent(task.resultText, filename); showToast(c2t('c2.files.downloadOk'), 'success'); } catch (err) { showToast((err && err.message) || c2t('c2.files.failed'), 'error'); } } else { C2.saveDownloadBlob(new Blob([], { type: 'application/octet-stream' }), filename); showToast(c2t('c2.files.downloadOk'), 'success'); } } else if (task && task.status === 'failed') { showToast(task.error || task.resultText || c2t('c2.files.failed'), 'error'); } else { setTimeout(check, 500); } }); }; check(); }; C2.downloadFile = function(filename) { if (!C2.selectedSessionId) return; const remotePath = C2.resolveRemotePath(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'); return; } const taskId = data.task?.id || data.task_id; if (!taskId) { showToast(c2t('c2.payloads.toastDownloadQueued'), 'success'); return; } C2.waitForFileDownload(taskId, filename); }); }; // ============================================================================ // 任务管理 // ============================================================================ 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 = '
'; html += `
${escapeHtml(c2t('c2.tasks.paginationShow', { start, end, total }))}
${escapeHtml(c2t('c2.tasks.paginationPage', { current: currentPage, total: totalPages }))}
`; html += '
'; 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 = '
' + escapeHtml(c2t('c2.tasks.emptySession')) + '
'; return; } container.innerHTML = tasks.map(t => { const rawId = t.id || ''; const cmd = formatTaskCommand(t); const cmdShort = truncateCommand(cmd, 40); return `
${escapeHtml(t.taskType || '')} ${cmdShort ? `${escapeHtml(cmdShort)}` : ''} ${escapeHtml(taskStatusLabel(t.status))} | ${formatDuration(t.durationMs)}
`; }).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 = '
' + escapeHtml(c2t('c2.tasks.emptyAll')) + '
'; if (selAll) selAll.disabled = true; C2.syncTasksToolbar(); return; } if (selAll) selAll.disabled = false; const delTitle = escapeHtml(c2t('c2.tasks.deleteOne')); container.innerHTML = ` ${C2.tasks.map(t => { const rawId = t.id || ''; 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' : '-'; const cmd = formatTaskCommand(t); const cmdShort = truncateCommand(cmd, 48); return ` `; }).join('')}
${escapeHtml(c2t('c2.tasks.colTask'))} ${escapeHtml(c2t('c2.tasks.colSession'))} ${escapeHtml(c2t('c2.tasks.colType'))} ${escapeHtml(c2t('c2.tasks.colCommand'))} ${escapeHtml(c2t('c2.tasks.colStatus'))} ${escapeHtml(c2t('c2.tasks.colDuration'))} ${escapeHtml(c2t('c2.tasks.colCreated'))} ${escapeHtml(c2t('c2.tasks.colActions'))}
${shortTaskId} ${sid} ${escapeHtml(t.taskType || '')} ${cmdShort ? escapeHtml(cmdShort) : '-'} ${escapeHtml(taskStatusLabel(t.status))} ${formatDuration(t.durationMs)} ${formatTime(t.createdAt)}
${t.status === 'queued' || t.status === 'sent' ? `` : ''}
`; 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; const cmd = formatTaskCommand(t); const hasPayload = t.payload && typeof t.payload === 'object' && Object.keys(t.payload).length > 0; const modalBox = modal.querySelector('.c2-modal'); if (modalBox) modalBox.classList.add('c2-modal--wide'); content.innerHTML = `

${escapeHtml(c2t('c2.tasks.modalTitle'))}

${escapeHtml(taskStatusLabel(t.status))}
${escapeHtml(c2t('c2.tasks.labelId'))} ${escapeHtml(t.id || '-')}
${escapeHtml(c2t('c2.tasks.labelSession'))} ${escapeHtml(t.sessionId || '-')}
${escapeHtml(c2t('c2.tasks.labelType'))} ${escapeHtml(t.taskType || '-')}
${escapeHtml(c2t('c2.tasks.labelDuration'))} ${formatDuration(t.durationMs)}
${escapeHtml(c2t('c2.tasks.labelCreated'))} ${formatTime(t.createdAt) || '-'}
${escapeHtml(c2t('c2.tasks.labelSent'))} ${formatTime(t.sentAt) || '-'}
${escapeHtml(c2t('c2.tasks.labelCompleted'))} ${formatTime(t.completedAt) || '-'}
${cmd ? `
${escapeHtml(c2t('c2.tasks.labelCommand'))}
${escapeHtml(cmd)}
` : ''} ${hasPayload && !cmd ? `
${escapeHtml(c2t('c2.tasks.labelPayload'))}
${escapeHtml(JSON.stringify(t.payload, null, 2))}
` : ''} ${t.error ? `
${escapeHtml(c2t('c2.tasks.labelError'))}
${escapeHtml(t.error)}
` : ''} ${t.resultText ? `
${escapeHtml(c2t('c2.tasks.labelResult'))}
${escapeHtml(t.resultText)}
` : ''}
`; modal.style.display = 'flex'; }; const local = C2.tasks.find(x => x.id === id); if (local) { renderTaskModal(local); return; } apiRequest('GET', `${API_BASE}/tasks/${encodeURIComponent(id)}`).then(data => { if (data.error) { showToast(String(data.error), 'error'); return; } if (data.task) renderTaskModal(data.task); else showToast(c2t('c2.tasks.emptyAll'), 'warn'); }).catch(err => showToast(err.message || String(err), 'error')); }; C2.cancelTask = function(id) { apiRequest('POST', `${API_BASE}/tasks/${encodeURIComponent(id)}/cancel`, {}).then(data => { if (data.error) showToast(String(data.error), 'error'); else { showToast(c2t('c2.tasks.toastCancelled'), 'success'); C2.loadTasks(C2.tasksPage || 1); } }).catch(err => showToast(err.message || String(err), 'error')); }; // ============================================================================ // 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 = ''; } else { kindSelect.innerHTML = kinds.map(function(k) { return ''; }).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 => `` ).join('') : ''; 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 => `` ).join(''); } else { buildOptionsHtml = ''; } 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 = `
✓ ${escapeHtml(c2t('c2.payloads.buildSuccessTitle'))}
${escapeHtml(c2t('c2.payloads.buildMetaOsArch', { os: data.payload?.os, arch: data.payload?.arch }))}
${escapeHtml(c2t('c2.payloads.buildSize', { bytes: data.payload?.size_bytes }))}
`; } } }).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 = '
'; html += `
${escapeHtml(c2t('c2.events.paginationShow', { start, end, total }))}
${escapeHtml(c2t('c2.events.paginationPage', { current: currentPage, total: totalPages }))}
`; html += '
'; 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 = '
' + escapeHtml(c2t('c2.events.empty')) + '
'; 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 `
${escapeHtml(e.message)}
${formatTime(e.createdAt)} · ${escapeHtml(e.category || '')}${e.sessionId ? ' · ' + escapeHtml(String(e.sessionId).substring(0, 8)) : ''}
`; }).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 = '
' + escapeHtml(c2t('c2.profiles.empty')) + '
'; return; } const defVal = c2t('c2.profiles.defaultValue'); container.innerHTML = C2.profiles.map(p => `

${escapeHtml(p.name)}

UA: ${escapeHtml(p.userAgent || defVal)}
URIs: ${escapeHtml((p.uris || []).join(', ') || defVal)}
Jitter: ${p.jitterMinMs || 0}ms – ${p.jitterMaxMs || 0}ms
`).join(''); }; C2.showCreateProfileModal = function() { const modal = document.getElementById('c2-modal'); const content = document.getElementById('c2-modal-content'); if (!content) return; content.innerHTML = `

${escapeHtml(c2t('c2.profiles.modalCreateTitle'))}

${escapeHtml(c2t('c2.profiles.hintUa'))}
${escapeHtml(c2t('c2.profiles.hintUris'))}
${escapeHtml(c2t('c2.profiles.hintHeaders'))}
`; 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.copyTaskBlock = function(elementId) { const el = document.getElementById(elementId); if (el && el.textContent) copyToClipboard(el.textContent); }; C2.closeModal = function() { const modal = document.getElementById('c2-modal'); if (modal) { modal.style.display = 'none'; const modalBox = modal.querySelector('.c2-modal'); if (modalBox) modalBox.classList.remove('c2-modal--wide'); } }; // ============================================================================ // 暴露到全局 // ============================================================================ 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); } }); })();