mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-15 04:51:01 +02:00
Add files via upload
This commit is contained in:
+213
-48
@@ -84,6 +84,16 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 原生下拉:避免 appearance:none 在部分浏览器中导致 select 无法正常展开 */
|
||||
#page-c2 select.form-control.c2-native-select,
|
||||
#page-c2-payloads select.form-control.c2-native-select,
|
||||
.c2-modal select.form-control.c2-native-select {
|
||||
appearance: auto;
|
||||
-webkit-appearance: menulist-button;
|
||||
background-image: none;
|
||||
padding-right: 14px;
|
||||
}
|
||||
|
||||
#page-c2 textarea.form-control,
|
||||
#page-c2-payloads textarea.form-control,
|
||||
.c2-modal textarea.form-control {
|
||||
@@ -104,7 +114,7 @@
|
||||
C2 Button Overrides (within C2 scope)
|
||||
============================================================================ */
|
||||
|
||||
.c2-listener-actions .btn-primary,
|
||||
.c2-listener-card-actions .btn-primary,
|
||||
.c2-payload-card .btn-primary,
|
||||
.c2-modal-footer .btn-primary {
|
||||
background: var(--c2-accent);
|
||||
@@ -118,7 +128,7 @@
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.c2-listener-actions .btn-primary:hover,
|
||||
.c2-listener-card-actions .btn-primary:hover,
|
||||
.c2-payload-card .btn-primary:hover,
|
||||
.c2-modal-footer .btn-primary:hover {
|
||||
background: var(--c2-accent-hover);
|
||||
@@ -258,10 +268,10 @@
|
||||
|
||||
.c2-listener-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
||||
gap: 16px;
|
||||
padding: 24px;
|
||||
align-items: start;
|
||||
grid-template-columns: repeat(auto-fill, minmax(292px, 1fr));
|
||||
gap: 20px;
|
||||
padding: 20px 24px 28px;
|
||||
align-items: stretch;
|
||||
}
|
||||
.c2-listener-grid:has(.c2-empty) {
|
||||
display: flex;
|
||||
@@ -269,68 +279,214 @@
|
||||
|
||||
.c2-listener-card {
|
||||
background: var(--c2-surface);
|
||||
border: 1.5px solid var(--c2-border);
|
||||
border-radius: var(--c2-radius);
|
||||
padding: 24px;
|
||||
transition: all 0.25s ease;
|
||||
position: relative;
|
||||
border: 1px solid var(--c2-border);
|
||||
border-radius: 14px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
box-shadow: var(--c2-shadow-sm);
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
border-top: 3px solid var(--c2-border);
|
||||
}
|
||||
|
||||
.c2-listener-card.running { border-left: 4px solid var(--c2-green); }
|
||||
.c2-listener-card.stopped { border-left: 4px solid var(--c2-text-muted); }
|
||||
.c2-listener-card.error { border-left: 4px solid var(--c2-amber); }
|
||||
.c2-listener-card--running { border-top-color: var(--c2-green); }
|
||||
.c2-listener-card--stopped { border-top-color: var(--c2-text-muted); }
|
||||
.c2-listener-card--error { border-top-color: var(--c2-amber); }
|
||||
|
||||
.c2-listener-card:hover {
|
||||
box-shadow: var(--c2-shadow-md);
|
||||
border-color: var(--c2-border-hover);
|
||||
}
|
||||
|
||||
.c2-listener-header {
|
||||
.c2-listener-card-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16px;
|
||||
padding: 18px 18px 0;
|
||||
}
|
||||
|
||||
.c2-ltype-mark {
|
||||
flex-shrink: 0;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.02em;
|
||||
color: #fff;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.c2-ltype-mark--http { background: linear-gradient(145deg, #3b82f6, #1d4ed8); }
|
||||
.c2-ltype-mark--https { background: linear-gradient(145deg, #6366f1, #4338ca); }
|
||||
.c2-ltype-mark--tcp { background: linear-gradient(145deg, #8b5cf6, #6d28d9); }
|
||||
.c2-ltype-mark--ws { background: linear-gradient(145deg, #0ea5e9, #0369a1); }
|
||||
.c2-ltype-mark--def { background: linear-gradient(145deg, #64748b, #475569); }
|
||||
|
||||
.c2-listener-card-head-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.c2-listener-card-title-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.c2-listener-name {
|
||||
margin: 0;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
font-size: 17px;
|
||||
line-height: 1.3;
|
||||
color: var(--c2-text);
|
||||
letter-spacing: -0.02em;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.c2-listener-id {
|
||||
font-size: 11px;
|
||||
color: var(--c2-text-muted);
|
||||
font-family: var(--c2-mono);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.c2-listener-type {
|
||||
.c2-listener-pill {
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
padding: 4px 10px;
|
||||
border-radius: var(--c2-radius-xs);
|
||||
border-radius: 999px;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.c2-listener-pill--running {
|
||||
background: var(--c2-green-dim);
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
.c2-listener-pill--stopped {
|
||||
background: var(--c2-surface-alt);
|
||||
color: var(--c2-text-dim);
|
||||
font-weight: 600;
|
||||
border: 1px solid var(--c2-border);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.c2-listener-info {
|
||||
font-size: 13px;
|
||||
color: var(--c2-text-dim);
|
||||
margin-bottom: 20px;
|
||||
line-height: 1.8;
|
||||
.c2-listener-pill--error {
|
||||
background: var(--c2-amber-dim);
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.c2-listener-address {
|
||||
.c2-listener-id-row {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.c2-listener-id-full {
|
||||
display: block;
|
||||
font-family: var(--c2-mono);
|
||||
font-size: 13px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 11px;
|
||||
color: var(--c2-text-muted);
|
||||
line-height: 1.4;
|
||||
word-break: break-all;
|
||||
background: var(--c2-surface-alt);
|
||||
padding: 6px 8px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--c2-border);
|
||||
}
|
||||
|
||||
.c2-listener-card-body {
|
||||
padding: 14px 18px 4px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.c2-listener-kv {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 8px 12px;
|
||||
align-items: baseline;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.c2-listener-kv-label {
|
||||
color: var(--c2-text-muted);
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.c2-listener-kv-val {
|
||||
color: var(--c2-text);
|
||||
font-weight: 600;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--c2-text);
|
||||
min-width: 0;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.c2-listener-mono {
|
||||
font-family: var(--c2-mono);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.c2-listener-profile-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
align-self: flex-start;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #5b21b6;
|
||||
background: rgba(139, 92, 246, 0.1);
|
||||
border: 1px solid rgba(139, 92, 246, 0.25);
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.c2-listener-profile-badge span:last-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.c2-listener-profile-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #7c3aed;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.c2-listener-remark {
|
||||
font-size: 12px;
|
||||
color: var(--c2-text-dim);
|
||||
line-height: 1.45;
|
||||
padding: 8px 10px;
|
||||
background: var(--c2-surface-alt);
|
||||
border-radius: 8px;
|
||||
border: 1px dashed var(--c2-border);
|
||||
}
|
||||
|
||||
.c2-listener-meta-row {
|
||||
font-size: 12px;
|
||||
color: var(--c2-text-dim);
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.c2-listener-meta-label {
|
||||
font-weight: 600;
|
||||
color: var(--c2-text-muted);
|
||||
}
|
||||
|
||||
.c2-listener-meta-time {
|
||||
font-family: var(--c2-mono);
|
||||
font-size: 11px;
|
||||
color: var(--c2-text-dim);
|
||||
}
|
||||
|
||||
.c2-status-dot {
|
||||
@@ -339,23 +495,32 @@
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
background: var(--c2-text-muted);
|
||||
}
|
||||
|
||||
.c2-status-dot.running { background: var(--c2-green); box-shadow: 0 0 0 3px var(--c2-green-dim); }
|
||||
.c2-status-dot.stopped { background: var(--c2-text-muted); }
|
||||
.c2-status-dot.active { background: var(--c2-green); box-shadow: 0 0 0 3px var(--c2-green-dim); }
|
||||
.c2-status-dot.running { background: var(--c2-green); box-shadow: 0 0 0 3px var(--c2-green-dim); }
|
||||
.c2-status-dot.stopped { background: var(--c2-text-muted); }
|
||||
.c2-status-dot.error { background: var(--c2-amber); box-shadow: 0 0 0 3px var(--c2-amber-dim); }
|
||||
.c2-status-dot.active { background: var(--c2-green); box-shadow: 0 0 0 3px var(--c2-green-dim); }
|
||||
.c2-status-dot.sleeping { background: var(--c2-amber); box-shadow: 0 0 0 3px var(--c2-amber-dim); }
|
||||
.c2-status-dot.dead { background: var(--c2-text-muted); }
|
||||
.c2-status-dot.dead { background: var(--c2-text-muted); }
|
||||
|
||||
.c2-listener-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
padding-top: 16px;
|
||||
.c2-listener-card-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 10px;
|
||||
padding: 14px 16px 16px;
|
||||
margin-top: auto;
|
||||
border-top: 1px solid var(--c2-border);
|
||||
background: linear-gradient(180deg, rgba(248, 250, 252, 0.5) 0%, var(--c2-surface-alt) 100%);
|
||||
}
|
||||
|
||||
.c2-listener-actions button { flex: 1; min-width: 70px; }
|
||||
.c2-listener-card-actions button {
|
||||
min-height: 40px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
border-radius: var(--c2-radius-xs);
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
Session Management
|
||||
|
||||
@@ -2106,9 +2106,19 @@
|
||||
"bindHintExternal": "Use 0.0.0.0 to allow external access",
|
||||
"callbackHost": "Callback host (optional)",
|
||||
"callbackHostHint": "Public IP or hostname stored for payloads/beacons; separate from bind address. If empty, payload generation falls back to bind address / auto-detect.",
|
||||
"malleableProfile": "Malleable Profile",
|
||||
"malleableProfileHint": "Optional; HTTP/HTTPS Beacon response headers and traffic disguise. Stop and start the listener again for changes to take effect.",
|
||||
"malleableProfileNone": "None",
|
||||
"malleableProfileNonHttpHint": "This listener type does not use a Malleable Profile. You can still bind one here for later if you switch to HTTP/HTTPS Beacon.",
|
||||
"malleableProfileEmptyListHint": "No saved profiles yet. Create one under C2 → Traffic disguise / Malleable Profile, then pick it here.",
|
||||
"placeholderRemarkLong": "Optional remark",
|
||||
"editTitle": "Edit Listener",
|
||||
"startedAt": "Started {{time}}",
|
||||
"startedAtPrefix": "Started",
|
||||
"statusError": "Error",
|
||||
"bindEndpoint": "Listen address",
|
||||
"callbackShort": "Callback",
|
||||
"profileBadgeTitle": "Malleable Profile bound",
|
||||
"confirmDelete": "Delete this listener? All related sessions and tasks will be removed.",
|
||||
"toastFillRequired": "Please fill in all required fields",
|
||||
"toastCreated": "Listener created",
|
||||
@@ -2116,6 +2126,8 @@
|
||||
"toastStopped": "Listener stopped",
|
||||
"toastDeleted": "Listener deleted",
|
||||
"toastUpdated": "Listener updated",
|
||||
"loadingProfiles": "Loading Malleable Profiles…",
|
||||
"toastProfilesLoadFailed": "Failed to load Malleable Profiles",
|
||||
"submitCreate": "Create",
|
||||
"typeLabels": {
|
||||
"http_beacon": "HTTP Beacon",
|
||||
|
||||
@@ -2095,9 +2095,19 @@
|
||||
"bindHintExternal": "使用 0.0.0.0 允许外部访问",
|
||||
"callbackHost": "回连地址(可选)",
|
||||
"callbackHostHint": "公网 IP 或域名,写入配置供 Payload/Beacon 使用;与「绑定地址」分离。不填则生成 Payload 时按绑定地址或自动探测。",
|
||||
"malleableProfile": "Malleable Profile",
|
||||
"malleableProfileHint": "可选;用于 HTTP/HTTPS Beacon 服务端响应头等流量伪装。修改后需停止并重新启动监听器才会生效。",
|
||||
"malleableProfileNone": "不使用",
|
||||
"malleableProfileNonHttpHint": "当前监听器类型不会使用 Profile;若之后改为 HTTP/HTTPS Beacon,可在此预先绑定。",
|
||||
"malleableProfileEmptyListHint": "暂无已保存的 Profile。请先到侧边栏「流量伪装 / Malleable Profile」页创建,再返回此处选择。",
|
||||
"placeholderRemarkLong": "可选的备注说明",
|
||||
"editTitle": "编辑监听器",
|
||||
"startedAt": "启动于 {{time}}",
|
||||
"startedAtPrefix": "启动于",
|
||||
"statusError": "异常",
|
||||
"bindEndpoint": "监听地址",
|
||||
"callbackShort": "回连",
|
||||
"profileBadgeTitle": "已绑定 Malleable Profile",
|
||||
"confirmDelete": "确定删除此监听器?相关会话与任务将被清除。",
|
||||
"toastFillRequired": "请填写必填项",
|
||||
"toastCreated": "监听器已创建",
|
||||
@@ -2105,6 +2115,8 @@
|
||||
"toastStopped": "监听器已停止",
|
||||
"toastDeleted": "监听器已删除",
|
||||
"toastUpdated": "监听器已更新",
|
||||
"loadingProfiles": "正在加载 Malleable Profile 列表…",
|
||||
"toastProfilesLoadFailed": "加载 Malleable Profile 列表失败",
|
||||
"submitCreate": "创建",
|
||||
"typeLabels": {
|
||||
"http_beacon": "HTTP Beacon",
|
||||
|
||||
+222
-32
@@ -151,6 +151,74 @@
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/** 监听器表单:Malleable Profile 下拉选项 HTML(value / 文本已转义) */
|
||||
function listenerProfileSelectHtml(selectedProfileId) {
|
||||
const sel = selectedProfileId ? String(selectedProfileId) : '';
|
||||
let opts = `<option value="">${escapeHtml(c2t('c2.listeners.malleableProfileNone'))}</option>`;
|
||||
for (const p of (C2.profiles || [])) {
|
||||
if (!p) continue;
|
||||
const pid = p.id || p.ID;
|
||||
if (!pid) continue;
|
||||
const idEsc = escapeHtml(String(pid));
|
||||
const nameEsc = escapeHtml(p.name || pid);
|
||||
const selected = sel && String(pid) === sel ? ' selected' : '';
|
||||
opts += `<option value="${idEsc}"${selected}>${nameEsc}</option>`;
|
||||
}
|
||||
return opts;
|
||||
}
|
||||
|
||||
function listenerResolvedProfileId(l) {
|
||||
if (!l) return '';
|
||||
const v = l.profileId != null && l.profileId !== '' ? l.profileId : l.profile_id;
|
||||
return v != null ? String(v).trim() : '';
|
||||
}
|
||||
|
||||
/** 监听器卡片展示用 Profile 名称(依赖 C2.profiles,由 loadListeners 一并拉取) */
|
||||
function listenerProfileDisplayName(l) {
|
||||
const pid = listenerResolvedProfileId(l);
|
||||
if (!pid) return '';
|
||||
const list = C2.profiles || [];
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const p = list[i];
|
||||
if (p && (p.id === pid || p.ID === pid)) return String(p.name || p.id || pid).trim() || pid;
|
||||
}
|
||||
return pid.length > 18 ? pid.substring(0, 16) + '…' : pid;
|
||||
}
|
||||
|
||||
function listenerTypeVisualClass(type) {
|
||||
const t = String(type || '').toLowerCase();
|
||||
if (t === 'https_beacon') return 'c2-ltype-mark--https';
|
||||
if (t === 'http_beacon') return 'c2-ltype-mark--http';
|
||||
if (t === 'tcp_reverse') return 'c2-ltype-mark--tcp';
|
||||
if (t === 'websocket') return 'c2-ltype-mark--ws';
|
||||
return 'c2-ltype-mark--def';
|
||||
}
|
||||
|
||||
function listenerTypeShortLabel(type) {
|
||||
const t = String(type || '').toLowerCase();
|
||||
if (t === 'https_beacon') return 'HTTPS';
|
||||
if (t === 'http_beacon') return 'HTTP';
|
||||
if (t === 'tcp_reverse') return 'TCP';
|
||||
if (t === 'websocket') return 'WS';
|
||||
return '?';
|
||||
}
|
||||
|
||||
function listenerCardStatusPillLabel(status) {
|
||||
const s = String(status || '').toLowerCase();
|
||||
if (s === 'running') return c2t('c2.listeners.running');
|
||||
if (s === 'stopped') return c2t('c2.listeners.stopped');
|
||||
if (s === 'error') return c2t('c2.listeners.statusError');
|
||||
return c2t('c2.listeners.stopped');
|
||||
}
|
||||
|
||||
/** 避免 i18n 插值把日期里的「/」转成 /,与 formatTime 拼接后整体转义 */
|
||||
function formatListenerStartedHtml(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
const prefix = c2t('c2.listeners.startedAtPrefix');
|
||||
const time = formatTime(dateStr);
|
||||
return '<div class="c2-listener-meta-row"><span class="c2-listener-meta-label">' + escapeHtml(prefix) + '</span> <span class="c2-listener-meta-time">' + escapeHtml(time) + '</span></div>';
|
||||
}
|
||||
|
||||
function copyToClipboard(text) {
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(text).then(() => showToast(c2t('c2.clipboardCopied'), 'success'));
|
||||
@@ -204,13 +272,33 @@
|
||||
// ============================================================================
|
||||
|
||||
C2.loadListeners = function() {
|
||||
apiRequest('GET', `${API_BASE}/listeners`).then(data => {
|
||||
C2.listeners = data.listeners || [];
|
||||
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;
|
||||
@@ -233,33 +321,60 @@
|
||||
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>
|
||||
container.innerHTML = C2.listeners.map(function(l) {
|
||||
const st = String(l.status || 'stopped').toLowerCase();
|
||||
const stUi = st === 'running' || st === 'stopped' || st === 'error' ? st : 'stopped';
|
||||
const profilePid = listenerResolvedProfileId(l);
|
||||
const profileName = listenerProfileDisplayName(l);
|
||||
const profileBadge = profilePid
|
||||
? '<div class="c2-listener-profile-badge" title="' + escapeHtml(c2t('c2.listeners.profileBadgeTitle')) + '"><span class="c2-listener-profile-dot" aria-hidden="true"></span><span>' + escapeHtml(profileName) + '</span></div>'
|
||||
: '';
|
||||
const cb = C2.getListenerCallbackHost(l);
|
||||
const cbRow = cb
|
||||
? '<div class="c2-listener-kv"><span class="c2-listener-kv-label">' + escapeHtml(c2t('c2.listeners.callbackShort')) + '</span><span class="c2-listener-kv-val c2-listener-mono">' + escapeHtml(cb) + '</span></div>'
|
||||
: '';
|
||||
const remarkRow = l.remark ? '<div class="c2-listener-remark">' + escapeHtml(l.remark) + '</div>' : '';
|
||||
const startedHtml = formatListenerStartedHtml(l.startedAt);
|
||||
const pillLabel = escapeHtml(listenerCardStatusPillLabel(st));
|
||||
const typeMark = escapeHtml(listenerTypeShortLabel(l.type));
|
||||
const typeVis = listenerTypeVisualClass(l.type);
|
||||
const fullType = escapeHtml(listenerTypeLabel(l.type));
|
||||
const bindVal = escapeHtml(String(l.bindHost)) + ':' + escapeHtml(String(l.bindPort));
|
||||
|
||||
return `
|
||||
<article class="c2-listener-card c2-listener-card--${stUi}" data-listener-id="${escapeHtml(l.id)}">
|
||||
<div class="c2-listener-card-head">
|
||||
<div class="c2-ltype-mark ${typeVis}" title="${fullType}"><span>${typeMark}</span></div>
|
||||
<div class="c2-listener-card-head-main">
|
||||
<div class="c2-listener-card-title-row">
|
||||
<h3 class="c2-listener-name">${escapeHtml(l.name)}</h3>
|
||||
<span class="c2-listener-pill c2-listener-pill--${stUi}">${pillLabel}</span>
|
||||
</div>
|
||||
<div class="c2-listener-id-row">
|
||||
<code class="c2-listener-id-full" title="${escapeHtml(l.id)}">${escapeHtml(l.id)}</code>
|
||||
</div>
|
||||
</div>
|
||||
<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 class="c2-listener-card-body">
|
||||
<div class="c2-listener-kv">
|
||||
<span class="c2-listener-kv-label">${escapeHtml(c2t('c2.listeners.bindEndpoint'))}</span>
|
||||
<span class="c2-listener-kv-val c2-listener-mono"><span class="c2-status-dot ${escapeHtml(st)}"></span>${bindVal}</span>
|
||||
</div>
|
||||
${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>` : ''}
|
||||
${cbRow}
|
||||
${profileBadge}
|
||||
${remarkRow}
|
||||
${startedHtml}
|
||||
</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>`
|
||||
<div class="c2-listener-card-actions">
|
||||
${l.status === 'stopped'
|
||||
? `<button type="button" class="btn-primary btn-sm" onclick="C2.startListener('${l.id}')">▶ ${escapeHtml(c2t('c2.listeners.start'))}</button>`
|
||||
: `<button type="button" class="btn-secondary btn-sm" onclick="C2.stopListener('${l.id}')">⏹ ${escapeHtml(c2t('c2.listeners.stop'))}</button>`
|
||||
}
|
||||
<button 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>
|
||||
<button type="button" class="btn-secondary btn-sm" onclick="C2.editListener('${l.id}')">${escapeHtml(c2t('c2.listeners.edit'))}</button>
|
||||
<button type="button" class="btn-danger btn-sm" onclick="C2.deleteListener('${l.id}')">${escapeHtml(c2t('c2.listeners.delete'))}</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
</article>`;
|
||||
}).join('');
|
||||
};
|
||||
|
||||
C2.getListenerCallbackHost = function(l) {
|
||||
@@ -276,9 +391,25 @@
|
||||
C2.showCreateListenerModal = function() {
|
||||
const modal = document.getElementById('c2-modal');
|
||||
const content = document.getElementById('c2-modal-content');
|
||||
if (!content) return;
|
||||
if (!content || !modal) return;
|
||||
|
||||
modal.style.display = 'flex';
|
||||
content.innerHTML = `
|
||||
<div class="c2-modal-header">
|
||||
<h3>${escapeHtml(c2t('c2.listeners.modalCreateTitle'))}</h3>
|
||||
<button class="c2-modal-close" onclick="C2.closeModal()">×</button>
|
||||
</div>
|
||||
<div class="c2-modal-body">
|
||||
<p class="form-hint" style="margin-top:0;">${escapeHtml(c2t('c2.listeners.loadingProfiles'))}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
C2.ensureProfilesLoaded().then(() => {
|
||||
const profileOpts = listenerProfileSelectHtml('');
|
||||
const emptyProfHintCreate = (C2.profiles && C2.profiles.length > 0)
|
||||
? ''
|
||||
: `<div class="form-hint" style="margin-bottom:6px;color:#b45309;">${escapeHtml(c2t('c2.listeners.malleableProfileEmptyListHint'))}</div>`;
|
||||
content.innerHTML = `
|
||||
<div class="c2-modal-header">
|
||||
<h3>${escapeHtml(c2t('c2.listeners.modalCreateTitle'))}</h3>
|
||||
<button class="c2-modal-close" onclick="C2.closeModal()">×</button>
|
||||
@@ -291,7 +422,7 @@
|
||||
</div>
|
||||
<div class="c2-form-group">
|
||||
<label>${escapeHtml(c2t('c2.listeners.type'))}</label>
|
||||
<select id="c2-listener-type" class="form-control">
|
||||
<select id="c2-listener-type" class="form-control c2-native-select" onchange="C2.syncListenerProfileRowForType()">
|
||||
<option value="http_beacon">HTTP Beacon</option>
|
||||
<option value="https_beacon">HTTPS Beacon</option>
|
||||
<option value="tcp_reverse">TCP Reverse</option>
|
||||
@@ -310,6 +441,12 @@
|
||||
<input type="number" id="c2-listener-port" class="form-control" placeholder="8443">
|
||||
</div>
|
||||
</div>
|
||||
<div class="c2-form-group" id="c2-listener-profile-group">
|
||||
<label>${escapeHtml(c2t('c2.listeners.malleableProfile'))}</label>
|
||||
${emptyProfHintCreate}
|
||||
<select id="c2-listener-profile-id" class="form-control c2-native-select">${profileOpts}</select>
|
||||
<div class="form-hint">${escapeHtml(c2t('c2.listeners.malleableProfileHint'))}</div>
|
||||
</div>
|
||||
<div class="c2-form-group">
|
||||
<label>${escapeHtml(c2t('c2.listeners.callbackHost'))}</label>
|
||||
<input type="text" id="c2-listener-callback-host" class="form-control" placeholder="">
|
||||
@@ -325,7 +462,25 @@
|
||||
<button class="btn-primary" onclick="C2.createListener()">${escapeHtml(c2t('c2.listeners.submitCreate'))}</button>
|
||||
</div>
|
||||
`;
|
||||
modal.style.display = 'flex';
|
||||
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() {
|
||||
@@ -341,9 +496,12 @@
|
||||
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
|
||||
callback_host: callbackHost,
|
||||
profile_id: profileId
|
||||
}).then(data => {
|
||||
if (data.error) {
|
||||
showToast(data.error, 'error');
|
||||
@@ -388,12 +546,32 @@
|
||||
if (!l) return;
|
||||
|
||||
const cbHost = C2.getListenerCallbackHost(l);
|
||||
|
||||
const modal = document.getElementById('c2-modal');
|
||||
const content = document.getElementById('c2-modal-content');
|
||||
if (!content) return;
|
||||
if (!content || !modal) return;
|
||||
|
||||
modal.style.display = 'flex';
|
||||
content.innerHTML = `
|
||||
<div class="c2-modal-header">
|
||||
<h3>${escapeHtml(c2t('c2.listeners.editTitle'))}</h3>
|
||||
<button class="c2-modal-close" onclick="C2.closeModal()">×</button>
|
||||
</div>
|
||||
<div class="c2-modal-body">
|
||||
<p class="form-hint" style="margin-top:0;">${escapeHtml(c2t('c2.listeners.loadingProfiles'))}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
C2.ensureProfilesLoaded().then(() => {
|
||||
const resolvedPid = listenerResolvedProfileId(l);
|
||||
const profileOpts = listenerProfileSelectHtml(resolvedPid);
|
||||
const lt = String(l.type || '').toLowerCase();
|
||||
const httpHint = (lt === 'http_beacon' || lt === 'https_beacon')
|
||||
? ''
|
||||
: `<div class="form-hint" style="margin-bottom:6px;">${escapeHtml(c2t('c2.listeners.malleableProfileNonHttpHint'))}</div>`;
|
||||
const emptyProfHint = (C2.profiles && C2.profiles.length > 0)
|
||||
? ''
|
||||
: `<div class="form-hint" style="margin-bottom:6px;color:#b45309;">${escapeHtml(c2t('c2.listeners.malleableProfileEmptyListHint'))}</div>`;
|
||||
content.innerHTML = `
|
||||
<div class="c2-modal-header">
|
||||
<h3>${escapeHtml(c2t('c2.listeners.editTitle'))}</h3>
|
||||
<button class="c2-modal-close" onclick="C2.closeModal()">×</button>
|
||||
@@ -406,13 +584,19 @@
|
||||
<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}">
|
||||
<input type="text" id="c2-listener-host" class="form-control" value="${escapeHtml(String(l.bindHost))}">
|
||||
</div>
|
||||
<div class="c2-form-group">
|
||||
<label>${escapeHtml(c2t('c2.listeners.bindPort'))}</label>
|
||||
<input type="number" id="c2-listener-port" class="form-control" value="${l.bindPort}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="c2-form-group" id="c2-listener-profile-group">
|
||||
<label>${escapeHtml(c2t('c2.listeners.malleableProfile'))}</label>
|
||||
${httpHint}${emptyProfHint}
|
||||
<select id="c2-listener-profile-id" class="form-control c2-native-select">${profileOpts}</select>
|
||||
<div class="form-hint">${escapeHtml(c2t('c2.listeners.malleableProfileHint'))}</div>
|
||||
</div>
|
||||
<div class="c2-form-group">
|
||||
<label>${escapeHtml(c2t('c2.listeners.callbackHost'))}</label>
|
||||
<input type="text" id="c2-listener-callback-host" class="form-control" value="${escapeHtml(cbHost)}">
|
||||
@@ -428,7 +612,10 @@
|
||||
<button class="btn-primary" onclick="C2.saveListener('${l.id}')">${escapeHtml(c2t('common.save'))}</button>
|
||||
</div>
|
||||
`;
|
||||
modal.style.display = 'flex';
|
||||
}).catch(() => {
|
||||
showToast(c2t('c2.listeners.toastProfilesLoadFailed'), 'error');
|
||||
C2.closeModal();
|
||||
});
|
||||
};
|
||||
|
||||
C2.saveListener = function(id) {
|
||||
@@ -437,10 +624,13 @@
|
||||
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
|
||||
callback_host: callbackHost,
|
||||
profile_id: profileId
|
||||
}).then(data => {
|
||||
if (data.error) showToast(data.error, 'error');
|
||||
else {
|
||||
|
||||
Reference in New Issue
Block a user