重构:核心迁移至 oss/core + NBPF 多重签名加密 + NIR 编译器 + README 全面升级
- 核心功能从 store/ 迁移至 oss/core/ 框架层 - 实现 NBPF 包格式:多重签名(Ed25519+RSA-PSS+HMAC)+ 多重加密(AES-256-GCM) - 实现 NIR 编译器:基于 compile()+marshal 的跨平台中间表示 - 新增 nebula nbpf CLI 命令组(pack/unpack/verify/sign/keygen) - 新增 19 个 NBPF 测试用例,覆盖全链路 - 彻底重写 README,大型项目标准框架风格,所有图表使用 SVG - 更新 LICENSE 版权声明 - 清理旧版 store 插件目录(已迁移至 oss/core)
This commit is contained in:
0
oss/core/http_api/__init__.py
Normal file
0
oss/core/http_api/__init__.py
Normal file
113
oss/core/http_api/middleware.py
Normal file
113
oss/core/http_api/middleware.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""中间件链 - CORS/鉴权/日志/限流/CSRF/输入验证等"""
|
||||
import json
|
||||
import time
|
||||
import threading
|
||||
from collections import deque
|
||||
from typing import Callable, Optional, Any
|
||||
|
||||
from oss.config import get_config
|
||||
from oss.logger.logger import Log
|
||||
from .server import Request, Response
|
||||
from .rate_limiter import RateLimitMiddleware
|
||||
|
||||
|
||||
class Middleware:
|
||||
"""中间件基类"""
|
||||
def process(self, ctx: dict[str, Any], next_fn: Callable) -> Optional[Response]:
|
||||
return next_fn()
|
||||
|
||||
|
||||
class CorsMiddleware(Middleware):
|
||||
"""CORS 中间件"""
|
||||
def process(self, ctx: dict, next_fn: Callable) -> Optional[Response]:
|
||||
config = get_config()
|
||||
allowed_origins = config.get("CORS_ALLOWED_ORIGINS", ["http://localhost:3000", "http://127.0.0.1:3000"])
|
||||
|
||||
req = ctx.get("request")
|
||||
origin = req.headers.get("Origin", "") if req else ""
|
||||
|
||||
if not allowed_origins or not origin:
|
||||
return next_fn()
|
||||
|
||||
if origin in allowed_origins or "*" in allowed_origins:
|
||||
ctx["response_headers"] = {
|
||||
"Access-Control-Allow-Origin": origin,
|
||||
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
||||
"Access-Control-Allow-Credentials": "true",
|
||||
}
|
||||
|
||||
return next_fn()
|
||||
|
||||
|
||||
class AuthMiddleware(Middleware):
|
||||
"""鉴权中间件 - Bearer Token 认证"""
|
||||
_public_paths = {"/health", "/favicon.ico", "/api/status", "/api/health"}
|
||||
|
||||
def process(self, ctx: dict, next_fn: Callable) -> Optional[Response]:
|
||||
config = get_config()
|
||||
api_key = config.get("API_KEY")
|
||||
|
||||
if not api_key:
|
||||
return next_fn()
|
||||
|
||||
req = ctx.get("request")
|
||||
if req and req.path in self._public_paths:
|
||||
return next_fn()
|
||||
|
||||
if req and req.method == "OPTIONS":
|
||||
return next_fn()
|
||||
|
||||
auth_header = req.headers.get("Authorization", "") if req else ""
|
||||
token = auth_header.removeprefix("Bearer ").strip()
|
||||
|
||||
if token != api_key or not token:
|
||||
Log.warn("Core", f"鉴权失败: {req.method} {req.path}" if req else "鉴权失败")
|
||||
return Response(
|
||||
status=401,
|
||||
body=json.dumps({"error": "Unauthorized", "message": "需要有效的 API Key"}),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
return next_fn()
|
||||
|
||||
|
||||
class LoggerMiddleware(Middleware):
|
||||
"""日志中间件"""
|
||||
_silent_paths = {"/api/dashboard/stats", "/favicon.ico", "/health"}
|
||||
|
||||
def process(self, ctx: dict, next_fn: Callable) -> Optional[Response]:
|
||||
req = ctx.get("request")
|
||||
if req and req.path not in self._silent_paths:
|
||||
Log.info("Core", f"{req.method} {req.path}")
|
||||
return next_fn()
|
||||
|
||||
|
||||
class MiddlewareChain:
|
||||
"""中间件链"""
|
||||
|
||||
def __init__(self):
|
||||
self.middlewares: list[Middleware] = []
|
||||
self.add(CorsMiddleware())
|
||||
self.add(AuthMiddleware())
|
||||
self.add(LoggerMiddleware())
|
||||
self.add(RateLimitMiddleware())
|
||||
|
||||
def add(self, middleware: Middleware):
|
||||
self.middlewares.append(middleware)
|
||||
|
||||
def run(self, ctx: dict[str, Any]) -> Optional[Response]:
|
||||
idx = 0
|
||||
|
||||
def next_fn():
|
||||
nonlocal idx
|
||||
if idx < len(self.middlewares):
|
||||
mw = self.middlewares[idx]
|
||||
idx += 1
|
||||
return mw.process(ctx, next_fn)
|
||||
return None
|
||||
|
||||
resp = next_fn()
|
||||
response_headers = ctx.get("response_headers")
|
||||
if response_headers:
|
||||
ctx["_cors_headers"] = response_headers
|
||||
return resp
|
||||
138
oss/core/http_api/rate_limiter.py
Normal file
138
oss/core/http_api/rate_limiter.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""
|
||||
限流工具 - 令牌桶限流器
|
||||
"""
|
||||
import time
|
||||
import threading
|
||||
from typing import Dict, Callable, Optional
|
||||
from collections import defaultdict, deque
|
||||
|
||||
from oss.config import get_config
|
||||
from oss.core.http_api.server import Request, Response
|
||||
|
||||
|
||||
class RateLimiter:
|
||||
"""令牌桶限流器"""
|
||||
|
||||
def __init__(self, max_requests: int = 100, time_window: int = 60):
|
||||
self.max_requests = max_requests
|
||||
self.time_window = time_window
|
||||
self.requests: Dict[str, deque] = defaultdict(deque)
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def is_allowed(self, identifier: str) -> bool:
|
||||
"""检查是否允许请求"""
|
||||
with self.lock:
|
||||
now = time.time()
|
||||
request_times = self.requests[identifier]
|
||||
|
||||
# 清理过期的请求记录
|
||||
while request_times and request_times[0] <= now - self.time_window:
|
||||
request_times.popleft()
|
||||
|
||||
# 检查是否超过限制
|
||||
if len(request_times) >= self.max_requests:
|
||||
return False
|
||||
|
||||
# 记录当前请求
|
||||
request_times.append(now)
|
||||
return True
|
||||
|
||||
|
||||
class RateLimitMiddleware:
|
||||
"""限流中间件 - 防止DoS攻击"""
|
||||
def __init__(self):
|
||||
self.config = get_config()
|
||||
self.enabled = self.config.get("RATE_LIMIT_ENABLED", True)
|
||||
|
||||
# 不同端点的限流配置
|
||||
self.endpoint_limits = {
|
||||
"/api/dashboard/stats": {
|
||||
"max_requests": 10,
|
||||
"time_window": 60
|
||||
},
|
||||
}
|
||||
|
||||
# 全局限流配置
|
||||
self.global_limit = {
|
||||
"max_requests": self.config.get("RATE_LIMIT_MAX_REQUESTS", 100),
|
||||
"time_window": self.config.get("RATE_LIMIT_TIME_WINDOW", 60)
|
||||
}
|
||||
|
||||
# 请求记录
|
||||
self.requests = {}
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def _get_client_identifier(self, request: Request) -> str:
|
||||
"""获取客户端标识符"""
|
||||
ip = request.headers.get("X-Forwarded-For", request.headers.get("X-Real-IP", ""))
|
||||
if not ip:
|
||||
ip = request.headers.get("Remote-Addr", "unknown")
|
||||
|
||||
auth_header = request.headers.get("Authorization", "")
|
||||
if auth_header.startswith("Bearer "):
|
||||
return f"api_key:{auth_header[7:]}"
|
||||
|
||||
return f"ip:{ip}"
|
||||
|
||||
def _is_rate_limited(self, identifier: str, path: str) -> bool:
|
||||
"""检查是否被限流"""
|
||||
if not self.enabled:
|
||||
return False
|
||||
|
||||
now = time.time()
|
||||
limit_key = f"{identifier}:{path}"
|
||||
|
||||
# 获取端点特定的限制
|
||||
endpoint_limit = None
|
||||
for endpoint, config in self.endpoint_limits.items():
|
||||
if path.startswith(endpoint):
|
||||
endpoint_limit = config
|
||||
break
|
||||
|
||||
# 使用端点特定限制或全局限制
|
||||
limit = endpoint_limit or self.global_limit
|
||||
max_requests = limit["max_requests"]
|
||||
time_window = limit["time_window"]
|
||||
|
||||
with self.lock:
|
||||
if limit_key not in self.requests:
|
||||
self.requests[limit_key] = deque()
|
||||
|
||||
request_times = self.requests[limit_key]
|
||||
while request_times and request_times[0] <= now - time_window:
|
||||
request_times.popleft()
|
||||
|
||||
if len(request_times) >= max_requests:
|
||||
return True
|
||||
|
||||
request_times.append(now)
|
||||
return False
|
||||
|
||||
def _create_rate_limit_response(self) -> Response:
|
||||
"""创建限流响应"""
|
||||
return Response(
|
||||
status=429,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Retry-After": str(self.global_limit["time_window"]),
|
||||
"X-Rate-Limit-Limit": str(self.global_limit["max_requests"]),
|
||||
"X-Rate-Limit-Window": str(self.global_limit["time_window"]),
|
||||
},
|
||||
body='{"error": "Rate limit exceeded", "message": "请稍后再试"}'
|
||||
)
|
||||
|
||||
def process(self, ctx: dict, next_fn: Callable) -> Optional[Response]:
|
||||
"""处理限流逻辑"""
|
||||
if not self.enabled:
|
||||
return next_fn()
|
||||
|
||||
request = ctx.get("request")
|
||||
if not request:
|
||||
return next_fn()
|
||||
|
||||
identifier = self._get_client_identifier(request)
|
||||
|
||||
if self._is_rate_limited(identifier, request.path):
|
||||
return self._create_rate_limit_response()
|
||||
|
||||
return next_fn()
|
||||
41
oss/core/http_api/router.py
Normal file
41
oss/core/http_api/router.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""HTTP 路由 - 基于 oss/shared/router.py 的 BaseRouter"""
|
||||
import json
|
||||
from typing import Callable
|
||||
|
||||
from oss.shared.router import BaseRouter, BaseRoute, match_path, extract_path_params
|
||||
from .server import Request, Response
|
||||
|
||||
|
||||
class HttpRouter(BaseRouter):
|
||||
"""HTTP 路由"""
|
||||
|
||||
def add(self, method: str, path: str, handler: Callable):
|
||||
self.routes.append(BaseRoute(method, path, handler))
|
||||
|
||||
def handle(self, request: Request) -> Response:
|
||||
"""匹配路由并执行处理器"""
|
||||
for route in self.routes:
|
||||
if route.method == request.method and match_path(route.path, request.path):
|
||||
params = extract_path_params(route.path, request.path)
|
||||
try:
|
||||
result = route.handler(request, **params)
|
||||
if isinstance(result, Response):
|
||||
return result
|
||||
return Response(
|
||||
status=200,
|
||||
body=json.dumps(result) if not isinstance(result, str) else result,
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
except Exception as e:
|
||||
return Response(
|
||||
status=500,
|
||||
body=json.dumps({"error": "Internal Server Error", "message": str(e)}),
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
# 404 - 无匹配路由
|
||||
return Response(
|
||||
status=404,
|
||||
body=json.dumps({"error": "Not Found", "message": f"路由未找到: {request.method} {request.path}"}),
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
124
oss/core/http_api/server.py
Normal file
124
oss/core/http_api/server.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""HTTP 服务器核心"""
|
||||
import threading
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from typing import Any
|
||||
from oss.config import get_config
|
||||
from oss.logger.logger import Log
|
||||
|
||||
|
||||
class Request:
|
||||
"""请求对象"""
|
||||
def __init__(self, method, path, headers, body):
|
||||
self.method = method
|
||||
self.path = path
|
||||
self.headers = headers
|
||||
self.body = body
|
||||
|
||||
|
||||
class Response:
|
||||
"""响应对象"""
|
||||
def __init__(self, status=200, headers=None, body=""):
|
||||
self.status = status
|
||||
self.headers = headers or {}
|
||||
self.body = body
|
||||
|
||||
|
||||
class HttpServer:
|
||||
"""HTTP 服务器"""
|
||||
|
||||
def __init__(self, router, middleware, host=None, port=None):
|
||||
config = get_config()
|
||||
self.host = host or config.get("HOST", "127.0.0.1")
|
||||
self.port = port or config.get("HTTP_API_PORT", 8080)
|
||||
self.router = router
|
||||
self.middleware = middleware
|
||||
self._server = None
|
||||
self._thread = None
|
||||
|
||||
def start(self):
|
||||
"""启动服务器"""
|
||||
handler = self._create_handler()
|
||||
self._server = HTTPServer((self.host, self.port), handler)
|
||||
self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
|
||||
self._thread.start()
|
||||
Log.info("Core", f"HTTP 服务器启动: {self.host}:{self.port}")
|
||||
|
||||
def stop(self):
|
||||
"""停止服务器"""
|
||||
if self._server:
|
||||
self._server.shutdown()
|
||||
Log.info("Core", "HTTP 服务器已停止")
|
||||
|
||||
def _create_handler(self):
|
||||
"""创建请求处理器"""
|
||||
router = self.router
|
||||
middleware = self.middleware
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
self._handle("GET")
|
||||
|
||||
def do_POST(self):
|
||||
self._handle("POST")
|
||||
|
||||
def do_PUT(self):
|
||||
self._handle("PUT")
|
||||
|
||||
def do_DELETE(self):
|
||||
self._handle("DELETE")
|
||||
|
||||
def do_OPTIONS(self):
|
||||
"""处理 CORS 预检请求"""
|
||||
config = get_config()
|
||||
allowed_origins = config.get("CORS_ALLOWED_ORIGINS", ["http://localhost:3000", "http://127.0.0.1:3000"])
|
||||
origin = self.headers.get("Origin", "")
|
||||
|
||||
if origin in allowed_origins or "*" in allowed_origins:
|
||||
self.send_response(200)
|
||||
self.send_header("Access-Control-Allow-Origin", origin if origin else "*")
|
||||
self.send_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
self.send_header("Access-Control-Allow-Credentials", "true")
|
||||
else:
|
||||
self.send_response(204)
|
||||
self.end_headers()
|
||||
|
||||
def _handle(self, method):
|
||||
content_length = int(self.headers.get("Content-Length", 0))
|
||||
body = self.rfile.read(content_length) if content_length else b""
|
||||
|
||||
req = Request(
|
||||
method=method,
|
||||
path=self.path,
|
||||
headers=dict(self.headers),
|
||||
body=body.decode("utf-8")
|
||||
)
|
||||
|
||||
# 执行中间件
|
||||
ctx = {"request": req, "response": None}
|
||||
result = middleware.run(ctx)
|
||||
if result:
|
||||
self._send_response(result)
|
||||
return
|
||||
|
||||
# 路由匹配
|
||||
resp = router.handle(req)
|
||||
self._send_response(resp)
|
||||
|
||||
def _send_response(self, resp: Response):
|
||||
try:
|
||||
self.send_response(resp.status)
|
||||
for k, v in resp.headers.items():
|
||||
self.send_header(k, v)
|
||||
self.end_headers()
|
||||
if isinstance(resp.body, str):
|
||||
self.wfile.write(resp.body.encode("utf-8"))
|
||||
else:
|
||||
self.wfile.write(resp.body)
|
||||
except (BrokenPipeError, ConnectionAbortedError, ConnectionResetError):
|
||||
pass # 忽略客户端断开
|
||||
|
||||
def log_message(self, format, *args):
|
||||
Log.debug("Core", format % args)
|
||||
|
||||
return Handler
|
||||
Reference in New Issue
Block a user