@feng3d/ctc 0.0.11 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +12 -24
  2. package/admin-ui/dist/app.js +29 -0
  3. package/admin-ui/dist/index.html +222 -0
  4. package/admin-ui/dist/style.css +1 -0
  5. package/dist/admin-server.d.ts +65 -12
  6. package/dist/admin-server.d.ts.map +1 -1
  7. package/dist/admin-server.js +291 -549
  8. package/dist/admin-server.js.map +1 -1
  9. package/dist/cli.d.ts +12 -3
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +922 -311
  12. package/dist/cli.js.map +1 -1
  13. package/dist/config.d.ts +5 -1
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/config.js +13 -7
  16. package/dist/config.js.map +1 -1
  17. package/dist/controller.d.ts +0 -1
  18. package/dist/controller.d.ts.map +1 -1
  19. package/dist/controller.js +18 -9
  20. package/dist/controller.js.map +1 -1
  21. package/dist/data-channel.d.ts +5 -0
  22. package/dist/data-channel.d.ts.map +1 -1
  23. package/dist/data-channel.js +7 -2
  24. package/dist/data-channel.js.map +1 -1
  25. package/dist/forward-proxy.d.ts +100 -0
  26. package/dist/forward-proxy.d.ts.map +1 -0
  27. package/dist/forward-proxy.js +407 -0
  28. package/dist/forward-proxy.js.map +1 -0
  29. package/dist/handlers/unified-handler.d.ts +2 -0
  30. package/dist/handlers/unified-handler.d.ts.map +1 -1
  31. package/dist/handlers/unified-handler.js +31 -3
  32. package/dist/handlers/unified-handler.js.map +1 -1
  33. package/dist/index.d.ts +2 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +123 -37
  36. package/dist/index.js.map +1 -1
  37. package/dist/proxy-manager.d.ts +8 -1
  38. package/dist/proxy-manager.d.ts.map +1 -1
  39. package/dist/proxy-manager.js +51 -15
  40. package/dist/proxy-manager.js.map +1 -1
  41. package/package.json +7 -4
@@ -2,8 +2,12 @@
2
2
  * @module admin-server
3
3
  * @description 客户端管理页面 HTTP 服务器模块。
4
4
  * 提供一个本地 HTTP 服务,用于查看客户端状态和管理代理映射。
5
+ * 支持反向代理模式和正向穿透模式。
5
6
  */
6
7
  import { createServer } from 'http';
8
+ import { readFile } from 'fs';
9
+ import { join, dirname } from 'path';
10
+ import { fileURLToPath } from 'url';
7
11
  /**
8
12
  * 管理页面服务器类
9
13
  *
@@ -16,18 +20,43 @@ export class AdminServer {
16
20
  *
17
21
  * @param config - 服务器配置
18
22
  * @param getStatus - 获取状态的回调函数
19
- * @param addProxy - 添加代理的回调函数
20
- * @param removeProxy - 删除代理的回调函数
23
+ * @param addProxy - 添加反向代理的回调函数
24
+ * @param removeProxy - 删除反向代理的回调函数
25
+ * @param addForwardProxy - 添加正向穿透的回调函数
26
+ * @param removeForwardProxy - 删除正向穿透的回调函数
27
+ * @param registerClient - 注册客户端的回调函数
28
+ * @param getClientList - 获取客户端列表的回调函数
29
+ * @param reconnect - 触发重连的回调函数
21
30
  */
22
- constructor(config, getStatus, addProxy, removeProxy) {
31
+ constructor(config, getStatus, addProxy, removeProxy, addForwardProxy, removeForwardProxy, registerClient, getClientList, sendMessage, reconnect) {
32
+ /** 正向穿透代理列表(用于存储运行时的正向穿透配置) */
33
+ this.forwardProxies = new Map();
23
34
  this.port = config.port;
24
35
  this.host = config.host;
25
36
  this.startedAt = Date.now();
26
37
  this.getStatusCallback = getStatus;
27
38
  this.addProxyCallback = addProxy;
28
39
  this.removeProxyCallback = removeProxy;
40
+ this.addForwardProxyCallback = addForwardProxy;
41
+ this.removeForwardProxyCallback = removeForwardProxy;
42
+ this.registerClientCallback = registerClient;
43
+ this.getClientListCallback = getClientList;
44
+ this.sendMessageCallback = sendMessage;
45
+ this.reconnectCallback = reconnect;
29
46
  this.server = createServer((req, res) => this.handleRequest(req, res));
30
47
  }
48
+ /**
49
+ * 设置发送消息的回调
50
+ */
51
+ setSendMessageCallback(callback) {
52
+ this.sendMessageCallback = callback;
53
+ }
54
+ /**
55
+ * 设置重连回调
56
+ */
57
+ setReconnectCallback(callback) {
58
+ this.reconnectCallback = callback;
59
+ }
31
60
  /**
32
61
  * 启动服务器
33
62
  *
@@ -49,17 +78,56 @@ export class AdminServer {
49
78
  * 处理 HTTP 请求
50
79
  *
51
80
  * 提供以下端点:
52
- * - `GET /` - 管理页面
53
- * - `GET /_ctc/status` - 获取状态
54
- * - `POST /_ctc/proxies` - 添加代理
55
- * - `DELETE /_ctc/proxies/:port` - 删除代理
81
+ * 反向代理:
82
+ * - `GET /` - 管理页面
83
+ * - `GET /_ctc/status` - 获取状态
84
+ * - `POST /_ctc/proxies` - 添加反向代理
85
+ * - `DELETE /_ctc/proxies/:port` - 删除反向代理
86
+ * - `POST /_ctc/test-proxy?port=xxx` - 测试反向代理连接
87
+ * 正向穿透:
88
+ * - `GET /_ctc/forward/list` - 获取正向穿透列表
89
+ * - `POST /_ctc/forward/add` - 添加正向穿透
90
+ * - `POST /_ctc/forward/remove` - 删除正向穿透
91
+ * - `GET /_ctc/forward/clients` - 获取客户端列表
92
+ * - `POST /_ctc/forward/register` - 注册到服务器
56
93
  */
57
- handleRequest(req, res) {
94
+ async handleRequest(req, res) {
58
95
  const url = req.url ?? '/';
59
- // 管理页面
96
+ // 静态文件服务 - 首页读取模板文件
60
97
  if (url === '/' && req.method === 'GET') {
61
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
62
- res.end(AdminServer.STATUS_HTML);
98
+ readFile(AdminServer.TEMPLATE_PATH, 'utf-8', (err, data) => {
99
+ if (err) {
100
+ console.error('模板文件读取错误:', err);
101
+ res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
102
+ res.end('Error loading page');
103
+ }
104
+ else {
105
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
106
+ res.end(data);
107
+ }
108
+ });
109
+ return;
110
+ }
111
+ // 处理静态文件请求 (支持 .js, .css 等静态资源直接从根路径访问)
112
+ if (req.method === 'GET' && url !== '/' && !url.startsWith('/_ctc/')) {
113
+ const fileName = url.slice(1); // 去掉开头的 /
114
+ const filePath = join(AdminServer.STATIC_DIR, fileName);
115
+ readFile(filePath, 'utf-8', (err, data) => {
116
+ if (err || !data) {
117
+ console.error('静态文件读取错误:', err);
118
+ res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
119
+ res.end('File not found');
120
+ return;
121
+ }
122
+ const ext = fileName.split('.').pop() || 'html';
123
+ const contentType = ext === 'css' ? 'text/css; charset=utf-8' :
124
+ ext === 'js' ? 'text/javascript; charset=utf-8' : 'text/html; charset=utf-8';
125
+ res.writeHead(200, {
126
+ 'Content-Type': contentType,
127
+ 'Cache-Control': 'public, max-age=3600'
128
+ });
129
+ res.end(data);
130
+ });
63
131
  return;
64
132
  }
65
133
  // 状态 API
@@ -69,7 +137,8 @@ export class AdminServer {
69
137
  res.end(JSON.stringify(status));
70
138
  return;
71
139
  }
72
- // 添加代理 API
140
+ // ==================== 反向代理 API ====================
141
+ // 添加反向代理 API
73
142
  if (url === '/_ctc/proxies' && req.method === 'POST') {
74
143
  let body = '';
75
144
  req.on('data', (chunk) => { body += chunk; });
@@ -88,7 +157,7 @@ export class AdminServer {
88
157
  });
89
158
  return;
90
159
  }
91
- // 删除代理 API
160
+ // 删除反向代理 API
92
161
  if (url.startsWith('/_ctc/proxies/') && req.method === 'DELETE') {
93
162
  const port = parseInt(url.split('/').pop(), 10);
94
163
  if (isNaN(port)) {
@@ -108,6 +177,209 @@ export class AdminServer {
108
177
  });
109
178
  return;
110
179
  }
180
+ // 测试反向代理 API
181
+ if (url.startsWith('/_ctc/test-proxy') && req.method === 'POST') {
182
+ const urlObj = new URL(url, `http://${req.headers.host}`);
183
+ const portParam = urlObj.searchParams.get('port');
184
+ const port = portParam ? parseInt(portParam, 10) : NaN;
185
+ if (isNaN(port)) {
186
+ res.writeHead(400, { 'Content-Type': 'application/json' });
187
+ res.end(JSON.stringify({ error: '无效的端口号' }));
188
+ return;
189
+ }
190
+ // 通过 Controller 发送测试请求到服务端,然后测试连接
191
+ try {
192
+ // 简单测试:尝试连接本地端口来验证代理是否工作
193
+ // 实际测试需要通过服务端发起请求,这里只返回成功状态
194
+ const status = this.getStatusCallback();
195
+ const proxy = status.proxies.find((p) => p.remotePort === port);
196
+ if (!proxy) {
197
+ res.writeHead(404, { 'Content-Type': 'application/json' });
198
+ res.end(JSON.stringify({ success: false, error: '端口未注册' }));
199
+ return;
200
+ }
201
+ // 测试本地端口是否可连接
202
+ const net = await import('net');
203
+ const testClient = new net.Socket();
204
+ let connected = false;
205
+ let error = null;
206
+ await new Promise((resolve) => {
207
+ testClient.connect({ port: proxy.localPort, host: proxy.localHost || 'localhost' }, () => {
208
+ connected = true;
209
+ testClient.destroy();
210
+ resolve();
211
+ });
212
+ testClient.on('error', (err) => {
213
+ error = err.message;
214
+ testClient.destroy();
215
+ resolve();
216
+ });
217
+ // 2秒超时
218
+ setTimeout(() => {
219
+ if (!connected) {
220
+ error = '连接超时';
221
+ testClient.destroy();
222
+ resolve();
223
+ }
224
+ }, 2000);
225
+ });
226
+ if (connected) {
227
+ res.writeHead(200, { 'Content-Type': 'application/json' });
228
+ res.end(JSON.stringify({
229
+ success: true,
230
+ message: `本地端口 ${proxy.localPort} 连接成功`,
231
+ proxy: { remotePort: port, localPort: proxy.localPort }
232
+ }));
233
+ }
234
+ else {
235
+ res.writeHead(200, { 'Content-Type': 'application/json' });
236
+ res.end(JSON.stringify({
237
+ success: false,
238
+ error: `本地端口 ${proxy.localPort} 连接失败: ${error}`
239
+ }));
240
+ }
241
+ }
242
+ catch (e) {
243
+ const errorMessage = e instanceof Error ? e.message : String(e);
244
+ res.writeHead(500, { 'Content-Type': 'application/json' });
245
+ res.end(JSON.stringify({ success: false, error: errorMessage }));
246
+ }
247
+ return;
248
+ }
249
+ // ==================== 正向穿透 API ====================
250
+ // forward list - 列出正向穿透代理
251
+ if (url === '/_ctc/forward/list' && req.method === 'GET') {
252
+ const proxies = Array.from(this.forwardProxies.entries()).map(([key, value]) => ({
253
+ localPort: value.localPort,
254
+ targetClientId: value.targetClientId,
255
+ targetPort: value.targetPort,
256
+ enabled: true,
257
+ }));
258
+ res.writeHead(200, { 'Content-Type': 'application/json' });
259
+ res.end(JSON.stringify({ proxies }));
260
+ return;
261
+ }
262
+ // forward add - 添加正向穿透代理
263
+ if (url === '/_ctc/forward/add' && req.method === 'POST') {
264
+ let body = '';
265
+ req.on('data', (chunk) => { body += chunk; });
266
+ req.on('end', async () => {
267
+ try {
268
+ const data = JSON.parse(body);
269
+ const key = `${data.localPort}`;
270
+ if (this.addForwardProxyCallback) {
271
+ await this.addForwardProxyCallback({
272
+ ...data,
273
+ enabled: true,
274
+ });
275
+ }
276
+ this.forwardProxies.set(key, {
277
+ localPort: data.localPort,
278
+ targetClientId: data.targetClientId,
279
+ targetPort: data.targetPort,
280
+ });
281
+ res.writeHead(200, { 'Content-Type': 'application/json' });
282
+ res.end(JSON.stringify({ success: true }));
283
+ }
284
+ catch (error) {
285
+ const errorMessage = error instanceof Error ? error.message : String(error);
286
+ res.writeHead(400, { 'Content-Type': 'application/json' });
287
+ res.end(JSON.stringify({ error: errorMessage }));
288
+ }
289
+ });
290
+ return;
291
+ }
292
+ // forward remove - 移除正向穿透代理
293
+ if (url === '/_ctc/forward/remove' && req.method === 'POST') {
294
+ let body = '';
295
+ req.on('data', (chunk) => { body += chunk; });
296
+ req.on('end', async () => {
297
+ try {
298
+ const data = JSON.parse(body);
299
+ const key = `${data.localPort}`;
300
+ const deleted = this.forwardProxies.delete(key);
301
+ if (this.removeForwardProxyCallback) {
302
+ await this.removeForwardProxyCallback(data.localPort);
303
+ }
304
+ if (deleted) {
305
+ res.writeHead(200, { 'Content-Type': 'application/json' });
306
+ res.end(JSON.stringify({ success: true }));
307
+ }
308
+ else {
309
+ res.writeHead(404, { 'Content-Type': 'application/json' });
310
+ res.end(JSON.stringify({ error: '代理不存在' }));
311
+ }
312
+ }
313
+ catch (error) {
314
+ const errorMessage = error instanceof Error ? error.message : String(error);
315
+ res.writeHead(400, { 'Content-Type': 'application/json' });
316
+ res.end(JSON.stringify({ error: errorMessage }));
317
+ }
318
+ });
319
+ return;
320
+ }
321
+ // forward clients - 获取客户端列表
322
+ if (url === '/_ctc/forward/clients' && req.method === 'GET') {
323
+ if (!this.getClientListCallback) {
324
+ res.writeHead(503, { 'Content-Type': 'application/json' });
325
+ res.end(JSON.stringify({ error: '服务未就绪' }));
326
+ return;
327
+ }
328
+ try {
329
+ const result = await this.getClientListCallback();
330
+ res.writeHead(200, { 'Content-Type': 'application/json' });
331
+ res.end(JSON.stringify(result));
332
+ }
333
+ catch (error) {
334
+ const errorMessage = error instanceof Error ? error.message : String(error);
335
+ res.writeHead(500, { 'Content-Type': 'application/json' });
336
+ res.end(JSON.stringify({ error: errorMessage }));
337
+ }
338
+ return;
339
+ }
340
+ // forward register - 注册到服务器
341
+ if (url === '/_ctc/forward/register' && req.method === 'POST') {
342
+ let body = '';
343
+ req.on('data', (chunk) => { body += chunk; });
344
+ req.on('end', async () => {
345
+ try {
346
+ const data = JSON.parse(body);
347
+ if (!this.registerClientCallback) {
348
+ res.writeHead(503, { 'Content-Type': 'application/json' });
349
+ res.end(JSON.stringify({ error: '服务未就绪' }));
350
+ return;
351
+ }
352
+ const result = await this.registerClientCallback(data.description);
353
+ res.writeHead(200, { 'Content-Type': 'application/json' });
354
+ res.end(JSON.stringify(result));
355
+ }
356
+ catch (error) {
357
+ const errorMessage = error instanceof Error ? error.message : String(error);
358
+ res.writeHead(400, { 'Content-Type': 'application/json' });
359
+ res.end(JSON.stringify({ error: errorMessage }));
360
+ }
361
+ });
362
+ return;
363
+ }
364
+ // reconnect - 主动重连到服务器
365
+ if (url === '/_ctc/reconnect' && req.method === 'POST') {
366
+ if (!this.reconnectCallback) {
367
+ res.writeHead(503, { 'Content-Type': 'application/json' });
368
+ res.end(JSON.stringify({ error: '重连服务未就绪' }));
369
+ return;
370
+ }
371
+ try {
372
+ await this.reconnectCallback();
373
+ res.writeHead(200, { 'Content-Type': 'application/json' });
374
+ res.end(JSON.stringify({ success: true, message: '正在重连...' }));
375
+ }
376
+ catch (error) {
377
+ const errorMessage = error instanceof Error ? error.message : String(error);
378
+ res.writeHead(500, { 'Content-Type': 'application/json' });
379
+ res.end(JSON.stringify({ error: errorMessage }));
380
+ }
381
+ return;
382
+ }
111
383
  // 404
112
384
  res.writeHead(404);
113
385
  res.end('Not Found');
@@ -125,541 +397,11 @@ export class AdminServer {
125
397
  }
126
398
  }
127
399
  /**
128
- * 状态页面 HTML 模板
400
+ * 静态文件路径常量
129
401
  */
130
- AdminServer.STATUS_HTML = `<!DOCTYPE html>
131
- <html lang="zh-CN">
132
- <head>
133
- <meta charset="UTF-8">
134
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
135
- <title>穿透客户端管理</title>
136
- <style>
137
- * { margin: 0; padding: 0; box-sizing: border-box; }
138
- body {
139
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
140
- background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
141
- min-height: 100vh;
142
- color: #e0e0e0;
143
- padding: 20px;
144
- }
145
- .container {
146
- max-width: 900px;
147
- margin: 0 auto;
148
- }
149
- .header {
150
- text-align: center;
151
- margin-bottom: 30px;
152
- padding: 30px 20px;
153
- background: rgba(255,255,255,0.05);
154
- border-radius: 16px;
155
- backdrop-filter: blur(10px);
156
- border: 1px solid rgba(255,255,255,0.1);
157
- }
158
- .header h1 {
159
- font-size: 28px;
160
- margin-bottom: 8px;
161
- background: linear-gradient(90deg, #00d9ff, #00ff88);
162
- -webkit-background-clip: text;
163
- -webkit-text-fill-color: transparent;
164
- }
165
- .status {
166
- display: inline-flex;
167
- align-items: center;
168
- gap: 8px;
169
- padding: 6px 16px;
170
- border-radius: 20px;
171
- font-size: 14px;
172
- font-weight: 500;
173
- }
174
- .status.running {
175
- background: rgba(0, 255, 136, 0.15);
176
- color: #00ff88;
177
- }
178
- .status.running::before {
179
- content: "";
180
- width: 8px;
181
- height: 8px;
182
- border-radius: 50%;
183
- background: #00ff88;
184
- animation: pulse 1.5s infinite;
185
- }
186
- .status.stopped {
187
- background: rgba(255, 77, 77, 0.15);
188
- color: #ff4d4d;
189
- }
190
- @keyframes pulse {
191
- 0%, 100% { opacity: 1; }
192
- 50% { opacity: 0.4; }
193
- }
194
- .grid {
195
- display: grid;
196
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
197
- gap: 16px;
198
- margin-bottom: 20px;
199
- }
200
- .card {
201
- background: rgba(255,255,255,0.05);
202
- border-radius: 12px;
203
- padding: 20px;
204
- border: 1px solid rgba(255,255,255,0.1);
205
- backdrop-filter: blur(10px);
206
- }
207
- .card-label {
208
- font-size: 12px;
209
- color: #888;
210
- margin-bottom: 6px;
211
- text-transform: uppercase;
212
- letter-spacing: 0.5px;
213
- }
214
- .card-value {
215
- font-size: 20px;
216
- font-weight: 600;
217
- color: #fff;
218
- }
219
- .card-value .unit {
220
- font-size: 14px;
221
- color: #888;
222
- font-weight: 400;
223
- }
224
- .proxies-section {
225
- background: rgba(255,255,255,0.05);
226
- border-radius: 12px;
227
- padding: 20px;
228
- border: 1px solid rgba(255,255,255,0.1);
229
- margin-top: 20px;
230
- }
231
- .section-header {
232
- display: flex;
233
- justify-content: space-between;
234
- align-items: center;
235
- margin-bottom: 16px;
236
- }
237
- .section-title {
238
- font-size: 14px;
239
- color: #888;
240
- text-transform: uppercase;
241
- letter-spacing: 0.5px;
242
- }
243
- .btn {
244
- padding: 8px 16px;
245
- border-radius: 8px;
246
- border: none;
247
- font-size: 13px;
248
- cursor: pointer;
249
- transition: all 0.2s;
250
- }
251
- .btn-primary {
252
- background: linear-gradient(135deg, #00d9ff, #00ff88);
253
- color: #000;
254
- font-weight: 500;
255
- }
256
- .btn-primary:hover {
257
- opacity: 0.9;
258
- transform: translateY(-1px);
259
- }
260
- .btn-danger {
261
- background: rgba(255, 77, 77, 0.2);
262
- color: #ff4d4d;
263
- padding: 4px 10px;
264
- font-size: 12px;
265
- }
266
- .btn-danger:hover {
267
- background: rgba(255, 77, 77, 0.3);
268
- }
269
- .proxy-item {
270
- display: flex;
271
- justify-content: space-between;
272
- align-items: center;
273
- padding: 12px 16px;
274
- background: rgba(0,0,0,0.2);
275
- border-radius: 8px;
276
- margin-bottom: 8px;
277
- font-size: 14px;
278
- }
279
- .proxy-item:last-child {
280
- margin-bottom: 0;
281
- }
282
- .proxy-info {
283
- display: flex;
284
- align-items: center;
285
- gap: 12px;
286
- }
287
- .proxy-protocol {
288
- padding: 2px 8px;
289
- border-radius: 4px;
290
- font-size: 11px;
291
- font-weight: 500;
292
- text-transform: uppercase;
293
- background: rgba(0, 217, 255, 0.2);
294
- color: #00d9ff;
295
- }
296
- .proxy-remote {
297
- color: #00d9ff;
298
- font-family: monospace;
299
- }
300
- .proxy-arrow {
301
- color: #666;
302
- }
303
- .proxy-local {
304
- color: #888;
305
- font-family: monospace;
306
- }
307
- .empty-state {
308
- text-align: center;
309
- padding: 30px;
310
- color: #666;
311
- font-size: 14px;
312
- }
313
- .add-form {
314
- display: none;
315
- background: rgba(0,0,0,0.3);
316
- border-radius: 12px;
317
- padding: 20px;
318
- margin-bottom: 16px;
319
- }
320
- .add-form.show {
321
- display: block;
322
- }
323
- .form-row {
324
- display: grid;
325
- grid-template-columns: repeat(3, 1fr) auto;
326
- gap: 12px;
327
- align-items: end;
328
- }
329
- .form-group {
330
- display: flex;
331
- flex-direction: column;
332
- gap: 6px;
333
- }
334
- .form-group label {
335
- font-size: 11px;
336
- color: #888;
337
- text-transform: uppercase;
338
- }
339
- .form-group input, .form-group select {
340
- padding: 10px 14px;
341
- background: rgba(255,255,255,0.05);
342
- border: 1px solid rgba(255,255,255,0.1);
343
- border-radius: 8px;
344
- color: #fff;
345
- font-size: 14px;
346
- }
347
- .form-group input:focus, .form-group select:focus {
348
- outline: none;
349
- border-color: #00d9ff;
350
- }
351
- .form-actions {
352
- display: flex;
353
- gap: 8px;
354
- }
355
- .modal {
356
- display: none;
357
- position: fixed;
358
- top: 0;
359
- left: 0;
360
- right: 0;
361
- bottom: 0;
362
- background: rgba(0,0,0,0.7);
363
- align-items: center;
364
- justify-content: center;
365
- z-index: 1000;
366
- }
367
- .modal.show {
368
- display: flex;
369
- }
370
- .modal-content {
371
- background: #1a1a2e;
372
- border-radius: 16px;
373
- padding: 30px;
374
- max-width: 400px;
375
- width: 90%;
376
- border: 1px solid rgba(255,255,255,0.1);
377
- }
378
- .modal-title {
379
- font-size: 18px;
380
- margin-bottom: 20px;
381
- text-align: center;
382
- }
383
- .modal-actions {
384
- display: flex;
385
- gap: 12px;
386
- margin-top: 20px;
387
- }
388
- .modal-actions .btn {
389
- flex: 1;
390
- }
391
- .btn-secondary {
392
- background: rgba(255,255,255,0.1);
393
- color: #fff;
394
- }
395
- .btn-secondary:hover {
396
- background: rgba(255,255,255,0.15);
397
- }
398
- .last-update {
399
- text-align: center;
400
- color: #666;
401
- font-size: 12px;
402
- margin-top: 20px;
403
- }
404
- .footer {
405
- text-align: center;
406
- margin-top: 30px;
407
- padding: 20px;
408
- color: #666;
409
- font-size: 12px;
410
- }
411
- .footer a {
412
- color: #00d9ff;
413
- text-decoration: none;
414
- }
415
- .toast {
416
- position: fixed;
417
- bottom: 20px;
418
- right: 20px;
419
- padding: 12px 20px;
420
- border-radius: 8px;
421
- font-size: 14px;
422
- transform: translateY(100px);
423
- opacity: 0;
424
- transition: all 0.3s;
425
- }
426
- .toast.show {
427
- transform: translateY(0);
428
- opacity: 1;
429
- }
430
- .toast.success {
431
- background: rgba(0, 255, 136, 0.2);
432
- color: #00ff88;
433
- border: 1px solid rgba(0, 255, 136, 0.3);
434
- }
435
- .toast.error {
436
- background: rgba(255, 77, 77, 0.2);
437
- color: #ff4d4d;
438
- border: 1px solid rgba(255, 77, 77, 0.3);
439
- }
440
- </style>
441
- </head>
442
- <body>
443
- <div class="container">
444
- <div class="header">
445
- <h1>🔌 feng3d-ctc 穿透客户端</h1>
446
- <div class="status running" id="status">运行中</div>
447
- </div>
448
-
449
- <div class="grid">
450
- <div class="card">
451
- <div class="card-label">服务器</div>
452
- <div class="card-value" id="server">-</div>
453
- </div>
454
- <div class="card">
455
- <div class="card-label">连接状态</div>
456
- <div class="card-value" id="connection">-</div>
457
- </div>
458
- <div class="card">
459
- <div class="card-label">运行时长</div>
460
- <div class="card-value"><span id="uptime">-</span></div>
461
- </div>
462
- <div class="card">
463
- <div class="card-label">代理数量</div>
464
- <div class="card-value"><span id="proxyCount">0</span><span class="unit"> 个</span></div>
465
- </div>
466
- <div class="card">
467
- <div class="card-label">重连次数</div>
468
- <div class="card-value"><span id="reconnectCount">0</span><span class="unit"> 次</span></div>
469
- </div>
470
- </div>
471
-
472
- <div class="proxies-section">
473
- <div class="section-header">
474
- <div class="section-title">代理映射</div>
475
- <button class="btn btn-primary" id="showAddForm">+ 添加代理</button>
476
- </div>
477
-
478
- <div class="add-form" id="addForm">
479
- <div class="form-row">
480
- <div class="form-group">
481
- <label>远程端口</label>
482
- <input type="number" id="newRemotePort" placeholder="8080" min="1" max="65535">
483
- </div>
484
- <div class="form-group">
485
- <label>本地端口</label>
486
- <input type="number" id="newLocalPort" placeholder="3000" min="1" max="65535">
487
- </div>
488
- <div class="form-group">
489
- <label>本地地址</label>
490
- <input type="text" id="newLocalHost" placeholder="localhost">
491
- </div>
492
- <div class="form-actions">
493
- <button class="btn btn-primary" id="addProxy">添加</button>
494
- <button class="btn btn-secondary" id="cancelAdd">取消</button>
495
- </div>
496
- </div>
497
- </div>
498
-
499
- <div id="proxiesList"></div>
500
- </div>
501
-
502
- <div class="last-update">最后更新: <span id="lastUpdate">-</span></div>
503
-
504
- <div class="footer">
505
- <a href="https://github.com/feng3d/chuantou" target="_blank">feng3d-ctc</a>
506
- — 内网穿透客户端
507
- </div>
508
- </div>
509
-
510
- <div class="modal" id="deleteModal">
511
- <div class="modal-content">
512
- <div class="modal-title">确认删除代理</div>
513
- <p style="color: #888; text-align: center;">确定要删除此代理映射吗?</p>
514
- <div class="modal-actions">
515
- <button class="btn btn-secondary" id="cancelDelete">取消</button>
516
- <button class="btn btn-danger" id="confirmDelete">删除</button>
517
- </div>
518
- </div>
519
- </div>
520
-
521
- <div class="toast" id="toast"></div>
522
-
523
- <script>
524
- let deletePort = null;
525
-
526
- function formatUptime(ms) {
527
- const seconds = Math.floor(ms / 1000);
528
- const minutes = Math.floor(seconds / 60);
529
- const hours = Math.floor(minutes / 60);
530
- const days = Math.floor(hours / 24);
531
- if (days > 0) return \`\${days}天 \${hours % 24}小时\`;
532
- if (hours > 0) return \`\${hours}小时 \${minutes % 60}分钟\`;
533
- if (minutes > 0) return \`\${minutes}分钟 \${seconds % 60}秒\`;
534
- return \`\${seconds}秒\`;
535
- }
536
-
537
- function showToast(message, type = 'success') {
538
- const toast = document.getElementById('toast');
539
- toast.textContent = message;
540
- toast.className = \`toast \${type} show\`;
541
- setTimeout(() => toast.classList.remove('show'), 3000);
542
- }
543
-
544
- async function updateStatus() {
545
- try {
546
- const res = await fetch('/_ctc/status');
547
- const data = await res.json();
548
-
549
- const statusEl = document.getElementById('status');
550
- if (data.running) {
551
- statusEl.textContent = data.authenticated ? '已连接' : (data.connected ? '认证中...' : '连接中...');
552
- statusEl.className = 'status running';
553
- } else {
554
- statusEl.textContent = '已停止';
555
- statusEl.className = 'status stopped';
556
- }
557
-
558
- document.getElementById('server').textContent = data.serverUrl.replace('ws://', '').replace('wss://', '');
559
- document.getElementById('connection').textContent = data.authenticated ? '已认证' : (data.connected ? '已连接' : '未连接');
560
- document.getElementById('uptime').textContent = formatUptime(data.uptime);
561
- document.getElementById('proxyCount').textContent = data.proxies.length;
562
- document.getElementById('reconnectCount').textContent = data.reconnectAttempts;
563
- document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString('zh-CN');
564
-
565
- // 更新代理列表
566
- const listEl = document.getElementById('proxiesList');
567
- if (data.proxies.length === 0) {
568
- listEl.innerHTML = '<div class="empty-state">暂无代理映射,点击上方按钮添加</div>';
569
- } else {
570
- listEl.innerHTML = data.proxies.map(p => {
571
- return \`
572
- <div class="proxy-item">
573
- <div class="proxy-info">
574
- <span class="proxy-protocol">ALL</span>
575
- <span class="proxy-remote">:\${p.remotePort}</span>
576
- <span class="proxy-arrow">→</span>
577
- <span class="proxy-local">\${p.localHost || 'localhost'}:\${p.localPort}</span>
578
- </div>
579
- <button class="btn btn-danger" onclick="showDeleteModal(\${p.remotePort})">删除</button>
580
- </div>
581
- \`;
582
- }).join('');
583
- }
584
- } catch (e) {
585
- console.error('获取状态失败:', e);
586
- }
587
- }
588
-
589
- function showDeleteModal(port) {
590
- deletePort = port;
591
- document.getElementById('deleteModal').classList.add('show');
592
- }
593
-
594
- document.getElementById('cancelDelete').addEventListener('click', () => {
595
- document.getElementById('deleteModal').classList.remove('show');
596
- deletePort = null;
597
- });
598
-
599
- document.getElementById('confirmDelete').addEventListener('click', async () => {
600
- if (deletePort) {
601
- try {
602
- const res = await fetch(\`/_ctc/proxies/\${deletePort}\`, { method: 'DELETE' });
603
- if (res.ok) {
604
- showToast('代理已删除');
605
- updateStatus();
606
- } else {
607
- const data = await res.json();
608
- showToast(\`删除失败: \${data.error}\`, 'error');
609
- }
610
- } catch (e) {
611
- showToast('删除失败: 网络错误', 'error');
612
- }
613
- }
614
- document.getElementById('deleteModal').classList.remove('show');
615
- deletePort = null;
616
- });
617
-
618
- document.getElementById('showAddForm').addEventListener('click', () => {
619
- document.getElementById('addForm').classList.add('show');
620
- });
621
-
622
- document.getElementById('cancelAdd').addEventListener('click', () => {
623
- document.getElementById('addForm').classList.remove('show');
624
- });
625
-
626
- document.getElementById('addProxy').addEventListener('click', async () => {
627
- const remotePort = parseInt(document.getElementById('newRemotePort').value);
628
- const localPort = parseInt(document.getElementById('newLocalPort').value);
629
- const localHost = document.getElementById('newLocalHost').value || 'localhost';
630
-
631
- if (!remotePort || !localPort) {
632
- showToast('请填写完整信息', 'error');
633
- return;
634
- }
635
-
636
- try {
637
- const res = await fetch('/_ctc/proxies', {
638
- method: 'POST',
639
- headers: { 'Content-Type': 'application/json' },
640
- body: JSON.stringify({ remotePort, localPort, localHost })
641
- });
642
-
643
- if (res.ok) {
644
- showToast('代理已添加');
645
- document.getElementById('addForm').classList.remove('show');
646
- document.getElementById('newRemotePort').value = '';
647
- document.getElementById('newLocalPort').value = '';
648
- document.getElementById('newLocalHost').value = 'localhost';
649
- updateStatus();
650
- } else {
651
- const data = await res.json();
652
- showToast(\`添加失败: \${data.error}\`, 'error');
653
- }
654
- } catch (e) {
655
- showToast('添加失败: 网络错误', 'error');
656
- }
657
- });
658
-
659
- updateStatus();
660
- setInterval(updateStatus, 3000);
661
- </script>
662
- </body>
663
- </html>
664
- `;
402
+ AdminServer.STATIC_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'admin-ui', 'dist');
403
+ /**
404
+ * HTML 模板文件路径
405
+ */
406
+ AdminServer.TEMPLATE_PATH = join(AdminServer.STATIC_DIR, 'index.html');
665
407
  //# sourceMappingURL=admin-server.js.map