cacode commited on
Commit
4e9a80d
·
verified ·
1 Parent(s): 87b841b

Upload 7 files

Browse files
Dockerfile ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ # 安装nmap和其他必要的包
4
+ RUN apt-get update && apt-get install -y \
5
+ nmap \
6
+ procps \
7
+ && rm -rf /var/lib/apt/lists/*
8
+
9
+ # 创建工作目录
10
+ WORKDIR /app
11
+
12
+ # 复制依赖文件并安装
13
+ COPY requirements.txt .
14
+ RUN pip install --no-cache-dir -r requirements.txt
15
+
16
+ # 复制应用代码
17
+ COPY app/ ./app/
18
+
19
+ # 设置环境变量
20
+ ENV PYTHONPATH=/app
21
+ ENV PYTHONUNBUFFERED=1
22
+ ENV PYTHONDONTWRITEBYTECODE=1
23
+ ENV GEVENT_SUPPORT=True
24
+
25
+ # 暴露端口
26
+ EXPOSE 5000
27
+
28
+ # 启动命令,使用gevent-websocket作为WSGI服务器,支持WebSocket和多线程
29
+ CMD ["gunicorn", "--worker-class", "geventwebsocket.gunicorn.workers.GeventWebSocketWorker", "--workers", "2", "--threads", "4", "--bind", "0.0.0.0:5000", "--timeout", "120", "app.app:app"]
app/app.py ADDED
@@ -0,0 +1,599 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request, jsonify
2
+ import subprocess
3
+ import json
4
+ import re
5
+ import os
6
+ import time
7
+ import threading
8
+ import uuid
9
+ import ipaddress
10
+ import math
11
+ import queue
12
+ from werkzeug.middleware.proxy_fix import ProxyFix
13
+ from flask_socketio import SocketIO, emit
14
+
15
+ app = Flask(__name__)
16
+ app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1)
17
+ app.config['SECRET_KEY'] = os.urandom(24)
18
+ socketio = SocketIO(app,
19
+ cors_allowed_origins="*",
20
+ async_mode='gevent',
21
+ logger=True,
22
+ engineio_logger=True,
23
+ ping_timeout=60,
24
+ ping_interval=25,
25
+ always_connect=True)
26
+
27
+ # 存储活动扫描任务
28
+ active_scans = {}
29
+
30
+ # 默认并行任务数
31
+ DEFAULT_PARALLEL_TASKS = 8
32
+ MIN_PARALLEL_TASKS = 4
33
+ MAX_PARALLEL_TASKS = 16
34
+
35
+ # 安全考虑:限制允许扫描的端口范围和常见选项
36
+ ALLOWED_OPTIONS = {
37
+ # 扫描类型
38
+ "-sS": "SYN扫描",
39
+ "-sT": "TCP连接扫描",
40
+ "-sU": "UDP扫描",
41
+ "-sV": "服务版本检测",
42
+ "-O": "操作系统检测",
43
+ "-A": "综合扫描",
44
+
45
+ # 扫描速度
46
+ "-T0": "偷偷摸摸扫描",
47
+ "-T1": "鬼鬼祟祟扫描",
48
+ "-T2": "礼貌扫描",
49
+ "-T3": "普通扫描",
50
+ "-T4": "激进扫描"
51
+ }
52
+
53
+ @app.route('/')
54
+ def index():
55
+ return render_template('index.html')
56
+
57
+ def is_valid_target(target):
58
+ pattern = r'^[a-zA-Z0-9][a-zA-Z0-9\.\-\/]+$'
59
+ return bool(re.match(pattern, target))
60
+
61
+ def is_valid_ports(ports):
62
+ if not ports:
63
+ return True
64
+
65
+ if ports == "all" or ports == "-":
66
+ return True
67
+
68
+ pattern = r'^(?:\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*)?$'
69
+ return bool(re.match(pattern, ports))
70
+
71
+ def split_ip_range(target, num_chunks):
72
+ try:
73
+ if '/' in target:
74
+ network = ipaddress.ip_network(target, strict=False)
75
+ total_ips = network.num_addresses
76
+ if total_ips < num_chunks:
77
+ num_chunks = max(1, total_ips)
78
+ ips_per_chunk = math.ceil(total_ips / num_chunks)
79
+
80
+ chunks = []
81
+ start_ip = int(network.network_address)
82
+ for i in range(num_chunks):
83
+ chunk_start = start_ip + i * ips_per_chunk
84
+ chunk_end = min(start_ip + (i + 1) * ips_per_chunk - 1, int(network.broadcast_address))
85
+
86
+ if chunk_start <= chunk_end:
87
+ start_ip_obj = ipaddress.ip_address(chunk_start)
88
+ end_ip_obj = ipaddress.ip_address(chunk_end)
89
+
90
+ if chunk_start == chunk_end:
91
+ chunks.append(str(start_ip_obj))
92
+ else:
93
+ chunks.append(f"{start_ip_obj}-{end_ip_obj}")
94
+
95
+ return chunks
96
+
97
+ elif '-' in target:
98
+ start_ip, end_ip = target.split('-')
99
+ start_ip = ipaddress.ip_address(start_ip.strip())
100
+ end_ip = ipaddress.ip_address(end_ip.strip())
101
+
102
+ total_ips = int(end_ip) - int(start_ip) + 1
103
+ if total_ips < num_chunks:
104
+ num_chunks = max(1, total_ips)
105
+
106
+ ips_per_chunk = math.ceil(total_ips / num_chunks)
107
+
108
+ chunks = []
109
+ for i in range(num_chunks):
110
+ chunk_start = int(start_ip) + i * ips_per_chunk
111
+ chunk_end = min(int(start_ip) + (i + 1) * ips_per_chunk - 1, int(end_ip))
112
+
113
+ if chunk_start <= chunk_end:
114
+ start_ip_obj = ipaddress.ip_address(chunk_start)
115
+ end_ip_obj = ipaddress.ip_address(chunk_end)
116
+
117
+ if chunk_start == chunk_end:
118
+ chunks.append(str(start_ip_obj))
119
+ else:
120
+ chunks.append(f"{start_ip_obj}-{end_ip_obj}")
121
+
122
+ return chunks
123
+ except:
124
+ pass
125
+
126
+ return [target]
127
+
128
+ def split_port_range(ports, num_chunks):
129
+ if not ports or ports == "all" or ports == "-":
130
+ total_ports = 65535
131
+ ports_per_chunk = math.ceil(total_ports / num_chunks)
132
+
133
+ chunks = []
134
+ for i in range(num_chunks):
135
+ start_port = i * ports_per_chunk + 1
136
+ end_port = min((i + 1) * ports_per_chunk, 65535)
137
+ chunks.append(f"{start_port}-{end_port}")
138
+
139
+ return chunks
140
+
141
+ port_list = []
142
+ for port_range in ports.split(','):
143
+ if '-' in port_range:
144
+ start, end = map(int, port_range.split('-'))
145
+ port_list.extend(range(start, end + 1))
146
+ else:
147
+ port_list.append(int(port_range))
148
+
149
+ if len(port_list) < num_chunks:
150
+ num_chunks = max(1, len(port_list))
151
+
152
+ chunks = []
153
+ ports_per_chunk = math.ceil(len(port_list) / num_chunks)
154
+
155
+ for i in range(num_chunks):
156
+ start_idx = i * ports_per_chunk
157
+ end_idx = min((i + 1) * ports_per_chunk, len(port_list))
158
+
159
+ if start_idx < len(port_list):
160
+ chunk_ports = sorted(port_list[start_idx:end_idx])
161
+ if not chunk_ports:
162
+ continue
163
+
164
+ ranges = []
165
+ range_start = chunk_ports[0]
166
+ prev = chunk_ports[0]
167
+
168
+ for port in chunk_ports[1:]:
169
+ if port == prev + 1:
170
+ prev = port
171
+ continue
172
+
173
+ if range_start == prev:
174
+ ranges.append(str(range_start))
175
+ else:
176
+ ranges.append(f"{range_start}-{prev}")
177
+
178
+ range_start = port
179
+ prev = port
180
+
181
+ if range_start == prev:
182
+ ranges.append(str(range_start))
183
+ else:
184
+ ranges.append(f"{range_start}-{prev}")
185
+
186
+ chunks.append(','.join(ranges))
187
+
188
+ return chunks
189
+
190
+ def execute_nmap_scan(scan_id, target, ports, options, task_id, result_queue):
191
+ try:
192
+ command = ["nmap"] + options
193
+
194
+ if ports:
195
+ command.extend(["-p", ports])
196
+
197
+ command.append(target)
198
+ command_str = " ".join(command)
199
+
200
+ socketio.emit('scan_update', {
201
+ 'scan_id': scan_id,
202
+ 'task_id': task_id,
203
+ 'status': 'task_running',
204
+ 'message': f'子任务 {task_id}: 扫描 {target} 端口 {ports if ports else "默认"}...',
205
+ 'command': command_str
206
+ }, room=scan_id)
207
+
208
+ process = subprocess.Popen(
209
+ command,
210
+ stdout=subprocess.PIPE,
211
+ stderr=subprocess.PIPE,
212
+ text=True,
213
+ bufsize=1
214
+ )
215
+
216
+ results = []
217
+ errors = []
218
+
219
+ for line in iter(process.stdout.readline, ''):
220
+ results.append(line)
221
+ if len(results) % 10 == 0:
222
+ socketio.emit('scan_update', {
223
+ 'scan_id': scan_id,
224
+ 'task_id': task_id,
225
+ 'status': 'task_progress',
226
+ 'partial_result': line
227
+ }, room=scan_id)
228
+
229
+ for line in iter(process.stderr.readline, ''):
230
+ errors.append(line)
231
+
232
+ process.wait()
233
+
234
+ result_data = {
235
+ 'task_id': task_id,
236
+ 'target': target,
237
+ 'ports': ports,
238
+ 'command': command_str,
239
+ 'success': process.returncode == 0,
240
+ 'result': ''.join(results),
241
+ 'error': ''.join(errors)
242
+ }
243
+
244
+ result_queue.put(result_data)
245
+
246
+ socketio.emit('scan_update', {
247
+ 'scan_id': scan_id,
248
+ 'task_id': task_id,
249
+ 'status': 'task_completed',
250
+ 'message': f'子任务 {task_id} 已完成'
251
+ }, room=scan_id)
252
+
253
+ except Exception as e:
254
+ error_msg = str(e)
255
+ socketio.emit('scan_update', {
256
+ 'scan_id': scan_id,
257
+ 'task_id': task_id,
258
+ 'status': 'task_error',
259
+ 'message': f'子任务 {task_id} 出错: {error_msg}'
260
+ }, room=scan_id)
261
+
262
+ result_queue.put({
263
+ 'task_id': task_id,
264
+ 'target': target,
265
+ 'ports': ports,
266
+ 'success': False,
267
+ 'error': error_msg
268
+ })
269
+
270
+ def run_nmap_scan(scan_id, target, ports, selected_options, scan_all_ports, parallel_tasks):
271
+ try:
272
+ num_threads = min(parallel_tasks, MAX_PARALLEL_TASKS)
273
+
274
+ options = list(selected_options)
275
+
276
+ if scan_all_ports:
277
+ ports = "-"
278
+
279
+ socketio.emit('scan_update', {
280
+ 'scan_id': scan_id,
281
+ 'status': 'starting',
282
+ 'message': f'正在使用 {num_threads} 个线程开始扫描...',
283
+ 'threads': num_threads
284
+ }, room=scan_id)
285
+
286
+ targets = split_ip_range(target, num_threads)
287
+
288
+ if len(targets) == 1:
289
+ port_chunks = split_port_range(ports, num_threads)
290
+ tasks = [(targets[0], port) for port in port_chunks]
291
+ else:
292
+ tasks = [(t, ports) for t in targets]
293
+
294
+ result_queue = queue.Queue()
295
+
296
+ threads = []
297
+
298
+ active_scans[scan_id]['total_tasks'] = len(tasks)
299
+ active_scans[scan_id]['completed_tasks'] = 0
300
+ active_scans[scan_id]['tasks'] = {}
301
+
302
+ task_info = []
303
+ for i, (task_target, task_ports) in enumerate(tasks):
304
+ task_id = f"task_{i+1}"
305
+ task_info.append({
306
+ 'task_id': task_id,
307
+ 'target': task_target,
308
+ 'ports': task_ports if task_ports else '默认端口'
309
+ })
310
+
311
+ active_scans[scan_id]['tasks'][task_id] = {
312
+ 'status': 'pending',
313
+ 'target': task_target,
314
+ 'ports': task_ports
315
+ }
316
+
317
+ socketio.emit('scan_update', {
318
+ 'scan_id': scan_id,
319
+ 'status': 'tasks_created',
320
+ 'message': f'已创建 {len(tasks)} 个扫描子任务',
321
+ 'tasks': task_info
322
+ }, room=scan_id)
323
+
324
+ for i, (task_target, task_ports) in enumerate(tasks):
325
+ task_id = f"task_{i+1}"
326
+ thread = threading.Thread(
327
+ target=execute_nmap_scan,
328
+ args=(scan_id, task_target, task_ports, options, task_id, result_queue)
329
+ )
330
+ thread.daemon = True
331
+ threads.append(thread)
332
+
333
+ for thread in threads:
334
+ thread.start()
335
+ time.sleep(0.2)
336
+
337
+ for thread in threads:
338
+ thread.join()
339
+
340
+ all_results = []
341
+ all_errors = []
342
+
343
+ while not result_queue.empty():
344
+ result = result_queue.get()
345
+
346
+ if result.get('success', False):
347
+ all_results.append({
348
+ 'target': result.get('target', ''),
349
+ 'ports': result.get('ports', ''),
350
+ 'result': result.get('result', '')
351
+ })
352
+ else:
353
+ all_errors.append(result.get('error', ''))
354
+
355
+ if all_errors:
356
+ socketio.emit('scan_update', {
357
+ 'scan_id': scan_id,
358
+ 'status': 'error',
359
+ 'message': '扫描过程中出现错误',
360
+ 'error': '\n'.join(all_errors)
361
+ }, room=scan_id)
362
+ else:
363
+ combined_result = merge_scan_results(all_results)
364
+
365
+ socketio.emit('scan_update', {
366
+ 'scan_id': scan_id,
367
+ 'status': 'completed',
368
+ 'message': '扫描完成',
369
+ 'result': combined_result
370
+ }, room=scan_id)
371
+
372
+ except Exception as e:
373
+ socketio.emit('scan_update', {
374
+ 'scan_id': scan_id,
375
+ 'status': 'error',
376
+ 'message': f'扫描过程中出错: {str(e)}'
377
+ }, room=scan_id)
378
+
379
+ if scan_id in active_scans:
380
+ del active_scans[scan_id]
381
+
382
+ def merge_scan_results(results):
383
+ if not results:
384
+ return ""
385
+
386
+ if len(results) == 1:
387
+ return results[0]['result']
388
+
389
+ open_ports = {}
390
+ scan_headers = {}
391
+ scan_footers = {}
392
+
393
+ header_pattern = re.compile(r'Starting Nmap.*?(?=PORT)', re.DOTALL)
394
+ ports_pattern = re.compile(r'PORT\s+STATE\s+SERVICE.*?(?=\n\n|\nNmap done:)', re.DOTALL)
395
+ footer_pattern = re.compile(r'Nmap done:.*$', re.DOTALL)
396
+ port_line_pattern = re.compile(r'^(\d+/\w+)\s+(\w+)\s+(.*)$', re.MULTILINE)
397
+
398
+ for result_data in results:
399
+ result_text = result_data['result']
400
+ target = result_data['target']
401
+
402
+ header_match = header_pattern.search(result_text)
403
+ if header_match:
404
+ header = header_match.group(0).strip()
405
+ scan_headers[target] = header
406
+
407
+ ports_match = ports_pattern.search(result_text)
408
+ if ports_match:
409
+ ports_section = ports_match.group(0)
410
+ port_lines = ports_section.split('\n')[1:]
411
+
412
+ for line in port_lines:
413
+ port_match = port_line_pattern.match(line.strip())
414
+ if port_match:
415
+ port, state, service = port_match.groups()
416
+ if target not in open_ports:
417
+ open_ports[target] = {}
418
+ open_ports[target][port] = {'state': state, 'service': service}
419
+
420
+ footer_match = footer_pattern.search(result_text)
421
+ if footer_match:
422
+ footer = footer_match.group(0).strip()
423
+ scan_footers[target] = footer
424
+
425
+ combined_output = []
426
+
427
+ if scan_headers:
428
+ main_header = next(iter(scan_headers.values()))
429
+ combined_output.append(main_header)
430
+ combined_output.append("\nPORT\tSTATE\tSERVICE")
431
+
432
+ for target, ports in open_ports.items():
433
+ if len(open_ports) > 1:
434
+ combined_output.append(f"\n目标: {target}")
435
+
436
+ for port, info in sorted(ports.items(), key=lambda x: int(x[0].split('/')[0])):
437
+ combined_output.append(f"{port}\t{info['state']}\t{info['service']}")
438
+
439
+ if scan_footers:
440
+ total_time = 0
441
+ total_hosts = 0
442
+ for footer in scan_footers.values():
443
+ time_match = re.search(r'scanned in ([\d\.]+) seconds', footer)
444
+ if time_match:
445
+ total_time += float(time_match.group(1))
446
+
447
+ hosts_match = re.search(r'(\d+) IP address', footer)
448
+ if hosts_match:
449
+ total_hosts += int(hosts_match.group(1))
450
+
451
+ combined_output.append(f"\nNmap 多线程扫描完成: 总共扫描 {total_hosts} 个IP地址,用时 {total_time:.2f} 秒")
452
+
453
+ return "\n".join(combined_output)
454
+
455
+ @socketio.on('connect')
456
+ def handle_connect():
457
+ print(f"客户端已连接: {request.sid}, 传输方式: {request.environ.get('wsgi.websocket') and 'WebSocket' or '轮询'}")
458
+ emit('connection_response', {
459
+ 'status': 'connected',
460
+ 'transport': request.environ.get('wsgi.websocket') and 'websocket' or 'polling',
461
+ 'sid': request.sid
462
+ })
463
+
464
+ @socketio.on('join_scan')
465
+ def handle_join(data):
466
+ scan_id = data['scan_id']
467
+ if scan_id:
468
+ print(f"Client joined scan room: {scan_id}")
469
+ socketio.server.enter_room(request.sid, scan_id)
470
+
471
+ @socketio.on('start_scan')
472
+ def handle_scan_request(data):
473
+ if not data:
474
+ emit('scan_update', {'status': 'error', 'message': '没有提供数据'})
475
+ return
476
+
477
+ target = data.get('target', '')
478
+ ports = data.get('ports', '')
479
+ selected_options = data.get('options', [])
480
+ scan_all_ports = data.get('scan_all_ports', False)
481
+ parallel_tasks = data.get('parallel_tasks', DEFAULT_PARALLEL_TASKS)
482
+
483
+ try:
484
+ parallel_tasks = int(parallel_tasks)
485
+ parallel_tasks = max(MIN_PARALLEL_TASKS, min(parallel_tasks, MAX_PARALLEL_TASKS))
486
+ except:
487
+ parallel_tasks = DEFAULT_PARALLEL_TASKS
488
+
489
+ if not target or not is_valid_target(target):
490
+ emit('scan_update', {'status': 'error', 'message': '无效的目标格式'})
491
+ return
492
+
493
+ if not is_valid_ports(ports):
494
+ emit('scan_update', {'status': 'error', 'message': '无效的端口格式'})
495
+ return
496
+
497
+ valid_options = []
498
+ for option in selected_options:
499
+ if option in ALLOWED_OPTIONS:
500
+ valid_options.append(option)
501
+ else:
502
+ emit('scan_update', {'status': 'error', 'message': f'不允许的选项: {option}'})
503
+ return
504
+
505
+ scan_id = str(uuid.uuid4())
506
+
507
+ socketio.server.enter_room(request.sid, scan_id)
508
+
509
+ scan_thread = threading.Thread(
510
+ target=run_nmap_scan,
511
+ args=(scan_id, target, ports, valid_options, scan_all_ports, parallel_tasks)
512
+ )
513
+ scan_thread.daemon = True
514
+
515
+ active_scans[scan_id] = {
516
+ 'thread': scan_thread,
517
+ 'start_time': time.time(),
518
+ 'target': target,
519
+ 'parallel_tasks': parallel_tasks
520
+ }
521
+
522
+ scan_thread.start()
523
+
524
+ emit('scan_started', {
525
+ 'scan_id': scan_id,
526
+ 'parallel_tasks': parallel_tasks
527
+ })
528
+
529
+ @socketio.on('cancel_scan')
530
+ def handle_cancel_scan(data):
531
+ scan_id = data.get('scan_id')
532
+ if scan_id in active_scans:
533
+ active_scans[scan_id]['cancelled'] = True
534
+ emit('scan_update', {
535
+ 'scan_id': scan_id,
536
+ 'status': 'cancelled',
537
+ 'message': '扫描已取消'
538
+ }, room=scan_id)
539
+
540
+ @app.route('/scan', methods=['POST'])
541
+ def scan():
542
+ data = request.get_json()
543
+
544
+ if not data:
545
+ return jsonify({"error": "没有提供数据"}), 400
546
+
547
+ target = data.get('target', '')
548
+ ports = data.get('ports', '')
549
+ selected_options = data.get('options', [])
550
+ scan_all_ports = data.get('scan_all_ports', False)
551
+
552
+ if not target or not is_valid_target(target):
553
+ return jsonify({"error": "无效的目标格式"}), 400
554
+
555
+ if not is_valid_ports(ports):
556
+ return jsonify({"error": "无效的端口格式"}), 400
557
+
558
+ for option in selected_options:
559
+ if option not in ALLOWED_OPTIONS:
560
+ return jsonify({"error": f"不允许的选项: {option}"}), 400
561
+
562
+ command = ["nmap"]
563
+
564
+ for option in selected_options:
565
+ command.append(option)
566
+
567
+ if scan_all_ports:
568
+ command.extend(["-p-"])
569
+ elif ports:
570
+ command.extend(["-p", ports])
571
+
572
+ command.append(target)
573
+
574
+ try:
575
+ process = subprocess.run(
576
+ command,
577
+ capture_output=True,
578
+ text=True,
579
+ timeout=600
580
+ )
581
+
582
+ if process.returncode != 0:
583
+ return jsonify({
584
+ "error": "扫描失败",
585
+ "message": process.stderr
586
+ }), 500
587
+
588
+ return jsonify({
589
+ "result": process.stdout,
590
+ "command": " ".join(command)
591
+ })
592
+
593
+ except subprocess.TimeoutExpired:
594
+ return jsonify({"error": "扫描超时"}), 408
595
+ except Exception as e:
596
+ return jsonify({"error": f"扫描过程中出错: {str(e)}"}), 500
597
+
598
+ if __name__ == '__main__':
599
+ socketio.run(app, host='0.0.0.0', port=5000, debug=False, allow_unsafe_werkzeug=True)
app/static/css/style.css ADDED
@@ -0,0 +1,1294 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --primary-color: #2563eb;
3
+ --primary-dark: #1d4ed8;
4
+ --secondary-color: #3b82f6;
5
+ --accent-color: #ef4444;
6
+ --dark-bg: #1e293b;
7
+ --light-bg: #f1f5f9;
8
+ --card-bg: #ffffff;
9
+ --text-primary: #1e293b;
10
+ --text-secondary: #64748b;
11
+ --text-light: #f8fafc;
12
+ --border-color: #e2e8f0;
13
+ --success-color: #22c55e;
14
+ --warning-color: #f59e0b;
15
+ --error-color: #ef4444;
16
+ }
17
+
18
+ * {
19
+ margin: 0;
20
+ padding: 0;
21
+ box-sizing: border-box;
22
+ }
23
+
24
+ body {
25
+ font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Cantarell, sans-serif;
26
+ font-size: 16px;
27
+ line-height: 1.6;
28
+ color: var(--text-primary);
29
+ background-color: var(--light-bg);
30
+ background-image: linear-gradient(135deg, #f0f4f8 0%, #d9e2ec 100%);
31
+ min-height: 100vh;
32
+ }
33
+
34
+ .main-wrapper {
35
+ min-height: 100vh;
36
+ padding: 2rem 1rem;
37
+ }
38
+
39
+ .container {
40
+ max-width: 1100px;
41
+ margin: 0 auto;
42
+ }
43
+
44
+ /* 卡片样式 */
45
+ .card {
46
+ background-color: var(--card-bg);
47
+ border-radius: 12px;
48
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
49
+ margin-bottom: 2rem;
50
+ overflow: hidden;
51
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
52
+ }
53
+
54
+ .card:hover {
55
+ transform: translateY(-2px);
56
+ box-shadow: 0 6px 24px rgba(0, 0, 0, 0.12);
57
+ }
58
+
59
+ .card-header {
60
+ padding: 1.25rem 1.5rem;
61
+ background-color: var(--primary-color);
62
+ color: var(--text-light);
63
+ font-weight: 600;
64
+ font-size: 1.25rem;
65
+ display: flex;
66
+ align-items: center;
67
+ gap: 0.75rem;
68
+ }
69
+
70
+ .card-body {
71
+ padding: 1.5rem;
72
+ }
73
+
74
+ /* 头部样式 */
75
+ header {
76
+ text-align: center;
77
+ margin-bottom: 2.5rem;
78
+ }
79
+
80
+ .logo-area {
81
+ display: flex;
82
+ align-items: center;
83
+ justify-content: center;
84
+ margin-bottom: 1rem;
85
+ gap: 1rem;
86
+ }
87
+
88
+ .logo-icon {
89
+ font-size: 2.5rem;
90
+ color: var(--primary-color);
91
+ background-color: var(--card-bg);
92
+ width: 5rem;
93
+ height: 5rem;
94
+ border-radius: 50%;
95
+ display: flex;
96
+ align-items: center;
97
+ justify-content: center;
98
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
99
+ }
100
+
101
+ header h1 {
102
+ font-size: 2.5rem;
103
+ font-weight: 700;
104
+ color: var(--primary-color);
105
+ margin: 0;
106
+ letter-spacing: -0.5px;
107
+ }
108
+
109
+ .subtitle {
110
+ font-size: 1.25rem;
111
+ color: var(--text-secondary);
112
+ margin-top: 0.5rem;
113
+ }
114
+
115
+ /* 表单样式 */
116
+ .form-group {
117
+ margin-bottom: 1.75rem;
118
+ }
119
+
120
+ .form-group label {
121
+ display: block;
122
+ font-size: 1.1rem;
123
+ font-weight: 600;
124
+ margin-bottom: 0.75rem;
125
+ color: var(--text-primary);
126
+ display: flex;
127
+ align-items: center;
128
+ gap: 0.5rem;
129
+ }
130
+
131
+ .form-group label i {
132
+ color: var(--primary-color);
133
+ }
134
+
135
+ .input-wrapper {
136
+ position: relative;
137
+ }
138
+
139
+ input[type="text"] {
140
+ width: 100%;
141
+ padding: 0.875rem 1rem;
142
+ font-size: 1rem;
143
+ border: 2px solid var(--border-color);
144
+ border-radius: 8px;
145
+ background-color: var(--card-bg);
146
+ transition: all 0.2s ease;
147
+ color: var(--text-primary);
148
+ }
149
+
150
+ input[type="text"]:focus {
151
+ border-color: var(--secondary-color);
152
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
153
+ outline: none;
154
+ }
155
+
156
+ input[type="text"]::placeholder {
157
+ color: var(--text-secondary);
158
+ opacity: 0.7;
159
+ }
160
+
161
+ .input-hint {
162
+ font-size: 0.875rem;
163
+ color: var(--text-secondary);
164
+ margin-top: 0.5rem;
165
+ }
166
+
167
+ /* 端口选项 */
168
+ .port-option {
169
+ margin-top: 1rem;
170
+ display: flex;
171
+ align-items: center;
172
+ flex-wrap: wrap;
173
+ gap: 0.5rem;
174
+ padding: 1rem;
175
+ background-color: rgba(239, 68, 68, 0.07);
176
+ border-radius: 8px;
177
+ border-left: 3px solid var(--accent-color);
178
+ }
179
+
180
+ .port-option input[type="checkbox"] {
181
+ width: 1.25rem;
182
+ height: 1.25rem;
183
+ cursor: pointer;
184
+ accent-color: var(--accent-color);
185
+ }
186
+
187
+ .port-option label {
188
+ display: flex;
189
+ align-items: center;
190
+ margin-bottom: 0;
191
+ font-weight: 500;
192
+ color: var(--accent-color);
193
+ font-size: 1rem;
194
+ gap: 0.5rem;
195
+ }
196
+
197
+ .option-hint {
198
+ color: var(--text-secondary);
199
+ font-size: 0.875rem;
200
+ margin-top: 0.5rem;
201
+ margin-left: 1.75rem;
202
+ width: 100%;
203
+ }
204
+
205
+ /* 扫描选项样式 */
206
+ .options-container {
207
+ display: grid;
208
+ grid-template-columns: repeat(3, 1fr);
209
+ gap: 0.5rem;
210
+ padding: 0.5rem;
211
+ background-color: #f8fafc;
212
+ border-radius: 8px;
213
+ border: 1px solid var(--border-color);
214
+ }
215
+
216
+ .speed-options {
217
+ grid-template-columns: repeat(5, 1fr);
218
+ }
219
+
220
+ .option-item {
221
+ position: relative;
222
+ padding: 0.5rem;
223
+ border-radius: 6px;
224
+ background-color: white;
225
+ border: 1px solid var(--border-color);
226
+ transition: all 0.2s ease;
227
+ }
228
+
229
+ .option-item:hover {
230
+ border-color: var(--primary-color);
231
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
232
+ }
233
+
234
+ .option-item.option-selected,
235
+ .option-item input[type="radio"]:checked + label {
236
+ background-color: #e6f0ff;
237
+ border-color: #3b82f6;
238
+ }
239
+
240
+ .option-item input[type="radio"] {
241
+ position: absolute;
242
+ opacity: 0;
243
+ width: 0;
244
+ height: 0;
245
+ }
246
+
247
+ .option-item input[type="radio"] + label {
248
+ display: flex;
249
+ flex-direction: column;
250
+ cursor: pointer;
251
+ width: 100%;
252
+ padding-left: 1.25rem;
253
+ position: relative;
254
+ border-radius: 5px;
255
+ transition: background-color 0.2s ease;
256
+ }
257
+
258
+ .option-item input[type="radio"] + label::before {
259
+ content: '';
260
+ position: absolute;
261
+ left: 0;
262
+ top: 0.25rem;
263
+ width: 14px;
264
+ height: 14px;
265
+ border: 2px solid #cbd5e1;
266
+ border-radius: 50%;
267
+ background-color: white;
268
+ transition: all 0.2s ease;
269
+ }
270
+
271
+ .option-item input[type="radio"]:checked + label::before {
272
+ border-color: var(--primary-color);
273
+ background-color: var(--primary-color);
274
+ box-shadow: inset 0 0 0 2px white;
275
+ }
276
+
277
+ .option-item input[type="radio"]:checked + label .option-code,
278
+ .option-item input[type="radio"]:checked + label .option-desc {
279
+ color: var(--primary-color);
280
+ font-weight: 600;
281
+ }
282
+
283
+ .option-code {
284
+ font-family: 'Courier New', monospace;
285
+ font-weight: bold;
286
+ color: var(--primary-color);
287
+ margin-bottom: 0.15rem;
288
+ font-size: 0.9rem;
289
+ text-align: center;
290
+ }
291
+
292
+ .option-desc {
293
+ font-weight: 500;
294
+ color: var(--text-primary);
295
+ margin-bottom: 0.15rem;
296
+ font-size: 0.9rem;
297
+ text-align: center;
298
+ }
299
+
300
+ .option-detail {
301
+ font-size: 0.75rem;
302
+ color: var(--text-secondary);
303
+ line-height: 1.3;
304
+ text-align: center;
305
+ }
306
+
307
+ .option-item input[type="radio"]:checked + label .option-desc {
308
+ color: var(--primary-color);
309
+ }
310
+
311
+ .option-ripple {
312
+ position: absolute;
313
+ top: 50%;
314
+ left: 50%;
315
+ transform: translate(-50%, -50%) scale(0);
316
+ width: 100%;
317
+ height: 100%;
318
+ background-color: rgba(59, 130, 246, 0.1);
319
+ border-radius: 6px;
320
+ animation: ripple 0.5s ease-out forwards;
321
+ }
322
+
323
+ @keyframes ripple {
324
+ to {
325
+ transform: translate(-50%, -50%) scale(1);
326
+ opacity: 0;
327
+ }
328
+ }
329
+
330
+ /* 按钮样式 */
331
+ .form-actions {
332
+ display: flex;
333
+ gap: 1rem;
334
+ margin-top: 2rem;
335
+ }
336
+
337
+ .btn {
338
+ padding: 0.875rem 1.5rem;
339
+ font-size: 1rem;
340
+ font-weight: 600;
341
+ border-radius: 8px;
342
+ cursor: pointer;
343
+ display: flex;
344
+ align-items: center;
345
+ justify-content: center;
346
+ gap: 0.5rem;
347
+ transition: all 0.2s ease;
348
+ border: none;
349
+ }
350
+
351
+ .primary-btn {
352
+ background-color: var(--primary-color);
353
+ color: white;
354
+ min-width: 160px;
355
+ }
356
+
357
+ .primary-btn:hover {
358
+ background-color: var(--primary-dark);
359
+ transform: translateY(-2px);
360
+ box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
361
+ }
362
+
363
+ .primary-btn:disabled {
364
+ opacity: 0.7;
365
+ cursor: not-allowed;
366
+ }
367
+
368
+ .secondary-btn {
369
+ background-color: #f1f5f9;
370
+ color: var(--text-secondary);
371
+ border: 1px solid var(--border-color);
372
+ }
373
+
374
+ .secondary-btn:hover {
375
+ background-color: #e2e8f0;
376
+ }
377
+
378
+ /* 结果区域 */
379
+ .results-container {
380
+ margin-top: 2rem;
381
+ }
382
+
383
+ /* 加载动画 */
384
+ .loading {
385
+ text-align: center;
386
+ padding: 2.5rem;
387
+ background-color: white;
388
+ border-radius: 12px;
389
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
390
+ }
391
+
392
+ .spinner-container {
393
+ margin-bottom: 1.5rem;
394
+ }
395
+
396
+ .spinner {
397
+ border: 4px solid rgba(0, 0, 0, 0.1);
398
+ border-radius: 50%;
399
+ border-top: 4px solid var(--primary-color);
400
+ width: 50px;
401
+ height: 50px;
402
+ animation: spin 1s linear infinite;
403
+ margin: 0 auto;
404
+ }
405
+
406
+ .loading h3 {
407
+ color: var(--primary-color);
408
+ margin-bottom: 0.75rem;
409
+ display: flex;
410
+ align-items: center;
411
+ justify-content: center;
412
+ gap: 0.75rem;
413
+ }
414
+
415
+ .scan-warning {
416
+ color: var(--accent-color);
417
+ font-weight: 500;
418
+ margin-top: 1rem;
419
+ padding: 0.75rem 1rem;
420
+ background-color: rgba(239, 68, 68, 0.07);
421
+ border-radius: 6px;
422
+ display: flex;
423
+ align-items: center;
424
+ justify-content: center;
425
+ gap: 0.5rem;
426
+ }
427
+
428
+ @keyframes spin {
429
+ 0% { transform: rotate(0deg); }
430
+ 100% { transform: rotate(360deg); }
431
+ }
432
+
433
+ /* 结果显示 */
434
+ .results-card {
435
+ overflow: hidden;
436
+ }
437
+
438
+ .command-used {
439
+ background-color: var(--light-bg);
440
+ padding: 1rem 1.25rem;
441
+ border-radius: 8px;
442
+ margin-bottom: 1.5rem;
443
+ font-family: 'Courier New', monospace;
444
+ display: flex;
445
+ flex-direction: column;
446
+ gap: 0.75rem;
447
+ }
448
+
449
+ .command-label {
450
+ font-weight: 600;
451
+ color: var(--text-secondary);
452
+ display: flex;
453
+ align-items: center;
454
+ gap: 0.5rem;
455
+ }
456
+
457
+ .command-used code {
458
+ padding: 0.5rem 0.75rem;
459
+ background-color: var(--dark-bg);
460
+ color: #10b981;
461
+ border-radius: 4px;
462
+ font-family: 'Courier New', monospace;
463
+ overflow-x: auto;
464
+ white-space: nowrap;
465
+ }
466
+
467
+ .results-content {
468
+ margin-top: 1rem;
469
+ }
470
+
471
+ pre {
472
+ background-color: var(--dark-bg);
473
+ color: var(--text-light);
474
+ padding: 1.5rem;
475
+ border-radius: 8px;
476
+ overflow-x: auto;
477
+ white-space: pre-wrap;
478
+ word-wrap: break-word;
479
+ font-family: 'Courier New', Courier, monospace;
480
+ font-size: 0.9rem;
481
+ line-height: 1.5;
482
+ max-height: 500px;
483
+ overflow-y: auto;
484
+ }
485
+
486
+ /* 错误显示 */
487
+ .error-card .card-header {
488
+ background-color: var(--error-color);
489
+ }
490
+
491
+ #errorMessage {
492
+ background-color: rgba(239, 68, 68, 0.07);
493
+ border-left: 4px solid var(--error-color);
494
+ padding: 1rem 1.25rem;
495
+ border-radius: 4px;
496
+ font-weight: 500;
497
+ }
498
+
499
+ /* 底部 */
500
+ footer {
501
+ text-align: center;
502
+ padding: 1.5rem 0;
503
+ color: var(--text-secondary);
504
+ font-size: 0.9rem;
505
+ }
506
+
507
+ .footer-content {
508
+ display: flex;
509
+ align-items: center;
510
+ justify-content: center;
511
+ gap: 0.5rem;
512
+ }
513
+
514
+ .footer-content i {
515
+ color: var(--primary-color);
516
+ }
517
+
518
+ /* 响应式设计 */
519
+ @media (max-width: 768px) {
520
+ .main-wrapper {
521
+ padding: 1rem;
522
+ }
523
+
524
+ .form-actions {
525
+ flex-direction: column;
526
+ }
527
+
528
+ .options-container {
529
+ grid-template-columns: repeat(2, 1fr);
530
+ }
531
+
532
+ .speed-options {
533
+ grid-template-columns: repeat(3, 1fr);
534
+ }
535
+
536
+ header h1 {
537
+ font-size: 2rem;
538
+ }
539
+
540
+ .subtitle {
541
+ font-size: 1rem;
542
+ }
543
+
544
+ .btn {
545
+ width: 100%;
546
+ }
547
+ }
548
+
549
+ @media (max-width: 480px) {
550
+ .options-container {
551
+ grid-template-columns: 1fr;
552
+ }
553
+
554
+ .speed-options {
555
+ grid-template-columns: repeat(2, 1fr);
556
+ }
557
+
558
+ .port-option {
559
+ padding: 0.75rem;
560
+ }
561
+
562
+ .option-hint {
563
+ margin-left: 0;
564
+ }
565
+
566
+ .card-header {
567
+ padding: 1rem;
568
+ }
569
+
570
+ .card-body {
571
+ padding: 1rem;
572
+ }
573
+
574
+ pre {
575
+ padding: 1rem;
576
+ font-size: 0.8rem;
577
+ }
578
+ }
579
+
580
+ /* 输入验证样式 */
581
+ .input-error {
582
+ border-color: var(--error-color) !important;
583
+ box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2) !important;
584
+ }
585
+
586
+ .was-validated .input-wrapper::after {
587
+ content: '✓';
588
+ position: absolute;
589
+ right: 12px;
590
+ top: 50%;
591
+ transform: translateY(-50%);
592
+ color: var(--success-color);
593
+ font-weight: bold;
594
+ opacity: 0;
595
+ transition: opacity 0.3s;
596
+ }
597
+
598
+ .was-validated input:valid ~ .input-wrapper::after {
599
+ opacity: 1;
600
+ }
601
+
602
+ .disabled {
603
+ opacity: 0.6;
604
+ }
605
+
606
+ /* 选项选中样式 */
607
+ .option-selected {
608
+ background-color: rgba(37, 99, 235, 0.08);
609
+ border-left: 3px solid var(--primary-color);
610
+ }
611
+
612
+ .option-ripple {
613
+ position: absolute;
614
+ width: 20px;
615
+ height: 20px;
616
+ background-color: rgba(37, 99, 235, 0.3);
617
+ border-radius: 50%;
618
+ transform: scale(0);
619
+ animation: ripple 0.5s linear;
620
+ left: 10px;
621
+ top: 15px;
622
+ }
623
+
624
+ @keyframes ripple {
625
+ to {
626
+ transform: scale(3);
627
+ opacity: 0;
628
+ }
629
+ }
630
+
631
+ /* 按钮动画 */
632
+ .btn-spinner {
633
+ width: 16px;
634
+ height: 16px;
635
+ border: 2px solid rgba(255, 255, 255, 0.3);
636
+ border-radius: 50%;
637
+ border-top-color: white;
638
+ animation: spin 0.8s linear infinite;
639
+ margin-right: 8px;
640
+ }
641
+
642
+ .btn-active {
643
+ transform: scale(0.95);
644
+ }
645
+
646
+ /* 结果高亮 */
647
+ .hl-port {
648
+ font-weight: 600;
649
+ color: #8b5cf6;
650
+ }
651
+
652
+ .hl-status-open {
653
+ color: var(--success-color);
654
+ font-weight: 600;
655
+ }
656
+
657
+ .hl-status-closed {
658
+ color: var(--error-color);
659
+ }
660
+
661
+ .hl-status-filtered {
662
+ color: var(--warning-color);
663
+ }
664
+
665
+ .hl-ip {
666
+ font-weight: 600;
667
+ color: #ec4899;
668
+ }
669
+
670
+ .hl-service-label {
671
+ font-weight: 600;
672
+ color: #f59e0b;
673
+ }
674
+
675
+ .hl-service {
676
+ color: #10b981;
677
+ }
678
+
679
+ /* 动画和过渡效果 */
680
+ .results-card,
681
+ .error-card,
682
+ .loading {
683
+ transition: opacity 0.3s ease;
684
+ }
685
+
686
+ /* 滚动条美化 */
687
+ ::-webkit-scrollbar {
688
+ width: 8px;
689
+ height: 8px;
690
+ }
691
+
692
+ ::-webkit-scrollbar-track {
693
+ background: #f1f5f9;
694
+ border-radius: 4px;
695
+ }
696
+
697
+ ::-webkit-scrollbar-thumb {
698
+ background: #94a3b8;
699
+ border-radius: 4px;
700
+ }
701
+
702
+ ::-webkit-scrollbar-thumb:hover {
703
+ background: #64748b;
704
+ }
705
+
706
+ /* 进度条样式 */
707
+ .progress-container {
708
+ margin: 1.5rem 0;
709
+ }
710
+
711
+ .progress-bar {
712
+ width: 100%;
713
+ height: 12px;
714
+ background-color: #e2e8f0;
715
+ border-radius: 6px;
716
+ overflow: hidden;
717
+ }
718
+
719
+ .progress-fill {
720
+ height: 100%;
721
+ width: 0%;
722
+ background-color: var(--primary-color);
723
+ background-image: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
724
+ transition: width 0.3s ease;
725
+ border-radius: 6px;
726
+ }
727
+
728
+ .progress-text {
729
+ margin-top: 0.5rem;
730
+ font-size: 0.9rem;
731
+ color: var(--text-secondary);
732
+ text-align: center;
733
+ }
734
+
735
+ /* 实时输出区域 */
736
+ .live-output {
737
+ margin-top: 1.5rem;
738
+ background-color: var(--dark-bg);
739
+ border-radius: 6px;
740
+ overflow: hidden;
741
+ }
742
+
743
+ .live-output h4 {
744
+ background-color: rgba(0, 0, 0, 0.3);
745
+ color: var(--text-light);
746
+ padding: 0.75rem 1rem;
747
+ margin: 0;
748
+ font-size: 0.9rem;
749
+ font-weight: 500;
750
+ }
751
+
752
+ .live-output-content {
753
+ padding: 1rem;
754
+ margin: 0;
755
+ height: 150px;
756
+ overflow-y: auto;
757
+ font-family: 'Courier New', Courier, monospace;
758
+ font-size: 0.85rem;
759
+ line-height: 1.5;
760
+ color: #a3e635;
761
+ background-color: transparent;
762
+ }
763
+
764
+ /* 取消扫描按钮 */
765
+ .cancel-btn {
766
+ background-color: var(--error-color);
767
+ color: white;
768
+ margin-top: 1rem;
769
+ }
770
+
771
+ .cancel-btn:hover {
772
+ background-color: #dc2626;
773
+ }
774
+
775
+ /* 全屏模式下的样式调整 */
776
+ .fullscreen-mode .live-output-content {
777
+ height: 300px;
778
+ }
779
+
780
+ /* 消息提示 */
781
+ .info-toast {
782
+ position: fixed;
783
+ bottom: 30px;
784
+ left: 50%;
785
+ transform: translateX(-50%) translateY(100px);
786
+ background-color: var(--dark-bg);
787
+ color: var(--text-light);
788
+ padding: 12px 20px;
789
+ border-radius: 8px;
790
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
791
+ z-index: 1000;
792
+ opacity: 0;
793
+ transition: transform 0.4s ease, opacity 0.4s ease;
794
+ max-width: 80%;
795
+ text-align: center;
796
+ display: flex;
797
+ align-items: center;
798
+ gap: 8px;
799
+ }
800
+
801
+ .info-toast.show {
802
+ transform: translateX(-50%) translateY(0);
803
+ opacity: 1;
804
+ }
805
+
806
+ .info-toast i {
807
+ color: var(--secondary-color);
808
+ }
809
+
810
+ /* WebSocket连接状态指示器 */
811
+ .socket-status {
812
+ position: fixed;
813
+ bottom: 10px;
814
+ right: 10px;
815
+ width: 12px;
816
+ height: 12px;
817
+ border-radius: 50%;
818
+ background-color: #ef4444;
819
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
820
+ z-index: 1000;
821
+ transition: all 0.3s ease;
822
+ }
823
+
824
+ .socket-status.connected {
825
+ background-color: #10b981;
826
+ animation: pulse-green 2s infinite;
827
+ }
828
+
829
+ .socket-status.secure {
830
+ background-color: #3b82f6;
831
+ animation: pulse-blue 2s infinite;
832
+ }
833
+
834
+ .socket-status:hover::after {
835
+ content: attr(title);
836
+ position: absolute;
837
+ right: 20px;
838
+ top: -5px;
839
+ background-color: rgba(0, 0, 0, 0.8);
840
+ color: white;
841
+ padding: 5px 10px;
842
+ border-radius: 4px;
843
+ font-size: 12px;
844
+ white-space: nowrap;
845
+ }
846
+
847
+ @keyframes pulse-green {
848
+ 0% {
849
+ box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
850
+ }
851
+ 70% {
852
+ box-shadow: 0 0 0 5px rgba(16, 185, 129, 0);
853
+ }
854
+ 100% {
855
+ box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
856
+ }
857
+ }
858
+
859
+ @keyframes pulse-blue {
860
+ 0% {
861
+ box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7);
862
+ }
863
+ 70% {
864
+ box-shadow: 0 0 0 5px rgba(59, 130, 246, 0);
865
+ }
866
+ 100% {
867
+ box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
868
+ }
869
+ }
870
+
871
+ /* socket.io 必要引入 */
872
+ #app-template-section {
873
+ display: none;
874
+ }
875
+
876
+ /* 线程选择样式 */
877
+ .threads-container {
878
+ padding: 1rem;
879
+ background-color: #f8fafc;
880
+ border-radius: 8px;
881
+ border: 1px solid var(--border-color);
882
+ }
883
+
884
+ .threads-slider-container {
885
+ display: flex;
886
+ align-items: center;
887
+ gap: 1rem;
888
+ margin-bottom: 0.75rem;
889
+ }
890
+
891
+ input[type="range"] {
892
+ flex-grow: 1;
893
+ height: 6px;
894
+ appearance: none;
895
+ background: linear-gradient(to right, var(--primary-color), var(--secondary-color));
896
+ outline: none;
897
+ border-radius: 3px;
898
+ }
899
+
900
+ input[type="range"]::-webkit-slider-thumb {
901
+ appearance: none;
902
+ width: 20px;
903
+ height: 20px;
904
+ background: #fff;
905
+ border: 2px solid var(--primary-color);
906
+ border-radius: 50%;
907
+ cursor: pointer;
908
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
909
+ }
910
+
911
+ input[type="range"]::-moz-range-thumb {
912
+ width: 20px;
913
+ height: 20px;
914
+ background: #fff;
915
+ border: 2px solid var(--primary-color);
916
+ border-radius: 50%;
917
+ cursor: pointer;
918
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
919
+ }
920
+
921
+ .threads-display {
922
+ display: flex;
923
+ align-items: center;
924
+ gap: 0.5rem;
925
+ min-width: 100px;
926
+ }
927
+
928
+ .threads-display input[type="number"] {
929
+ width: 50px;
930
+ padding: 0.5rem;
931
+ text-align: center;
932
+ border: 1px solid var(--border-color);
933
+ border-radius: 4px;
934
+ }
935
+
936
+ .threads-display span {
937
+ color: var(--text-secondary);
938
+ }
939
+
940
+ input[type="number"]::-webkit-inner-spin-button,
941
+ input[type="number"]::-webkit-outer-spin-button {
942
+ opacity: 1;
943
+ }
944
+
945
+ .threads-modes {
946
+ display: flex;
947
+ justify-content: space-between;
948
+ margin-top: 1rem;
949
+ }
950
+
951
+ .thread-mode {
952
+ text-align: center;
953
+ cursor: pointer;
954
+ padding: 0.75rem 0.5rem;
955
+ border-radius: 6px;
956
+ transition: all 0.2s ease;
957
+ flex: 1;
958
+ margin: 0 0.25rem;
959
+ }
960
+
961
+ .thread-mode:hover {
962
+ background-color: rgba(37, 99, 235, 0.08);
963
+ }
964
+
965
+ .thread-mode.active {
966
+ background-color: rgba(37, 99, 235, 0.12);
967
+ }
968
+
969
+ .mode-icon {
970
+ font-size: 1.25rem;
971
+ margin-bottom: 0.3rem;
972
+ color: var(--primary-color);
973
+ }
974
+
975
+ .mode-label {
976
+ font-size: 0.85rem;
977
+ color: var(--text-secondary);
978
+ }
979
+
980
+ .threads-hint {
981
+ margin: 0.75rem 0;
982
+ color: var(--text-secondary);
983
+ display: flex;
984
+ align-items: center;
985
+ gap: 0.5rem;
986
+ }
987
+
988
+ /* 任务状态显示 */
989
+ .thread-count-display {
990
+ font-size: 1rem;
991
+ color: var(--primary-color);
992
+ margin: 0.5rem 0;
993
+ display: flex;
994
+ align-items: center;
995
+ justify-content: center;
996
+ gap: 0.5rem;
997
+ }
998
+
999
+ .tasks-overview {
1000
+ margin-top: 1.5rem;
1001
+ background-color: white;
1002
+ border-radius: 8px;
1003
+ border: 1px solid var(--border-color);
1004
+ overflow: hidden;
1005
+ }
1006
+
1007
+ .tasks-overview h4 {
1008
+ background-color: #f1f5f9;
1009
+ padding: 0.75rem 1rem;
1010
+ margin: 0;
1011
+ font-size: 0.95rem;
1012
+ color: var(--text-secondary);
1013
+ font-weight: 500;
1014
+ display: flex;
1015
+ align-items: center;
1016
+ gap: 0.5rem;
1017
+ border-bottom: 1px solid var(--border-color);
1018
+ }
1019
+
1020
+ .tasks-grid {
1021
+ padding: 1rem;
1022
+ display: grid;
1023
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
1024
+ gap: 0.75rem;
1025
+ max-height: 200px;
1026
+ overflow-y: auto;
1027
+ }
1028
+
1029
+ .task-item {
1030
+ padding: 0.75rem;
1031
+ background-color: #f8fafc;
1032
+ border-radius: 6px;
1033
+ border-left: 3px solid var(--primary-color);
1034
+ font-size: 0.85rem;
1035
+ position: relative;
1036
+ }
1037
+
1038
+ .task-item .task-id {
1039
+ font-weight: 600;
1040
+ margin-bottom: 0.3rem;
1041
+ color: var(--primary-color);
1042
+ display: flex;
1043
+ align-items: center;
1044
+ justify-content: space-between;
1045
+ }
1046
+
1047
+ .task-item .task-target,
1048
+ .task-item .task-ports {
1049
+ margin: 0.2rem 0;
1050
+ white-space: nowrap;
1051
+ overflow: hidden;
1052
+ text-overflow: ellipsis;
1053
+ color: var(--text-secondary);
1054
+ font-family: 'Courier New', monospace;
1055
+ }
1056
+
1057
+ .task-status {
1058
+ position: absolute;
1059
+ top: 0.75rem;
1060
+ right: 0.75rem;
1061
+ width: 12px;
1062
+ height: 12px;
1063
+ border-radius: 50%;
1064
+ }
1065
+
1066
+ .task-status.pending {
1067
+ background-color: #94a3b8;
1068
+ }
1069
+
1070
+ .task-status.running {
1071
+ background-color: #3b82f6;
1072
+ animation: pulse 1.5s infinite;
1073
+ }
1074
+
1075
+ .task-status.completed {
1076
+ background-color: #10b981;
1077
+ }
1078
+
1079
+ .task-status.error {
1080
+ background-color: #ef4444;
1081
+ }
1082
+
1083
+ @keyframes pulse {
1084
+ 0% {
1085
+ transform: scale(0.95);
1086
+ box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.5);
1087
+ }
1088
+
1089
+ 70% {
1090
+ transform: scale(1);
1091
+ box-shadow: 0 0 0 5px rgba(59, 130, 246, 0);
1092
+ }
1093
+
1094
+ 100% {
1095
+ transform: scale(0.95);
1096
+ box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
1097
+ }
1098
+ }
1099
+
1100
+ /* 增强进度条样式 */
1101
+ .progress-container {
1102
+ margin: 1.5rem 0;
1103
+ }
1104
+
1105
+ .progress-bar {
1106
+ position: relative;
1107
+ width: 100%;
1108
+ height: 12px;
1109
+ background-color: #e2e8f0;
1110
+ border-radius: 6px;
1111
+ overflow: hidden;
1112
+ }
1113
+
1114
+ .progress-fill {
1115
+ height: 100%;
1116
+ width: 0%;
1117
+ background-image: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
1118
+ transition: width 0.3s ease;
1119
+ border-radius: 6px;
1120
+ position: relative;
1121
+ }
1122
+
1123
+ .progress-fill::after {
1124
+ content: '';
1125
+ position: absolute;
1126
+ top: 0;
1127
+ left: 0;
1128
+ right: 0;
1129
+ bottom: 0;
1130
+ background-image: linear-gradient(
1131
+ -45deg,
1132
+ rgba(255, 255, 255, 0.2) 25%,
1133
+ transparent 25%,
1134
+ transparent 50%,
1135
+ rgba(255, 255, 255, 0.2) 50%,
1136
+ rgba(255, 255, 255, 0.2) 75%,
1137
+ transparent 75%,
1138
+ transparent
1139
+ );
1140
+ background-size: 50px 50px;
1141
+ animation: move 2s linear infinite;
1142
+ }
1143
+
1144
+ @keyframes move {
1145
+ 0% {
1146
+ background-position: 0 0;
1147
+ }
1148
+ 100% {
1149
+ background-position: 50px 50px;
1150
+ }
1151
+ }
1152
+
1153
+ .progress-text {
1154
+ display: flex;
1155
+ justify-content: space-between;
1156
+ margin-top: 0.5rem;
1157
+ font-size: 0.9rem;
1158
+ color: var(--text-secondary);
1159
+ }
1160
+
1161
+ .progress-text .completed-tasks {
1162
+ font-weight: 500;
1163
+ color: var(--primary-color);
1164
+ }
1165
+
1166
+ /* 响应式调整 */
1167
+ @media (max-width: 768px) {
1168
+ .threads-slider-container {
1169
+ flex-direction: column;
1170
+ gap: 0.5rem;
1171
+ }
1172
+
1173
+ .threads-display {
1174
+ width: 100%;
1175
+ justify-content: center;
1176
+ }
1177
+
1178
+ .threads-modes {
1179
+ flex-wrap: wrap;
1180
+ }
1181
+
1182
+ .thread-mode {
1183
+ flex: 0 0 calc(50% - 0.5rem);
1184
+ margin-bottom: 0.5rem;
1185
+ }
1186
+
1187
+ .tasks-grid {
1188
+ grid-template-columns: 1fr;
1189
+ }
1190
+ }
1191
+
1192
+ /* 扫描结果高亮样式 */
1193
+ .results-content pre {
1194
+ font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
1195
+ line-height: 1.5;
1196
+ white-space: pre-wrap;
1197
+ word-break: break-all;
1198
+ }
1199
+
1200
+ .hl-header {
1201
+ color: #3b82f6;
1202
+ font-weight: bold;
1203
+ display: block;
1204
+ margin-bottom: 8px;
1205
+ padding-bottom: 4px;
1206
+ border-bottom: 1px solid rgba(59, 130, 246, 0.3);
1207
+ }
1208
+
1209
+ .hl-table-header {
1210
+ color: #6b7280;
1211
+ font-weight: bold;
1212
+ display: block;
1213
+ margin: 8px 0;
1214
+ padding: 4px 0;
1215
+ background-color: rgba(243, 244, 246, 0.5);
1216
+ }
1217
+
1218
+ .hl-target {
1219
+ color: #8b5cf6;
1220
+ font-weight: bold;
1221
+ display: block;
1222
+ margin-top: 12px;
1223
+ margin-bottom: 4px;
1224
+ padding: 4px 0;
1225
+ border-top: 1px dashed rgba(139, 92, 246, 0.3);
1226
+ }
1227
+
1228
+ .hl-port {
1229
+ color: #2563eb;
1230
+ font-weight: bold;
1231
+ display: inline-block;
1232
+ min-width: 90px;
1233
+ }
1234
+
1235
+ .hl-state-open {
1236
+ color: #10b981;
1237
+ font-weight: bold;
1238
+ display: inline-block;
1239
+ min-width: 70px;
1240
+ }
1241
+
1242
+ .hl-state-closed {
1243
+ color: #ef4444;
1244
+ font-weight: normal;
1245
+ display: inline-block;
1246
+ min-width: 70px;
1247
+ }
1248
+
1249
+ .hl-state-filtered {
1250
+ color: #f59e0b;
1251
+ font-weight: normal;
1252
+ display: inline-block;
1253
+ min-width: 70px;
1254
+ }
1255
+
1256
+ .hl-service {
1257
+ color: #6b7280;
1258
+ display: inline-block;
1259
+ }
1260
+
1261
+ .hl-ip {
1262
+ color: #ec4899;
1263
+ font-weight: bold;
1264
+ }
1265
+
1266
+ .hl-domain {
1267
+ color: #8b5cf6;
1268
+ font-weight: bold;
1269
+ }
1270
+
1271
+ .hl-service-label {
1272
+ color: #6b7280;
1273
+ font-weight: bold;
1274
+ margin-right: 5px;
1275
+ }
1276
+
1277
+ .hl-service-info {
1278
+ color: #059669;
1279
+ }
1280
+
1281
+ .hl-summary {
1282
+ display: block;
1283
+ margin-top: 12px;
1284
+ padding-top: 8px;
1285
+ color: #3b82f6;
1286
+ font-weight: bold;
1287
+ border-top: 1px solid rgba(59, 130, 246, 0.3);
1288
+ }
1289
+
1290
+ /* 表格式显示 */
1291
+ .results-content pre {
1292
+ tab-size: 4;
1293
+ -moz-tab-size: 4;
1294
+ }
app/static/js/script.js ADDED
@@ -0,0 +1,757 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener('DOMContentLoaded', function() {
2
+ const scanForm = document.getElementById('scanForm');
3
+ const scanButton = document.getElementById('scanButton');
4
+ const clearButton = document.getElementById('clearButton');
5
+ const loading = document.getElementById('loading');
6
+ const results = document.getElementById('results');
7
+ const error = document.getElementById('error');
8
+ const resultsContent = document.getElementById('resultsContent');
9
+ const errorMessage = document.getElementById('errorMessage');
10
+ const scanWarning = document.getElementById('scanWarning');
11
+ const portsInput = document.getElementById('ports');
12
+ const scanAllPortsCheckbox = document.getElementById('scanAllPorts');
13
+ const target = document.getElementById('target');
14
+ const optionCheckboxes = document.querySelectorAll('input[name="options"]');
15
+ const tasksGrid = document.getElementById('tasksGrid');
16
+ const threadsInUse = document.getElementById('threadsInUse');
17
+ const parallelTasksSlider = document.getElementById('parallelTasksSlider');
18
+ const parallelTasks = document.getElementById('parallelTasks');
19
+ const threadModes = document.querySelectorAll('.thread-mode');
20
+
21
+ const DEFAULT_THREADS = 8;
22
+ const MIN_THREADS = 4;
23
+ const MAX_THREADS = 16;
24
+
25
+ const TASK_STATUS = {
26
+ 'pending': '等待中',
27
+ 'task_running': '扫描中',
28
+ 'task_progress': '扫描中',
29
+ 'task_completed': '已完成',
30
+ 'task_error': '出错'
31
+ };
32
+
33
+ let socket;
34
+ let currentScanId = null;
35
+ let reconnectAttempts = 0;
36
+ let isConnected = false;
37
+ const maxReconnectAttempts = 5;
38
+
39
+ let activeTasks = {};
40
+ let totalTasks = 0;
41
+ let completedTasks = 0;
42
+
43
+ const progressContainer = document.createElement('div');
44
+ progressContainer.className = 'progress-container';
45
+ progressContainer.innerHTML = `
46
+ <div class="progress-bar">
47
+ <div class="progress-fill" id="progressFill"></div>
48
+ </div>
49
+ <div class="progress-text">
50
+ <div>扫描进度: <span id="progressText">0%</span></div>
51
+ <div class="completed-tasks"><span id="completedTasksCount">0</span>/<span id="totalTasksCount">0</span> 任务</div>
52
+ </div>
53
+ `;
54
+ loading.insertBefore(progressContainer, document.querySelector('#tasksOverview'));
55
+
56
+ const progressFill = document.getElementById('progressFill');
57
+ const progressText = document.getElementById('progressText');
58
+ const completedTasksCount = document.getElementById('completedTasksCount');
59
+ const totalTasksCount = document.getElementById('totalTasksCount');
60
+
61
+ const liveOutputContainer = document.createElement('div');
62
+ liveOutputContainer.className = 'live-output';
63
+ liveOutputContainer.innerHTML = `
64
+ <h4>实时扫描输出</h4>
65
+ <pre id="liveOutput" class="live-output-content"></pre>
66
+ `;
67
+ loading.appendChild(liveOutputContainer);
68
+
69
+ const liveOutput = document.getElementById('liveOutput');
70
+
71
+ const cancelButton = document.createElement('button');
72
+ cancelButton.type = 'button';
73
+ cancelButton.id = 'cancelScanButton';
74
+ cancelButton.className = 'btn cancel-btn';
75
+ cancelButton.innerHTML = '<i class="fas fa-stop-circle"></i> 取消扫描';
76
+ cancelButton.style.display = 'none';
77
+ loading.appendChild(cancelButton);
78
+
79
+ const formElements = document.querySelectorAll('.form-group, .form-actions');
80
+ formElements.forEach((element, index) => {
81
+ element.style.opacity = '0';
82
+ element.style.transform = 'translateY(20px)';
83
+ element.style.transition = 'opacity 0.4s ease, transform 0.4s ease';
84
+
85
+ setTimeout(() => {
86
+ element.style.opacity = '1';
87
+ element.style.transform = 'translateY(0)';
88
+ }, 100 + index * 100);
89
+ });
90
+
91
+ function initThreadControl() {
92
+ parallelTasksSlider.addEventListener('input', function() {
93
+ parallelTasks.value = this.value;
94
+ updateThreadModeSelection(this.value);
95
+ threadsInUse.textContent = this.value;
96
+ });
97
+
98
+ parallelTasks.addEventListener('input', function() {
99
+ let value = parseInt(this.value);
100
+ if (isNaN(value) || value < MIN_THREADS) value = MIN_THREADS;
101
+ if (value > MAX_THREADS) value = MAX_THREADS;
102
+
103
+ this.value = value;
104
+ parallelTasksSlider.value = value;
105
+ updateThreadModeSelection(value);
106
+ threadsInUse.textContent = value;
107
+ });
108
+
109
+ threadModes.forEach(mode => {
110
+ mode.addEventListener('click', function() {
111
+ const value = parseInt(this.dataset.value);
112
+ parallelTasksSlider.value = value;
113
+ parallelTasks.value = value;
114
+ updateThreadModeSelection(value);
115
+ threadsInUse.textContent = value;
116
+ });
117
+ });
118
+
119
+ updateThreadModeSelection(DEFAULT_THREADS);
120
+ }
121
+
122
+ function updateThreadModeSelection(value) {
123
+ threadModes.forEach(mode => {
124
+ if (parseInt(mode.dataset.value) === parseInt(value)) {
125
+ mode.classList.add('active');
126
+ } else {
127
+ mode.classList.remove('active');
128
+ }
129
+ });
130
+ }
131
+
132
+ function initWebSocket() {
133
+ if (socket && socket.connected) {
134
+ return;
135
+ }
136
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
137
+ console.log(`使用WebSocket协议: ${protocol}`);
138
+
139
+ const host = window.location.host;
140
+ const wsUrl = `${protocol}//${host}`;
141
+ console.log(`WebSocket连接URL: ${wsUrl}`);
142
+
143
+ socket = io(wsUrl, {
144
+ reconnection: true,
145
+ reconnectionDelay: 1000,
146
+ reconnectionDelayMax: 5000,
147
+ reconnectionAttempts: maxReconnectAttempts,
148
+ transports: ['websocket'],
149
+ upgrade: false
150
+ });
151
+
152
+ socket.on('connect', function() {
153
+ console.log('WebSocket连接已建立');
154
+ isConnected = true;
155
+ reconnectAttempts = 0;
156
+
157
+ updateSocketStatus(true, protocol === 'wss:');
158
+
159
+ if (currentScanId) {
160
+ joinScanRoom(currentScanId);
161
+ }
162
+ });
163
+
164
+ socket.on('connection_response', function(data) {
165
+ console.log('连接状态:', data.status);
166
+ console.log('传输方式:', data.transport);
167
+ console.log('会话ID:', data.sid);
168
+
169
+ const isWebSocket = data.transport === 'websocket';
170
+ const isSecure = window.location.protocol === 'https:';
171
+
172
+ updateSocketStatus(true, isWebSocket && isSecure);
173
+
174
+ showInfo(`已连接到服务器`);
175
+ });
176
+
177
+ socket.on('scan_started', function(data) {
178
+ console.log('扫描已开始,ID:', data.scan_id, '线程数:', data.parallel_tasks);
179
+ currentScanId = data.scan_id;
180
+ threadsInUse.textContent = data.parallel_tasks;
181
+
182
+ sessionStorage.setItem('currentScanId', currentScanId);
183
+ });
184
+
185
+ socket.on('scan_update', function(data) {
186
+ console.log('扫描更新:', data.status, data);
187
+
188
+ switch(data.status) {
189
+ case 'starting':
190
+ updateProgress(5);
191
+ updateLiveOutput(`正在使用 ${data.threads} 个线程开始扫描...\n`);
192
+ break;
193
+
194
+ case 'tasks_created':
195
+ activeTasks = {};
196
+ totalTasks = data.tasks.length;
197
+ completedTasks = 0;
198
+
199
+ totalTasksCount.textContent = totalTasks;
200
+ completedTasksCount.textContent = completedTasks;
201
+
202
+ createTaskCards(data.tasks);
203
+ updateLiveOutput(`已创建 ${totalTasks} 个扫描子任务\n`);
204
+ break;
205
+
206
+ case 'task_running':
207
+ updateTaskStatus(data.task_id, 'running');
208
+ updateLiveOutput(`${data.message}\n`);
209
+ break;
210
+
211
+ case 'task_progress':
212
+ if (data.partial_result) {
213
+ updateLiveOutput(data.partial_result);
214
+ }
215
+ increaseProgress();
216
+ break;
217
+
218
+ case 'task_completed':
219
+ updateTaskStatus(data.task_id, 'completed');
220
+ completedTasks++;
221
+ completedTasksCount.textContent = completedTasks;
222
+
223
+ const percentComplete = Math.round((completedTasks / totalTasks) * 100);
224
+ updateProgress(Math.min(percentComplete, 99));
225
+ break;
226
+
227
+ case 'task_error':
228
+ updateTaskStatus(data.task_id, 'error');
229
+ updateLiveOutput(`错误: ${data.message}\n`);
230
+ break;
231
+
232
+ case 'completed':
233
+ hideLoading();
234
+ showResults(data.result);
235
+ resetScanButton();
236
+ currentScanId = null;
237
+ sessionStorage.removeItem('currentScanId');
238
+ break;
239
+
240
+ case 'error':
241
+ hideLoading();
242
+ showError(data.message || data.error);
243
+ resetScanButton();
244
+ currentScanId = null;
245
+ sessionStorage.removeItem('currentScanId');
246
+ break;
247
+
248
+ case 'cancelled':
249
+ hideLoading();
250
+ showInfo('扫描已取消');
251
+ resetScanButton();
252
+ currentScanId = null;
253
+ sessionStorage.removeItem('currentScanId');
254
+ break;
255
+ }
256
+ });
257
+
258
+ socket.on('disconnect', function() {
259
+ console.log('WebSocket连接已断开');
260
+ isConnected = false;
261
+ updateSocketStatus(false, false);
262
+
263
+ if (reconnectAttempts < maxReconnectAttempts) {
264
+ console.log(`尝试重新连接 (${reconnectAttempts + 1}/${maxReconnectAttempts})...`);
265
+ reconnectAttempts++;
266
+ } else if (currentScanId) {
267
+ hideLoading();
268
+ showError('与服务器的连接已丢失,无法接收实时扫描更新。请检查您的网络连接并重试。');
269
+ resetScanButton();
270
+ }
271
+ });
272
+ }
273
+
274
+ function updateSocketStatus(connected, isSecure) {
275
+ const statusIndicator = document.getElementById('socketStatus');
276
+ if (connected) {
277
+ statusIndicator.classList.add('connected');
278
+ statusIndicator.title = isSecure ?
279
+ '已建立安全WebSocket连接 (WSS)' :
280
+ '已建立WebSocket连接 (WS)';
281
+
282
+ if (isSecure) {
283
+ statusIndicator.classList.add('secure');
284
+ } else {
285
+ statusIndicator.classList.remove('secure');
286
+ }
287
+ } else {
288
+ statusIndicator.classList.remove('connected', 'secure');
289
+ statusIndicator.title = '未连接到服务器';
290
+ }
291
+ }
292
+
293
+ function createTaskCards(tasks) {
294
+ tasksGrid.innerHTML = '';
295
+
296
+ tasks.forEach(task => {
297
+ const taskCard = document.createElement('div');
298
+ taskCard.className = 'task-item';
299
+ taskCard.id = `task-${task.task_id}`;
300
+
301
+ taskCard.innerHTML = `
302
+ <div class="task-id">
303
+ ${task.task_id}
304
+ <span class="task-status pending" title="等待中"></span>
305
+ </div>
306
+ <div class="task-target" title="${task.target}">
307
+ <i class="fas fa-crosshairs"></i> ${task.target}
308
+ </div>
309
+ <div class="task-ports" title="${task.ports}">
310
+ <i class="fas fa-plug"></i> ${task.ports}
311
+ </div>
312
+ `;
313
+
314
+ tasksGrid.appendChild(taskCard);
315
+
316
+ // 记录任务状态
317
+ activeTasks[task.task_id] = {
318
+ status: 'pending',
319
+ element: taskCard
320
+ };
321
+ });
322
+ }
323
+
324
+ // 更新任务状态
325
+ function updateTaskStatus(taskId, status) {
326
+ if (activeTasks[taskId]) {
327
+ activeTasks[taskId].status = status;
328
+
329
+ const taskCard = document.getElementById(`task-${taskId}`);
330
+ if (taskCard) {
331
+ const statusElement = taskCard.querySelector('.task-status');
332
+
333
+ // 移除所有状态类
334
+ statusElement.classList.remove('pending', 'running', 'completed', 'error');
335
+
336
+ // 添加新状态类
337
+ statusElement.classList.add(status);
338
+
339
+ // 更新提示文本
340
+ statusElement.title = TASK_STATUS[status] || status;
341
+ }
342
+ }
343
+ }
344
+
345
+ // 加入扫描房间
346
+ function joinScanRoom(scanId) {
347
+ if (socket && socket.connected) {
348
+ socket.emit('join_scan', { scan_id: scanId });
349
+ }
350
+ }
351
+
352
+ // 端口输入与全端口扫描选项互斥
353
+ scanAllPortsCheckbox.addEventListener('change', function() {
354
+ if (this.checked) {
355
+ portsInput.disabled = true;
356
+ portsInput.placeholder = "已选择全端口扫描";
357
+ portsInput.parentElement.classList.add('disabled');
358
+ } else {
359
+ portsInput.disabled = false;
360
+ portsInput.placeholder = "例如: 80,443 或 1-1000";
361
+ portsInput.parentElement.classList.remove('disabled');
362
+ }
363
+ });
364
+
365
+ // 为选项添加动画效果
366
+ const scanTypeRadios = document.querySelectorAll('input[name="scan_type"]');
367
+ const scanSpeedRadios = document.querySelectorAll('input[name="scan_speed"]');
368
+
369
+ function addRadioAnimations(radioButtons) {
370
+ // 清除所有选中样式的函数
371
+ function clearSelectionStyles(name) {
372
+ document.querySelectorAll(`input[name="${name}"]`).forEach(radio => {
373
+ const item = radio.closest('.option-item');
374
+ if (item) {
375
+ item.classList.remove('option-selected');
376
+ }
377
+ });
378
+ }
379
+
380
+ radioButtons.forEach(radio => {
381
+ radio.addEventListener('change', function() {
382
+ // 清除同组中所有单选按钮的选中样式
383
+ clearSelectionStyles(this.name);
384
+
385
+ if (this.checked) {
386
+ const optionItem = this.closest('.option-item');
387
+
388
+ // 添加选中样式
389
+ optionItem.classList.add('option-selected');
390
+
391
+ // 添加一个简单的选中动画
392
+ const ripple = document.createElement('span');
393
+ ripple.classList.add('option-ripple');
394
+ optionItem.appendChild(ripple);
395
+
396
+ setTimeout(() => {
397
+ ripple.remove();
398
+ }, 500);
399
+ }
400
+ });
401
+
402
+ // 初始化选中状态
403
+ if (radio.checked) {
404
+ radio.closest('.option-item').classList.add('option-selected');
405
+ }
406
+ });
407
+ }
408
+
409
+ // 初始化单选按钮动画
410
+ addRadioAnimations(scanTypeRadios);
411
+ addRadioAnimations(scanSpeedRadios);
412
+
413
+ // 表单验证
414
+ target.addEventListener('input', validateForm);
415
+ portsInput.addEventListener('input', validateForm);
416
+ scanAllPortsCheckbox.addEventListener('change', validateForm);
417
+
418
+ function validateForm() {
419
+ const targetValue = target.value.trim();
420
+ const isValid = targetValue.length > 0;
421
+
422
+ if (isValid) {
423
+ scanButton.disabled = false;
424
+ target.classList.remove('input-error');
425
+ } else {
426
+ scanButton.disabled = true;
427
+ if (targetValue === '' && target.classList.contains('was-validated')) {
428
+ target.classList.add('input-error');
429
+ }
430
+ }
431
+ }
432
+
433
+ target.addEventListener('blur', function() {
434
+ this.classList.add('was-validated');
435
+ validateForm();
436
+ });
437
+
438
+ // 表单提交处理
439
+ scanForm.addEventListener('submit', function(e) {
440
+ e.preventDefault();
441
+
442
+ // 检查WebSocket连接
443
+ if (!socket || !socket.connected) {
444
+ showError('未能连接到扫描服务器,请刷新页面重试');
445
+ return;
446
+ }
447
+
448
+ // 添加表单提交动画
449
+ scanButton.innerHTML = '<div class="btn-spinner"></div> 扫描中...';
450
+
451
+ // 收集表单数据
452
+ const targetValue = target.value.trim();
453
+ const ports = portsInput.value.trim();
454
+ const scanAllPorts = scanAllPortsCheckbox.checked;
455
+ const threadCount = parseInt(parallelTasks.value) || DEFAULT_THREADS;
456
+
457
+ // 获取选中的扫描类型和速度
458
+ const selectedScanType = document.querySelector('input[name="scan_type"]:checked')?.value || "-sS";
459
+ const selectedScanSpeed = document.querySelector('input[name="scan_speed"]:checked')?.value || "-T3";
460
+ const selectedOptions = [selectedScanType, selectedScanSpeed];
461
+
462
+ // 验证目标
463
+ if (!targetValue) {
464
+ showError('请输入有效的目标地址');
465
+ resetScanButton();
466
+ return;
467
+ }
468
+
469
+ // 显示加载状态与取消按钮
470
+ showLoading(scanAllPorts);
471
+ cancelButton.style.display = 'inline-block';
472
+
473
+ // 重置进度和状态
474
+ updateProgress(0);
475
+ completedTasks = 0;
476
+ totalTasks = 0;
477
+ completedTasksCount.textContent = "0";
478
+ totalTasksCount.textContent = "0";
479
+ tasksGrid.innerHTML = '';
480
+ liveOutput.textContent = '等待扫描开始...\n';
481
+
482
+ // 准备请求数据
483
+ const requestData = {
484
+ target: targetValue,
485
+ ports: ports,
486
+ options: selectedOptions,
487
+ scan_all_ports: scanAllPorts,
488
+ parallel_tasks: threadCount
489
+ };
490
+
491
+ // 记住最近扫描的目标 (本地存储)
492
+ saveRecentScan(targetValue);
493
+
494
+ // 使用WebSocket发送扫描请求
495
+ socket.emit('start_scan', requestData);
496
+ });
497
+
498
+ // 更新进度条
499
+ function updateProgress(percentage) {
500
+ progressFill.style.width = `${percentage}%`;
501
+ progressText.textContent = `${percentage}%`;
502
+ }
503
+
504
+ // 缓慢增加进度,模拟扫描进展
505
+ function increaseProgress() {
506
+ if (totalTasks === 0) return; // 防止除以零
507
+
508
+ const currentCompleted = completedTasks;
509
+ const taskPercentage = (currentCompleted / totalTasks) * 100;
510
+
511
+ // 在任务完成百分��和当前进度条之间找一个中间值
512
+ const currentWidth = parseFloat(progressFill.style.width) || 0;
513
+
514
+ // 如果实际完成的任务百分比大于进度条,直接更新到任务百分比
515
+ if (taskPercentage > currentWidth) {
516
+ updateProgress(Math.round(taskPercentage));
517
+ return;
518
+ }
519
+
520
+ // 否则微微增加当前进度
521
+ let increment;
522
+ if (currentWidth < 30) {
523
+ increment = 0.5;
524
+ } else if (currentWidth < 60) {
525
+ increment = 0.3;
526
+ } else if (currentWidth < 80) {
527
+ increment = 0.2;
528
+ } else if (currentWidth < 90) {
529
+ increment = 0.1;
530
+ } else {
531
+ increment = 0.05;
532
+ }
533
+
534
+ // 不要超过99%,留给最终完成状态
535
+ const newWidth = Math.min(99, currentWidth + increment);
536
+ updateProgress(Math.round(newWidth));
537
+ }
538
+
539
+ // 更新实时输出
540
+ function updateLiveOutput(text) {
541
+ // 将新文本添加到当前内容
542
+ liveOutput.textContent += text;
543
+
544
+ // 自动滚动到底部
545
+ liveOutput.scrollTop = liveOutput.scrollHeight;
546
+ }
547
+
548
+ function resetScanButton() {
549
+ scanButton.innerHTML = '<i class="fas fa-play"></i> 开始扫描';
550
+ cancelButton.style.display = 'none';
551
+ }
552
+
553
+ // 取消扫描
554
+ cancelButton.addEventListener('click', function() {
555
+ if (currentScanId && socket && socket.connected) {
556
+ socket.emit('cancel_scan', { scan_id: currentScanId });
557
+ }
558
+ });
559
+
560
+ // 保存最近扫描的目标
561
+ function saveRecentScan(targetValue) {
562
+ let recentScans = JSON.parse(localStorage.getItem('recentScans') || '[]');
563
+ // 避免重复
564
+ recentScans = recentScans.filter(scan => scan !== targetValue);
565
+ recentScans.unshift(targetValue); // 添加到开头
566
+ // 最多保存5个
567
+ recentScans = recentScans.slice(0, 5);
568
+ localStorage.setItem('recentScans', JSON.stringify(recentScans));
569
+ }
570
+
571
+ // 清除结果按钮
572
+ clearButton.addEventListener('click', function() {
573
+ hideResults();
574
+ hideError();
575
+
576
+ // 添加清除动画效果
577
+ this.classList.add('btn-active');
578
+ setTimeout(() => {
579
+ this.classList.remove('btn-active');
580
+ }, 300);
581
+ });
582
+
583
+ // 展示信息提示
584
+ function showInfo(message) {
585
+ // 创建一个临时消息提示
586
+ const infoToast = document.createElement('div');
587
+ infoToast.className = 'info-toast';
588
+ infoToast.innerHTML = `<i class="fas fa-info-circle"></i> ${message}`;
589
+ document.body.appendChild(infoToast);
590
+
591
+ // 显示动画
592
+ setTimeout(() => {
593
+ infoToast.classList.add('show');
594
+ }, 10);
595
+
596
+ // 自动消失
597
+ setTimeout(() => {
598
+ infoToast.classList.remove('show');
599
+ setTimeout(() => {
600
+ document.body.removeChild(infoToast);
601
+ }, 300);
602
+ }, 3000);
603
+ }
604
+
605
+ // 展示加载状态
606
+ function showLoading(isAllPorts) {
607
+ hideResults();
608
+ hideError();
609
+ loading.style.display = 'block';
610
+ scanButton.disabled = true;
611
+
612
+ // 淡入动画
613
+ loading.style.opacity = 0;
614
+ setTimeout(() => {
615
+ loading.style.opacity = 1;
616
+ }, 10);
617
+
618
+ // 如果是全端口扫描,显示额外警告
619
+ if (isAllPorts) {
620
+ scanWarning.style.display = 'block';
621
+ } else {
622
+ scanWarning.style.display = 'none';
623
+ }
624
+ }
625
+
626
+ // 隐藏加载状态
627
+ function hideLoading() {
628
+ loading.style.opacity = 0;
629
+ setTimeout(() => {
630
+ loading.style.display = 'none';
631
+ }, 300);
632
+ scanButton.disabled = false;
633
+ }
634
+
635
+ // 展示结果
636
+ function showResults(resultText) {
637
+ results.style.display = 'block';
638
+ resultsContent.textContent = resultText;
639
+
640
+ // 淡入动画
641
+ results.style.opacity = 0;
642
+ setTimeout(() => {
643
+ results.style.opacity = 1;
644
+ }, 10);
645
+
646
+ // 为结果添加语法高亮
647
+ highlightScanResults();
648
+
649
+ // 滚动到结果区域
650
+ results.scrollIntoView({ behavior: 'smooth', block: 'start' });
651
+ }
652
+
653
+ // 隐藏结果
654
+ function hideResults() {
655
+ if (results.style.display !== 'none') {
656
+ results.style.opacity = 0;
657
+ setTimeout(() => {
658
+ results.style.display = 'none';
659
+ }, 300);
660
+ }
661
+ }
662
+
663
+ // 展示错误
664
+ function showError(message) {
665
+ error.style.display = 'block';
666
+ errorMessage.textContent = message;
667
+
668
+ // 淡入动画
669
+ error.style.opacity = 0;
670
+ setTimeout(() => {
671
+ error.style.opacity = 1;
672
+ }, 10);
673
+
674
+ // 滚动到错误区域
675
+ error.scrollIntoView({ behavior: 'smooth', block: 'start' });
676
+ }
677
+
678
+ // 隐藏错误
679
+ function hideError() {
680
+ if (error.style.display !== 'none') {
681
+ error.style.opacity = 0;
682
+ setTimeout(() => {
683
+ error.style.display = 'none';
684
+ }, 300);
685
+ }
686
+ }
687
+
688
+ // 简单的结果高亮
689
+ function highlightScanResults() {
690
+ const content = resultsContent.textContent;
691
+ let highlighted = content;
692
+
693
+ // 高亮扫描头部
694
+ highlighted = highlighted.replace(/(Starting Nmap.*?)(?=\n)/g,
695
+ '<span class="hl-header">$1</span>');
696
+
697
+ // 高亮端口状态表头
698
+ highlighted = highlighted.replace(/(PORT\s+STATE\s+SERVICE)/g,
699
+ '<span class="hl-table-header">$1</span>');
700
+
701
+ // 高亮目标标记
702
+ highlighted = highlighted.replace(/(目标: .*?)(?=\n)/g,
703
+ '<span class="hl-target">$1</span>');
704
+
705
+ // 高亮端口状态
706
+ highlighted = highlighted.replace(/(\d+\/\w+)\s+(open|closed|filtered)\s+(.*?)(?=\n|$)/g, function(match, port, state, service) {
707
+ let stateClass = 'hl-state-' + state;
708
+ return `<span class="hl-port">${port}</span> <span class="${stateClass}">${state}</span> <span class="hl-service">${service}</span>`;
709
+ });
710
+
711
+ // 高亮IP地址
712
+ highlighted = highlighted.replace(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/g,
713
+ '<span class="hl-ip">$1</span>');
714
+
715
+ // 高亮域名
716
+ highlighted = highlighted.replace(/([a-zA-Z0-9][-a-zA-Z0-9]*\.)+[a-zA-Z0-9][-a-zA-Z0-9]*/g, function(match) {
717
+ // 避免重复高亮已经处理过的元素
718
+ if (match.includes('<span')) return match;
719
+ return `<span class="hl-domain">${match}</span>`;
720
+ });
721
+
722
+ // 高亮服务版本信息
723
+ highlighted = highlighted.replace(/(Running|Service Info):(.*?)(?=\n|$)/g,
724
+ '<span class="hl-service-label">$1:</span><span class="hl-service-info">$2</span>');
725
+
726
+ // 高亮总结信息
727
+ highlighted = highlighted.replace(/(Nmap 多线程扫描完成:.*?)(?=$)/g,
728
+ '<span class="hl-summary">$1</span>');
729
+
730
+ if (highlighted !== content) {
731
+ resultsContent.innerHTML = highlighted;
732
+ }
733
+ }
734
+
735
+ // 检查是否有之前的扫描
736
+ function checkPreviousScan() {
737
+ const savedScanId = sessionStorage.getItem('currentScanId');
738
+ if (savedScanId) {
739
+ currentScanId = savedScanId;
740
+ showLoading(false);
741
+ updateLiveOutput('正在恢复之前的扫描状态...\n');
742
+
743
+ // 当WebSocket连接建立后会自动加入此房间
744
+ }
745
+ }
746
+
747
+ // 初始化
748
+ function init() {
749
+ initWebSocket();
750
+ initThreadControl();
751
+ validateForm();
752
+ checkPreviousScan();
753
+ }
754
+
755
+ // 启动初始化
756
+ init();
757
+ });
app/templates/index.html ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>在线Nmap端口扫描</title>
7
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
8
+ <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
9
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
10
+ <script src="https://cdn.socket.io/4.4.1/socket.io.min.js"></script>
11
+ </head>
12
+ <body>
13
+ <div class="main-wrapper">
14
+ <div class="container">
15
+ <header>
16
+ <div class="logo-area">
17
+ <i class="fas fa-network-wired logo-icon"></i>
18
+ <h1>在线Nmap端口扫描</h1>
19
+ </div>
20
+ <p class="subtitle">安全、高效、专业的网络扫描服务</p>
21
+ </header>
22
+
23
+ <div class="card scan-form-card">
24
+ <div class="card-header">
25
+ <i class="fas fa-search"></i> 扫描设置
26
+ </div>
27
+ <div class="card-body">
28
+ <form id="scanForm">
29
+ <div class="form-group">
30
+ <label for="target">
31
+ <i class="fas fa-crosshairs"></i> 目标
32
+ </label>
33
+ <div class="input-wrapper">
34
+ <input type="text" id="target" name="target" placeholder="IP地址、域名或IP段" required>
35
+ <div class="input-hint">支持单个IP、域名、CIDR格式的网段</div>
36
+ </div>
37
+ </div>
38
+
39
+ <div class="form-group">
40
+ <label for="ports">
41
+ <i class="fas fa-plug"></i> 端口
42
+ </label>
43
+ <div class="input-wrapper">
44
+ <input type="text" id="ports" name="ports" placeholder="例如: 80,443 或 1-1000">
45
+ <div class="input-hint">指定端口或端口范围,留空则使用默认端口</div>
46
+
47
+ <div class="port-option">
48
+ <input type="checkbox" id="scanAllPorts" name="scanAllPorts">
49
+ <label for="scanAllPorts">
50
+ <i class="fas fa-exclamation-triangle"></i> 扫描全部端口 (1-65535)
51
+ </label>
52
+ <div class="option-hint">全端口扫描耗时较长,请耐心等待</div>
53
+ </div>
54
+ </div>
55
+ </div>
56
+
57
+ <div class="form-group">
58
+ <label for="parallelTasks">
59
+ <i class="fas fa-microchip"></i> 线程数
60
+ </label>
61
+ <div class="threads-container">
62
+ <div class="threads-slider-container">
63
+ <input type="range" id="parallelTasksSlider" name="parallelTasksSlider" min="4" max="16" value="8">
64
+ <div class="threads-display">
65
+ <input type="number" id="parallelTasks" name="parallelTasks" min="4" max="16" value="8">
66
+ <span>线程</span>
67
+ </div>
68
+ </div>
69
+ <div class="input-hint threads-hint">
70
+ <i class="fas fa-info-circle"></i> 更多线程可以加快扫描速度,但会消耗更多系统资源
71
+ </div>
72
+ <div class="threads-modes">
73
+ <div class="thread-mode" data-value="4">
74
+ <div class="mode-icon"><i class="fas fa-balance-scale"></i></div>
75
+ <div class="mode-label">标准</div>
76
+ </div>
77
+ <div class="thread-mode" data-value="8">
78
+ <div class="mode-icon"><i class="fas fa-tachometer-alt"></i></div>
79
+ <div class="mode-label">快速</div>
80
+ </div>
81
+ <div class="thread-mode" data-value="12">
82
+ <div class="mode-icon"><i class="fas fa-bolt"></i></div>
83
+ <div class="mode-label">��速</div>
84
+ </div>
85
+ <div class="thread-mode" data-value="16">
86
+ <div class="mode-icon"><i class="fas fa-rocket"></i></div>
87
+ <div class="mode-label">极速</div>
88
+ </div>
89
+ </div>
90
+ </div>
91
+ </div>
92
+
93
+ <div class="form-group">
94
+ <label>
95
+ <i class="fas fa-sliders-h"></i> 扫描类型
96
+ </label>
97
+ <div class="options-container">
98
+ <div class="option-item">
99
+ <input type="radio" id="-sS" name="scan_type" value="-sS" checked>
100
+ <label for="-sS">
101
+ <span class="option-code">-sS</span>
102
+ <span class="option-desc">SYN扫描</span>
103
+ <span class="option-detail">半开放扫描,速度快</span>
104
+ </label>
105
+ </div>
106
+ <div class="option-item">
107
+ <input type="radio" id="-sT" name="scan_type" value="-sT">
108
+ <label for="-sT">
109
+ <span class="option-code">-sT</span>
110
+ <span class="option-desc">TCP连接扫描</span>
111
+ <span class="option-detail">完全建立连接</span>
112
+ </label>
113
+ </div>
114
+ <div class="option-item">
115
+ <input type="radio" id="-sU" name="scan_type" value="-sU">
116
+ <label for="-sU">
117
+ <span class="option-code">-sU</span>
118
+ <span class="option-desc">UDP扫描</span>
119
+ <span class="option-detail">检测UDP服务</span>
120
+ </label>
121
+ </div>
122
+ <div class="option-item">
123
+ <input type="radio" id="-sV" name="scan_type" value="-sV">
124
+ <label for="-sV">
125
+ <span class="option-code">-sV</span>
126
+ <span class="option-desc">服务版本检测</span>
127
+ <span class="option-detail">识别服务信息</span>
128
+ </label>
129
+ </div>
130
+ <div class="option-item">
131
+ <input type="radio" id="-O" name="scan_type" value="-O">
132
+ <label for="-O">
133
+ <span class="option-code">-O</span>
134
+ <span class="option-desc">操作系统检测</span>
135
+ <span class="option-detail">识别系统类型</span>
136
+ </label>
137
+ </div>
138
+ <div class="option-item">
139
+ <input type="radio" id="-A" name="scan_type" value="-A">
140
+ <label for="-A">
141
+ <span class="option-code">-A</span>
142
+ <span class="option-desc">综合扫描</span>
143
+ <span class="option-detail">多种检测方式</span>
144
+ </label>
145
+ </div>
146
+ </div>
147
+ </div>
148
+
149
+ <div class="form-group">
150
+ <label>
151
+ <i class="fas fa-tachometer-alt"></i> 扫描速度
152
+ </label>
153
+ <div class="options-container speed-options">
154
+ <div class="option-item">
155
+ <input type="radio" id="-T0" name="scan_speed" value="-T0">
156
+ <label for="-T0">
157
+ <span class="option-code">-T0</span>
158
+ <span class="option-desc">偷偷摸摸</span>
159
+ <span class="option-detail">高度隐蔽</span>
160
+ </label>
161
+ </div>
162
+ <div class="option-item">
163
+ <input type="radio" id="-T1" name="scan_speed" value="-T1">
164
+ <label for="-T1">
165
+ <span class="option-code">-T1</span>
166
+ <span class="option-desc">鬼鬼祟祟</span>
167
+ <span class="option-detail">较为隐蔽</span>
168
+ </label>
169
+ </div>
170
+ <div class="option-item">
171
+ <input type="radio" id="-T2" name="scan_speed" value="-T2">
172
+ <label for="-T2">
173
+ <span class="option-code">-T2</span>
174
+ <span class="option-desc">文明礼貌</span>
175
+ <span class="option-detail">低资源占用</span>
176
+ </label>
177
+ </div>
178
+ <div class="option-item">
179
+ <input type="radio" id="-T3" name="scan_speed" value="-T3" checked>
180
+ <label for="-T3">
181
+ <span class="option-code">-T3</span>
182
+ <span class="option-desc">正常</span>
183
+ <span class="option-detail">默认速度</span>
184
+ </label>
185
+ </div>
186
+ <div class="option-item">
187
+ <input type="radio" id="-T4" name="scan_speed" value="-T4">
188
+ <label for="-T4">
189
+ <span class="option-code">-T4</span>
190
+ <span class="option-desc">激进</span>
191
+ <span class="option-detail">快速扫描</span>
192
+ </label>
193
+ </div>
194
+ </div>
195
+ </div>
196
+
197
+ <div class="form-actions">
198
+ <button type="submit" id="scanButton" class="btn primary-btn">
199
+ <i class="fas fa-play"></i> 开始扫描
200
+ </button>
201
+ <button type="button" id="clearButton" class="btn secondary-btn">
202
+ <i class="fas fa-broom"></i> 清除结果
203
+ </button>
204
+ </div>
205
+ </form>
206
+ </div>
207
+ </div>
208
+
209
+ <div class="results-container" id="resultsContainer">
210
+ <div class="loading" id="loading" style="display: none;">
211
+ <div class="spinner-container">
212
+ <div class="spinner"></div>
213
+ </div>
214
+ <h3><i class="fas fa-hourglass-half"></i> 正在扫描中</h3>
215
+ <div class="thread-count-display">
216
+ <i class="fas fa-microchip"></i> <span id="threadsInUse">8</span> 个线程正在工作中
217
+ </div>
218
+ <p>请耐心等待扫描完成...</p>
219
+ <p class="scan-warning" id="scanWarning" style="display: none;">
220
+ <i class="fas fa-exclamation-circle"></i> 全端口扫描可能需要较长时间,请耐心等待
221
+ </p>
222
+
223
+ <div class="tasks-overview" id="tasksOverview">
224
+ <h4><i class="fas fa-tasks"></i> 子任务状态</h4>
225
+ <div class="tasks-grid" id="tasksGrid"></div>
226
+ </div>
227
+
228
+ </div>
229
+
230
+ <div class="card results-card" id="results" style="display: none;">
231
+ <div class="card-header">
232
+ <i class="fas fa-clipboard-list"></i> 扫描结果
233
+ </div>
234
+ <div class="card-body">
235
+ <div class="results-content">
236
+ <pre id="resultsContent"></pre>
237
+ </div>
238
+ </div>
239
+ </div>
240
+
241
+ <div class="card error-card" id="error" style="display: none;">
242
+ <div class="card-header">
243
+ <i class="fas fa-exclamation-triangle"></i> 扫描错误
244
+ </div>
245
+ <div class="card-body">
246
+ <p id="errorMessage"></p>
247
+ </div>
248
+ </div>
249
+ </div>
250
+
251
+ <footer>
252
+ <div class="footer-content">
253
+ <p><i class="fas fa-shield-alt"></i> 免责声明: 本工具仅用于授权的安全测试。未经授权的网络扫描可能违反相关法律法规。</p>
254
+ </div>
255
+ </footer>
256
+ </div>
257
+ </div>
258
+
259
+ <div id="socketStatus" class="socket-status"></div>
260
+
261
+ <script src="{{ url_for('static', filename='js/script.js') }}"></script>
262
+ </body>
263
+ </html>
docker-compose.yml ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3'
2
+
3
+ services:
4
+ nmap-web:
5
+ build: .
6
+ container_name: nmap-online
7
+ ports:
8
+ - "8080:5000"
9
+ restart: unless-stopped
10
+ deploy:
11
+ resources:
12
+ limits:
13
+ cpus: '8'
14
+ memory: 4G
15
+ reservations:
16
+ cpus: '2'
17
+ memory: 1G
18
+ ulimits:
19
+ nproc: 65535
20
+ nofile:
21
+ soft: 20000
22
+ hard: 40000
23
+ environment:
24
+ - PYTHONUNBUFFERED=1
25
+ - GUNICORN_WORKERS=2
26
+ - GUNICORN_THREADS=4
27
+ volumes:
28
+ - ./app:/app/app
29
+
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ Flask==2.0.1
2
+ gunicorn==20.1.0
3
+ flask-socketio==5.1.1
4
+ gevent==21.8.0
5
+ gevent-websocket==0.10.1
6
+ ipaddress==1.0.23
7
+ Werkzeug==2.0.1
8
+ zope.event==4.5.0
9
+ zope.interface==5.4.0