mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-08-07 01:13:11 +02:00
refactor: companion FrameServer and related (substantive only, no Black)
Reapply refactor from ce8381a (replace monolithic FrameServer with thin pymc_core subclass, re-export constants, SQLite persistence hooks) while preserving pre-refactor whitespace where patch applied cleanly. Remaining files match refactor commit exactly. Diff vs ce8381a is whitespace-only. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,9 +1,5 @@
|
||||
from .jwt_handler import JWTHandler
|
||||
from .api_tokens import APITokenManager
|
||||
from .jwt_handler import JWTHandler
|
||||
from .middleware import require_auth
|
||||
|
||||
__all__ = [
|
||||
'JWTHandler',
|
||||
'APITokenManager',
|
||||
'require_auth'
|
||||
]
|
||||
__all__ = ["JWTHandler", "APITokenManager", "require_auth"]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import secrets
|
||||
import hmac
|
||||
import hashlib
|
||||
from typing import Optional, List, Dict
|
||||
import hmac
|
||||
import logging
|
||||
import secrets
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -11,18 +11,14 @@ class APITokenManager:
|
||||
def __init__(self, sqlite_handler, secret_key: str):
|
||||
|
||||
self.db = sqlite_handler
|
||||
self.secret_key = secret_key.encode('utf-8')
|
||||
|
||||
self.secret_key = secret_key.encode("utf-8")
|
||||
|
||||
def generate_api_token(self) -> str:
|
||||
return secrets.token_hex(32)
|
||||
|
||||
|
||||
def hash_token(self, token: str) -> str:
|
||||
return hmac.new(
|
||||
self.secret_key,
|
||||
token.encode('utf-8'),
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
return hmac.new(self.secret_key, token.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||
|
||||
def create_token(self, name: str) -> tuple[int, str]:
|
||||
plaintext_token = self.generate_api_token()
|
||||
token_hash = self.hash_token(plaintext_token)
|
||||
@@ -43,7 +39,6 @@ class APITokenManager:
|
||||
logger.info(f"Revoked API token ID {token_id}")
|
||||
|
||||
return deleted
|
||||
|
||||
|
||||
def list_tokens(self) -> List[Dict]:
|
||||
return self.db.list_api_tokens()
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
|
||||
import cherrypy
|
||||
|
||||
logger = logging.getLogger("HTTPServer")
|
||||
@@ -40,10 +41,10 @@ def check_auth():
|
||||
cherrypy.request.user = {
|
||||
"username": payload.get("sub"),
|
||||
"client_id": payload.get("client_id"),
|
||||
"auth_type": "jwt"
|
||||
"auth_type": "jwt",
|
||||
}
|
||||
return
|
||||
|
||||
|
||||
# Check for JWT token in query parameter (for EventSource/SSE)
|
||||
# EventSource doesn't support custom headers, so we use query param
|
||||
query_token = cherrypy.request.params.get("token")
|
||||
@@ -54,7 +55,7 @@ def check_auth():
|
||||
cherrypy.request.user = {
|
||||
"username": payload.get("sub"),
|
||||
"client_id": payload.get("client_id"),
|
||||
"auth_type": "jwt_query"
|
||||
"auth_type": "jwt_query",
|
||||
}
|
||||
# Remove token from params to avoid exposing it in logs
|
||||
del cherrypy.request.params["token"]
|
||||
@@ -69,15 +70,15 @@ def check_auth():
|
||||
cherrypy.request.user = {
|
||||
"token_id": token_info["id"],
|
||||
"token_name": token_info["name"],
|
||||
"auth_type": "api_token"
|
||||
"auth_type": "api_token",
|
||||
}
|
||||
return
|
||||
|
||||
|
||||
# No valid authentication found
|
||||
logger.warning(f"Unauthorized access attempt to {cherrypy.request.path_info}")
|
||||
raise cherrypy.HTTPError(401, "Unauthorized - Valid JWT or API token required")
|
||||
|
||||
|
||||
# Register the tool
|
||||
cherrypy.tools.require_auth = cherrypy.Tool('before_handler', check_auth)
|
||||
cherrypy.tools.require_auth = cherrypy.Tool("before_handler", check_auth)
|
||||
logger.info("CherryPy require_auth tool registered")
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import jwt
|
||||
import logging
|
||||
import time
|
||||
from typing import Dict, Optional
|
||||
import logging
|
||||
|
||||
import jwt
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class JWTHandler:
|
||||
def __init__(self, secret: str, expiry_minutes: int = 15):
|
||||
self.secret = secret
|
||||
@@ -14,21 +16,16 @@ class JWTHandler:
|
||||
|
||||
now = int(time.time())
|
||||
expiry = now + (self.expiry_minutes * 60)
|
||||
|
||||
payload = {
|
||||
'sub': username,
|
||||
'exp': expiry,
|
||||
'iat': now,
|
||||
'client_id': client_id
|
||||
}
|
||||
|
||||
token = jwt.encode(payload, self.secret, algorithm='HS256')
|
||||
|
||||
payload = {"sub": username, "exp": expiry, "iat": now, "client_id": client_id}
|
||||
|
||||
token = jwt.encode(payload, self.secret, algorithm="HS256")
|
||||
logger.info(f"Created JWT for user '{username}' with client_id '{client_id[:8]}...'")
|
||||
return token
|
||||
|
||||
|
||||
def verify_jwt(self, token: str) -> Optional[Dict]:
|
||||
try:
|
||||
payload = jwt.decode(token, self.secret, algorithms=['HS256'])
|
||||
payload = jwt.decode(token, self.secret, algorithms=["HS256"])
|
||||
return payload
|
||||
except jwt.ExpiredSignatureError:
|
||||
logger.warning("JWT token expired")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import cherrypy
|
||||
from functools import wraps
|
||||
import logging
|
||||
from functools import wraps
|
||||
|
||||
import cherrypy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -10,56 +11,56 @@ def require_auth(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
# Skip authentication for OPTIONS requests (CORS preflight)
|
||||
if cherrypy.request.method == 'OPTIONS':
|
||||
if cherrypy.request.method == "OPTIONS":
|
||||
return func(*args, **kwargs)
|
||||
|
||||
|
||||
# Get auth handlers from global cherrypy config (not app config)
|
||||
jwt_handler = cherrypy.config.get('jwt_handler')
|
||||
token_manager = cherrypy.config.get('token_manager')
|
||||
|
||||
jwt_handler = cherrypy.config.get("jwt_handler")
|
||||
token_manager = cherrypy.config.get("token_manager")
|
||||
|
||||
if not jwt_handler or not token_manager:
|
||||
logger.error("Auth handlers not configured")
|
||||
raise cherrypy.HTTPError(500, "Authentication not configured")
|
||||
|
||||
|
||||
# Try JWT authentication first
|
||||
auth_header = cherrypy.request.headers.get('Authorization', '')
|
||||
if auth_header.startswith('Bearer '):
|
||||
auth_header = cherrypy.request.headers.get("Authorization", "")
|
||||
if auth_header.startswith("Bearer "):
|
||||
token = auth_header[7:] # Remove 'Bearer ' prefix
|
||||
payload = jwt_handler.verify_jwt(token)
|
||||
|
||||
|
||||
if payload:
|
||||
# JWT is valid
|
||||
cherrypy.request.user = {
|
||||
'username': payload['sub'],
|
||||
'client_id': payload['client_id'],
|
||||
'auth_type': 'jwt'
|
||||
"username": payload["sub"],
|
||||
"client_id": payload["client_id"],
|
||||
"auth_type": "jwt",
|
||||
}
|
||||
return func(*args, **kwargs)
|
||||
else:
|
||||
logger.warning("Invalid or expired JWT token")
|
||||
|
||||
|
||||
# Try API token authentication
|
||||
api_key = cherrypy.request.headers.get('X-API-Key', '')
|
||||
api_key = cherrypy.request.headers.get("X-API-Key", "")
|
||||
if api_key:
|
||||
token_info = token_manager.verify_token(api_key)
|
||||
|
||||
|
||||
if token_info:
|
||||
# API token is valid
|
||||
cherrypy.request.user = {
|
||||
'username': 'api_token',
|
||||
'token_name': token_info['name'],
|
||||
'token_id': token_info['id'],
|
||||
'auth_type': 'api_token'
|
||||
"username": "api_token",
|
||||
"token_name": token_info["name"],
|
||||
"token_id": token_info["id"],
|
||||
"auth_type": "api_token",
|
||||
}
|
||||
return func(*args, **kwargs)
|
||||
else:
|
||||
logger.warning("Invalid API token")
|
||||
|
||||
|
||||
# No valid authentication found
|
||||
logger.warning(f"Unauthorized access attempt to {cherrypy.request.path_info}")
|
||||
|
||||
|
||||
cherrypy.response.status = 401
|
||||
cherrypy.response.headers['Content-Type'] = 'application/json'
|
||||
return {'success': False, 'error': 'Unauthorized - Valid JWT or API token required'}
|
||||
|
||||
return wrapper
|
||||
cherrypy.response.headers["Content-Type"] = "application/json"
|
||||
return {"success": False, "error": "Unauthorized - Valid JWT or API token required"}
|
||||
|
||||
return wrapper
|
||||
|
||||
Reference in New Issue
Block a user