From 9b4c6dedc81d4469d1bbf2408e8a67c1c110ae00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Mon, 4 May 2026 04:50:53 +0800 Subject: [PATCH] Add files via upload --- go.mod | 2 +- web/static/css/c2.css | 261 ++++++++++++++++++++++++++++++------- web/static/i18n/en-US.json | 12 ++ web/static/i18n/zh-CN.json | 12 ++ web/static/js/c2.js | 254 +++++++++++++++++++++++++++++++----- 5 files changed, 460 insertions(+), 81 deletions(-) diff --git a/go.mod b/go.mod index b9c6c049..ba95d3c2 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( github.com/pkoukk/tiktoken-go v0.1.8 github.com/robfig/cron/v3 v3.0.1 go.uber.org/zap v1.26.0 + golang.org/x/text v0.26.0 golang.org/x/time v0.14.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -77,7 +78,6 @@ require ( golang.org/x/net v0.24.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.26.0 // indirect google.golang.org/protobuf v1.30.0 // indirect ) diff --git a/web/static/css/c2.css b/web/static/css/c2.css index 96edb0dc..f7674d3f 100644 --- a/web/static/css/c2.css +++ b/web/static/css/c2.css @@ -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 diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index e86c926a..bf52aa7e 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -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", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index b099f3fa..f060c313 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -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", diff --git a/web/static/js/c2.js b/web/static/js/c2.js index 6b8e82c4..7d2a7bf9 100644 --- a/web/static/js/c2.js +++ b/web/static/js/c2.js @@ -151,6 +151,74 @@ return div.innerHTML; } + /** 监听器表单: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')); @@ -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 => ` -
-
-
-
${escapeHtml(l.name)}
-
${l.id.substring(0, 12)}...
+ 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(listenerTypeLabel(l.type))}
-
-
- - ${l.bindHost}:${l.bindPort} +
+
+ ${escapeHtml(c2t('c2.listeners.bindEndpoint'))} + ${bindVal}
- ${l.startedAt ? `
${escapeHtml(c2t('c2.listeners.startedAt', { time: formatTime(l.startedAt) }))}
` : ''} - ${l.remark ? `
${escapeHtml(l.remark)}
` : ''} + ${cbRow} + ${profileBadge} + ${remarkRow} + ${startedHtml}
-
- ${l.status === 'stopped' - ? `` - : `` +
+ ${l.status === 'stopped' + ? `` + : `` } - - + +
-
- `).join(''); +
`; + }).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 = ` +
+

${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'))}

@@ -291,7 +422,7 @@
- @@ -310,6 +441,12 @@
+
+ + ${emptyProfHintCreate} + +
${escapeHtml(c2t('c2.listeners.malleableProfileHint'))}
+
@@ -325,7 +462,25 @@
`; - 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 = ` +
+

${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'))}

@@ -406,13 +584,19 @@
- +
+
+ + ${httpHint}${emptyProfHint} + +
${escapeHtml(c2t('c2.listeners.malleableProfileHint'))}
+
@@ -428,7 +612,10 @@
`; - 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 {