Add files via upload

This commit is contained in:
公明
2026-05-04 04:50:53 +08:00
committed by GitHub
parent d603060511
commit 9b4c6dedc8
5 changed files with 460 additions and 81 deletions
+1 -1
View File
@@ -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
)
+213 -48
View File
@@ -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
+12
View File
@@ -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",
+12
View File
@@ -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
View File
@@ -151,6 +151,74 @@
return div.innerHTML;
}
/** 监听器表单:Malleable Profile 下拉选项 HTMLvalue / 文本已转义) */
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 插值把日期里的「/」转成 &#x2F;,与 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()">&times;</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()">&times;</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()">&times;</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()">&times;</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 {