#!/usr/bin/env python3 """ 服务发现 API 路由 - 扫描本机未注册的服务 方案C: 端口扫描 + 进程信息增强 """ import os import re import json import subprocess from flask import jsonify, request from utils import log_message from config import PROJECTS_FILE # 系统保留/常见基础服务端口,不纳入发现 SYSTEM_PORTS = { 22, # SSH 25, # SMTP 53, # DNS 80, # HTTP (nginx/apache 前端) 111, # portmapper 135, # RPC 137, # NetBIOS 138, # NetBIOS 139, # NetBIOS 443, # HTTPS 445, # SMB 587, # SMTP submission 631, # CUPS 993, # IMAPS 995, # POP3S 3306, # MySQL 3389, # RDP 5432, # PostgreSQL 6379, # Redis 8080, # 常见代理 8443, # 常见 HTTPS 代理 9090, # Prometheus 3000, # 常见开发端口 4200, # Angular dev 5000, # Flask dev 8000, # 常见开发 9000, # 常见开发 } def _get_listening_ports(): """获取所有TCP监听端口及进程信息""" services = [] try: result = subprocess.run( ['ss', '-tlnp'], capture_output=True, text=True, timeout=5 ) for line in result.stdout.split('\n'): line = line.strip() if not line or 'State' in line or 'Local' in line: continue # 解析: LISTEN 0 128 0.0.0.0:16022 0.0.0.0:* users:(("python3.12",pid=123,fd=3)) if '127.0.0.1' in line and '0.0.0.0' not in line.split()[3] and '*' not in line.split()[3]: # 跳过仅监听 localhost 的 pass # 提取端口和pid addr_part = line.split()[3] if len(line.split()) > 3 else '' port_match = re.search(r':(\d+)$', addr_part) if not port_match: continue port = int(port_match.group(1)) pid_match = re.search(r'pid=(\d+)', line) pid = int(pid_match.group(1)) if pid_match else None proc_match = re.search(r'users:\(\(\"([^\"]+)\"', line) proc_name = proc_match.group(1) if proc_match else '未知进程' services.append({ 'port': port, 'pid': pid, 'process': proc_name, }) except Exception as e: log_message(f"端口扫描失败: {e}") return services def _get_process_cmdline(pid): """读取进程命令行""" try: cmdline_path = f'/proc/{pid}/cmdline' if os.path.exists(cmdline_path): with open(cmdline_path, 'rb') as f: raw = f.read() # cmdline 以 \0 分隔 args = raw.split(b'\x00') # 解码,跳过空字符串 cmd = ' '.join(a.decode('utf-8', errors='replace') for a in args if a) return cmd except: pass return '' def _get_process_cwd(pid): """读取进程工作目录""" try: cwd_path = f'/proc/{pid}/cwd' if os.path.exists(cwd_path): return os.readlink(cwd_path) except: pass return '' def _guess_service_name(pid, proc_name, cmdline): """根据进程信息推测服务名""" # 已知框架特征 patterns = [ (r'python.*?app\.py', 'Python Web 服务'), (r'python.*?main\.py', 'Python 服务'), (r'python.*?server\.py', 'Python Server'), (r'node\s+.*?(server|app|index)\.js', 'Node.js 服务'), (r'npm\s+(start|run)', 'Node.js 服务'), (r'java\s+-jar\s+(\S+)', 'Java 服务'), (r'gunicorn', 'Gunicorn 服务'), (r'uvicorn', 'Uvicorn 服务'), (r'nginx', 'Nginx'), (r'php-fpm', 'PHP-FPM'), (r'mysqld', 'MySQL'), (r'redis-server', 'Redis'), (r'docker-proxy', 'Docker 代理'), (r'containerd', 'containerd'), ] for pattern, name in patterns: if re.search(pattern, cmdline, re.IGNORECASE): return name # 默认使用进程名 return proc_name def _guess_service_type(cmdline): """推测服务类型""" if re.search(r'(python|gunicorn|uvicorn|flask|django)', cmdline, re.IGNORECASE): return 'web' if re.search(r'(node|npm|next|express)', cmdline, re.IGNORECASE): return 'web' if re.search(r'(java|spring|tomcat)', cmdline, re.IGNORECASE): return 'web' if re.search(r'(mysqld|postgres|redis|mongo)', cmdline, re.IGNORECASE): return 'external' return 'external' def register_discovery_routes(app): """注册服务发现路由""" @app.route('/api/discovery/services') def api_discovery_services(): """扫描本机未注册的新服务""" # 1. 收集已知端口 known_ports = set() try: with open(PROJECTS_FILE, 'r', encoding='utf-8') as f: data = json.load(f) for proj in data.get('projects', []): for p in proj.get('ports', []): known_ports.add(p) for svc in data.get('external_services', []): for p in svc.get('ports', []): known_ports.add(p) except: pass # 2. 前端传入的额外排除端口(自定义服务等) exclude_str = request.args.get('exclude_ports', '') if exclude_str: try: for p in exclude_str.split(','): known_ports.add(int(p.strip())) except: pass # 3. 获取 Docker 容器端口 try: result = subprocess.run( ['docker', 'ps', '--format', '{{.Ports}}'], capture_output=True, text=True, timeout=5 ) for line in result.stdout.split('\n'): # 匹配 0.0.0.0:16001->7700/tcp 格式 for m in re.finditer(r':(\d+)->', line): known_ports.add(int(m.group(1))) except: pass # 3. 扫描所有监听端口 all_services = _get_listening_ports() # 4. 筛选未注册的 undiscovered = [] seen_ports = set() for svc in all_services: port = svc['port'] pid = svc['pid'] # 跳过已知/系统端口 if port in known_ports or port in SYSTEM_PORTS: continue if port in seen_ports: continue seen_ports.add(port) if pid: cmdline = _get_process_cmdline(pid) cwd = _get_process_cwd(pid) name = _guess_service_name(pid, svc['process'], cmdline) svc_type = _guess_service_type(cmdline) else: cmdline = '' cwd = '' name = svc['process'] svc_type = 'unknown' undiscovered.append({ 'port': port, 'pid': pid, 'process': svc['process'], 'cmdline': cmdline[:200] if cmdline else '', 'cwd': cwd, 'suggested_name': name, 'suggested_type': svc_type, }) # 按端口排序 undiscovered.sort(key=lambda x: x['port']) return jsonify({ 'services': undiscovered, 'count': len(undiscovered), 'known_ports': len(known_ports), })