#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 宇航百川通讯录 — 安全后端 API 服务 ===================================== 功能: - 静态文件服务(HTML/CSS/JS/图片) - JSON REST API 提供通讯录数据(需鉴权) - CORS 白名单限制 - 请求频率限制(反爬虫) - 安全响应头注入 - 敏感数据文件访问阻断 部署:python server.py --port 80 """ import os import sys import json import time import hashlib import hmac import threading import argparse import urllib.parse from http.server import HTTPServer, SimpleHTTPRequestHandler from collections import defaultdict from datetime import datetime, timedelta # ======================== 配置 ======================== class Config: # ---- 服务配置 ---- HOST = "0.0.0.0" PORT = 80 # ---- API 鉴权密钥池(支持多个密钥轮换)---- # 使用 HMAC-SHA256 签名方式:客户端用密钥对时间戳签名 API_SECRETS = [ "yhbc-api-key-2026-internal-secure", ] # 签名有效窗口(秒),防止重放攻击 SIGNATURE_WINDOW = 300 # ---- CORS 白名单 ---- # 仅允许这些来源访问 API(空列表表示仅允许同源) CORS_ORIGINS = [ "http://yhbc.com.cn", "https://yhbc.com.cn", "http://127.0.0.1", "http://localhost", ] # ---- 速率限制(每 IP)---- RATE_LIMIT_REQUESTS = 60 # 令牌桶容量 RATE_LIMIT_WINDOW = 60 # 时间窗口(秒) # ---- 静态资源目录 ---- STATIC_DIR = os.path.dirname(os.path.abspath(__file__)) # ---- 阻止直接访问的数据文件 ---- BLOCKED_PATHS = [ "/data.js", "/data.json", "/server.py", "/_gen_json.py", "/requirements.txt", "/.git", "/pyserver.log", "/pyserver.err", ] # ======================== 速率限制器 ======================== class RateLimiter: """滑动窗口 + 令牌桶混合限流器""" def __init__(self, max_requests, window_seconds): self.max_requests = max_requests self.window = window_seconds self._buckets = defaultdict(list) self._lock = threading.Lock() def allow(self, client_ip): now = time.time() with self._lock: bucket = self._buckets[client_ip] # 清理过期记录 cutoff = now - self.window while bucket and bucket[0] < cutoff: bucket.pop(0) if len(bucket) >= self.max_requests: return False bucket.append(now) return True def remaining(self, client_ip): with self._lock: bucket = self._buckets[client_ip] cutoff = time.time() - self.window while bucket and bucket[0] < cutoff: bucket.pop(0) return max(0, self.max_requests - len(bucket)) # ======================== 数据处理 ======================== class DataService: """通讯录数据服务(单例)""" _instance = None _members = None _departments = None _mtime = 0 @classmethod def instance(cls): if cls._instance is None: cls._instance = cls() return cls._instance def __init__(self): self._load() def _load(self): data_path = os.path.join(Config.STATIC_DIR, "data.json") try: mtime = os.path.getmtime(data_path) if mtime == self._mtime and self._members is not None: return self._mtime = mtime with open(data_path, "r", encoding="utf-8") as f: self._members = json.load(f) # 预构建部门索引 depts = defaultdict(list) for m in self._members: depts[m.get("department", "未知")].append(m) self._departments = dict(depts) print(f"[DataService] 已加载 {len(self._members)} 条成员数据," f"{len(self._departments)} 个部门") except FileNotFoundError: print("[DataService] 警告: data.json 不存在") self._members = [] self._departments = {} def get_all(self): self._load() return self._members def get_by_id(self, member_id): self._load() for m in self._members: if m.get("id") == member_id: return m return None def get_departments(self): self._load() return list(self._departments.keys()) def get_by_department(self, dept_name): self._load() return self._departments.get(dept_name, []) def get_department_counts(self): self._load() return {k: len(v) for k, v in self._departments.items()} def get_total_count(self): self._load() return len(self._members) # ======================== HTTP 请求处理器 ======================== class SecureHandler(SimpleHTTPRequestHandler): """自定义 HTTP 处理器(安全增强版)""" rate_limiter = RateLimiter( Config.RATE_LIMIT_REQUESTS, Config.RATE_LIMIT_WINDOW ) def __init__(self, *args, **kwargs): # 设置静态文件根目录 self.directory = Config.STATIC_DIR super().__init__(*args, directory=Config.STATIC_DIR) # ---- 日志抑制(不输出 favicon 等静态资源日志)---- def log_message(self, format, *args): # 仅记录 API 请求和错误 if "/api/" in (args[0] if args else "") or self.is_error: sys.stderr.write("[%s] %s - %s\n" % (self.log_date_time_string(), self.address_string(), format % args)) @property def is_error(self): return hasattr(self, '_is_error') and self._is_error # ---- 安全响应头 ---- def send_security_headers(self): self.send_header("X-Content-Type-Options", "nosniff") self.send_header("X-Frame-Options", "DENY") self.send_header("X-XSS-Protection", "1; mode=block") self.send_header("Referrer-Policy", "strict-origin-when-cross-origin") self.send_header("Permissions-Policy", "geolocation=(), microphone=(), camera=()") # ---- CORS 处理 ---- def handle_cors(self): origin = self.headers.get("Origin", "") if not origin: return True # 同源请求,允许 if origin in Config.CORS_ORIGINS: self.send_header("Access-Control-Allow-Origin", origin) self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS") self.send_header("Access-Control-Allow-Headers", "Content-Type, X-API-Timestamp, X-API-Signature") self.send_header("Access-Control-Max-Age", "3600") return True return False # ---- API 鉴权验证 ---- def verify_api_auth(self): """ 验证请求鉴权,支持两种方式: 1. HMAC-SHA256 签名(需要 secure context / localhost) 2. Plain API Key(非安全上下文降级方案) """ # 方式1:HMAC 签名 timestamp_str = self.headers.get("X-API-Timestamp", "") signature = self.headers.get("X-API-Signature", "") if timestamp_str and signature: try: timestamp = int(timestamp_str) except ValueError: pass else: now = int(time.time()) if abs(now - timestamp) <= Config.SIGNATURE_WINDOW: for secret in Config.API_SECRETS: expected = hmac.new( secret.encode("utf-8"), timestamp_str.encode("utf-8"), hashlib.sha256 ).hexdigest() if hmac.compare_digest(expected, signature): return True # 方式2:Plain API Key(降级方案,非安全上下文使用) api_key = self.headers.get("X-API-Key", "") if api_key: for secret in Config.API_SECRETS: if hmac.compare_digest(secret.encode(), api_key.encode()): return True return False # ---- OPTIONS 预检请求 ---- def do_OPTIONS(self): self.send_response(204) origin = self.headers.get("Origin", "") if origin in Config.CORS_ORIGINS: self.send_header("Access-Control-Allow-Origin", origin) self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS") self.send_header("Access-Control-Allow-Headers", "Content-Type, X-API-Timestamp, X-API-Signature") self.send_header("Access-Control-Max-Age", "3600") self.send_security_headers() self.end_headers() # ---- GET 请求分发 ---- def do_GET(self): parsed = urllib.parse.urlparse(self.path) path = parsed.path # ---- 阻止访问敏感文件 ---- for blocked in Config.BLOCKED_PATHS: if path == blocked or path.startswith(blocked): self._is_error = True self.send_json_error(403, "Forbidden", "Access to this resource is restricted") return # ---- API 路由 ---- if path.startswith("/api/"): self.handle_api(path, parsed) return # ---- 速率限制(静态资源也轻度限制)---- client_ip = self.client_address[0] if not self.rate_limiter.allow(client_ip): self._is_error = True self.send_json_error(429, "Too Many Requests", "Rate limit exceeded. Please try again later.") return # ---- 静态文件服务 ---- # 强制 index.html 跳转 if path == "/": path = "/contacts.html" # 禁用目录列表 full_path = os.path.join(Config.STATIC_DIR, path.lstrip("/")) if os.path.isdir(full_path): self._is_error = True self.send_json_error(403, "Forbidden", "Directory listing disabled") return # 注入安全响应头 self._add_security_headers_to_static = True super().do_GET() # ---- 注入安全头到静态响应 ---- def end_headers(self): self.send_security_headers() super().end_headers() # ---- API 处理 ---- def handle_api(self, path, parsed): client_ip = self.client_address[0] # API 严格速率限制 if not self.rate_limiter.allow(client_ip): self._is_error = True self.send_json_error(429, "Too Many Requests", "API rate limit exceeded") return # CORS 检查 if not self.handle_cors(): self._is_error = True self.send_json_error(403, "Forbidden", "Cross-origin requests not allowed") return # 鉴权验证 if not self.verify_api_auth(): self._is_error = True self.send_json_error(401, "Unauthorized", "Invalid or missing API credentials") return # 路由分发 qs = urllib.parse.parse_qs(parsed.query) api_path = path[len("/api"):] or "/" if api_path == "/contacts": self.api_get_contacts(qs) elif api_path == "/contacts/departments": self.api_get_departments() elif api_path == "/contacts/stats": self.api_get_stats() elif api_path.startswith("/contacts/department/"): dept_name = urllib.parse.unquote(api_path[len("/contacts/department/"):]) self.api_get_department_members(dept_name, qs) elif api_path.startswith("/contacts/"): try: member_id = int(api_path[len("/contacts/"):]) self.api_get_member(member_id) except ValueError: self.send_json_error(404, "Not Found", "Invalid API path") else: self.send_json_error(404, "Not Found", "Unknown API endpoint") # ---- API 端点实现 ---- def api_get_contacts(self, qs): """GET /api/contacts?dept=xxx&limit=50&offset=0""" ds = DataService.instance() members = ds.get_all() # 部门筛选 dept = qs.get("dept", [None])[0] if dept: members = [m for m in members if m.get("department") == urllib.parse.unquote(dept)] # 分页 try: offset = int(qs.get("offset", [0])[0]) limit = min(int(qs.get("limit", [100])[0]), 200) except (ValueError, IndexError): offset = 0 limit = 100 result = members[offset:offset + limit] self.send_json(200, { "success": True, "data": result, "total": len(members), "offset": offset, "limit": limit, "count": len(result) }) def api_get_member(self, member_id): """GET /api/contacts/{id}""" ds = DataService.instance() member = ds.get_by_id(member_id) if member is None: self.send_json_error(404, "Not Found", f"Member {member_id} not found") return self.send_json(200, {"success": True, "data": member}) def api_get_departments(self): """GET /api/contacts/departments""" ds = DataService.instance() counts = ds.get_department_counts() depts = [ {"name": k, "count": v} for k, v in counts.items() ] self.send_json(200, {"success": True, "data": depts}) def api_get_stats(self): """GET /api/contacts/stats""" ds = DataService.instance() total = ds.get_total_count() counts = ds.get_department_counts() dept_count = len(counts) # 有联系方式的人数 members = ds.get_all() with_phone = sum(1 for m in members if m.get("phone") and m["phone"].strip()) self.send_json(200, { "success": True, "data": { "total_members": total, "department_count": dept_count, "members_with_phone": with_phone, "departments": counts } }) def api_get_department_members(self, dept_name, qs): """GET /api/contacts/department/{name}""" ds = DataService.instance() members = ds.get_by_department(dept_name) if not members: self.send_json_error(404, "Not Found", f"Department '{dept_name}' not found") return self.send_json(200, {"success": True, "data": members}) # ---- JSON 响应工具 ---- def send_json(self, status_code, data): body = json.dumps(data, ensure_ascii=False).encode("utf-8") self.send_response(status_code) self.send_header("Content-Type", "application/json; charset=utf-8") self.send_security_headers() self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def send_json_error(self, status_code, error, message): self._is_error = True data = { "success": False, "error": error, "message": message } self.send_json(status_code, data) # ======================== 主入口 ======================== def main(): parser = argparse.ArgumentParser(description="宇航百川安全通讯录服务") parser.add_argument("--port", type=int, default=80, help="监听端口(默认 80)") parser.add_argument("--host", default="0.0.0.0", help="绑定地址(默认 0.0.0.0)") args = parser.parse_args() Config.PORT = args.port Config.HOST = args.host # 初始加载数据 DataService.instance() print("=" * 60) print(" 宇航百川通讯录 — 安全后端服务") print("=" * 60) print(f" 监听地址: http://{Config.HOST}:{Config.PORT}") print(f" API 端点: /api/contacts") print(f" CORS 白名单: {Config.CORS_ORIGINS}") print(f" 速率限制: {Config.RATE_LIMIT_REQUESTS}次/" f"{Config.RATE_LIMIT_WINDOW}秒/IP") print(f" 签名窗口: {Config.SIGNATURE_WINDOW}秒") print("=" * 60) server = HTTPServer((Config.HOST, Config.PORT), SecureHandler) try: server.serve_forever() except KeyboardInterrupt: print("\n服务器已停止") server.shutdown() if __name__ == "__main__": main()