chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:08:41 +08:00
commit bcc7968ff6
9988 changed files with 1016778 additions and 0 deletions
+344
View File
@@ -0,0 +1,344 @@
import base64
import hashlib
import hmac
import json
import secrets
import time
from typing import Optional
from fastapi import Request
from starlette.responses import Response
from utils.get_env import get_user_config_path_env, is_disable_auth_enabled
from utils.user_config_store import read_user_config_file, update_user_config_file
SESSION_COOKIE_NAME = "presenton_session"
PBKDF2_ITERATIONS = 200_000
SESSION_TTL_SECONDS = 60 * 60 * 24 * 30
AUTH_CONFIG_FIELDS = ("AUTH_USERNAME", "AUTH_PASSWORD_HASH", "AUTH_SECRET_KEY")
def _base64url_encode(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("utf-8")
def _base64url_decode(value: str) -> bytes:
padded = value + "=" * (-len(value) % 4)
return base64.urlsafe_b64decode(padded.encode("utf-8"))
def _load_user_config() -> dict:
user_config_path = get_user_config_path_env()
if not user_config_path:
return {}
try:
return read_user_config_file(user_config_path)
except Exception:
return {}
def _save_user_config(config: dict, removed_keys: tuple[str, ...] = ()) -> None:
user_config_path = get_user_config_path_env()
if not user_config_path:
raise ValueError("USER_CONFIG_PATH is not set")
auth_config = {
key: config[key]
for key in AUTH_CONFIG_FIELDS
if key in config
}
def merge_auth_config(existing: dict) -> dict:
existing.update(auth_config)
for key in removed_keys:
existing.pop(key, None)
return existing
update_user_config_file(user_config_path, merge_auth_config)
def _hash_password(password: str, salt: bytes) -> bytes:
return hashlib.pbkdf2_hmac(
"sha256", password.encode("utf-8"), salt, PBKDF2_ITERATIONS
)
def _encode_password_hash(password: str) -> str:
salt = secrets.token_bytes(16)
digest = _hash_password(password, salt)
salt_encoded = _base64url_encode(salt)
digest_encoded = _base64url_encode(digest)
return (
f"pbkdf2_sha256${PBKDF2_ITERATIONS}${salt_encoded}${digest_encoded}"
)
def _verify_password_hash(password: str, encoded_hash: str) -> bool:
try:
algorithm, iterations_str, salt_encoded, digest_encoded = encoded_hash.split("$")
if algorithm != "pbkdf2_sha256":
return False
iterations = int(iterations_str)
salt = _base64url_decode(salt_encoded)
expected_digest = _base64url_decode(digest_encoded)
actual_digest = hashlib.pbkdf2_hmac(
"sha256", password.encode("utf-8"), salt, iterations
)
return hmac.compare_digest(actual_digest, expected_digest)
except Exception:
return False
def _get_or_create_auth_secret(config: dict) -> str:
secret = config.get("AUTH_SECRET_KEY")
if secret:
return secret
secret = _base64url_encode(secrets.token_bytes(32))
config["AUTH_SECRET_KEY"] = secret
_save_user_config(config)
return secret
def is_auth_configured() -> bool:
config = _load_user_config()
return bool(config.get("AUTH_USERNAME") and config.get("AUTH_PASSWORD_HASH"))
def get_configured_auth_username() -> Optional[str]:
config = _load_user_config()
username = config.get("AUTH_USERNAME")
if isinstance(username, str) and username.strip():
return username.strip()
return None
def setup_initial_credentials(username: str, password: str) -> None:
cleaned_username = (username or "").strip()
if len(cleaned_username) < 3:
raise ValueError("Username must be at least 3 characters")
if len(password or "") < 6:
raise ValueError("Password must be at least 6 characters")
config = _load_user_config()
if config.get("AUTH_USERNAME") and config.get("AUTH_PASSWORD_HASH"):
raise ValueError("Credentials already configured")
config["AUTH_USERNAME"] = cleaned_username
config["AUTH_PASSWORD_HASH"] = _encode_password_hash(password)
_get_or_create_auth_secret(config)
_save_user_config(config)
def force_set_credentials(username: str, password: str) -> None:
"""Overwrite stored credentials; used by env-based preseed/override."""
cleaned_username = (username or "").strip()
if len(cleaned_username) < 3:
raise ValueError("Username must be at least 3 characters")
if len(password or "") < 6:
raise ValueError("Password must be at least 6 characters")
config = _load_user_config()
config["AUTH_USERNAME"] = cleaned_username
config["AUTH_PASSWORD_HASH"] = _encode_password_hash(password)
# Rotate the signing secret so any previously-issued tokens stop validating.
config["AUTH_SECRET_KEY"] = _base64url_encode(secrets.token_bytes(32))
_save_user_config(config)
def clear_stored_credentials() -> None:
"""Remove stored credentials; next boot will request setup again."""
config = _load_user_config()
removed = False
for key in ("AUTH_USERNAME", "AUTH_PASSWORD_HASH", "AUTH_SECRET_KEY"):
if key in config:
config.pop(key, None)
removed = True
if removed:
_save_user_config(config, removed_keys=AUTH_CONFIG_FIELDS)
def verify_credentials(username: str, password: str) -> bool:
config = _load_user_config()
stored_username = config.get("AUTH_USERNAME")
stored_hash = config.get("AUTH_PASSWORD_HASH")
if not stored_username or not stored_hash:
return False
cleaned_username = (username or "").strip()
if not hmac.compare_digest(cleaned_username, stored_username):
return False
return _verify_password_hash(password or "", stored_hash)
def _sign_payload(payload_encoded: str, secret: str) -> str:
signature = hmac.new(
secret.encode("utf-8"), payload_encoded.encode("utf-8"), hashlib.sha256
).digest()
return _base64url_encode(signature)
def create_session_token(username: str) -> str:
config = _load_user_config()
secret = _get_or_create_auth_secret(config)
issued_at = int(time.time())
payload = {
"v": 1,
"u": username,
"iat": issued_at,
"exp": issued_at + SESSION_TTL_SECONDS,
}
payload_encoded = _base64url_encode(
json.dumps(payload, separators=(",", ":")).encode("utf-8")
)
signature_encoded = _sign_payload(payload_encoded, secret)
return f"{payload_encoded}.{signature_encoded}"
def validate_session_token(token: Optional[str]) -> Optional[str]:
if not token:
return None
config = _load_user_config()
stored_username = config.get("AUTH_USERNAME")
if not stored_username:
return None
secret = config.get("AUTH_SECRET_KEY")
if not secret:
return None
try:
payload_encoded, signature_encoded = token.split(".", 1)
except ValueError:
return None
expected_signature = _sign_payload(payload_encoded, secret)
if not hmac.compare_digest(signature_encoded, expected_signature):
return None
try:
payload_raw = _base64url_decode(payload_encoded)
payload = json.loads(payload_raw)
except Exception:
return None
username = payload.get("u")
version = payload.get("v")
expires_at = payload.get("exp")
if not isinstance(username, str) or not isinstance(expires_at, int):
return None
if version != 1:
return None
if not hmac.compare_digest(username, stored_username):
return None
if expires_at < int(time.time()):
return None
return username
def get_session_token_from_request(request: Request) -> Optional[str]:
cookie_token = request.cookies.get(SESSION_COOKIE_NAME)
if cookie_token:
return cookie_token
auth_header = request.headers.get("Authorization", "")
if auth_header.lower().startswith("bearer "):
return auth_header[7:].strip() or None
return None
def get_basic_auth_credentials_from_request(
request: Request,
) -> Optional[tuple[str, str]]:
auth_header = request.headers.get("Authorization", "")
if not auth_header.lower().startswith("basic "):
return None
encoded_value = auth_header[6:].strip()
if not encoded_value:
return None
try:
decoded_value = base64.b64decode(encoded_value).decode("utf-8")
except Exception:
return None
if ":" not in decoded_value:
return None
username, password = decoded_value.split(":", 1)
return username, password
def get_auth_status(session_token: Optional[str] = None) -> dict:
config = _load_user_config()
configured = bool(config.get("AUTH_USERNAME") and config.get("AUTH_PASSWORD_HASH"))
if not configured:
return {
"configured": False,
"authenticated": False,
"username": None,
}
username = validate_session_token(session_token)
return {
"configured": True,
"authenticated": bool(username),
"username": username,
}
def get_internal_auth_headers() -> dict[str, str]:
"""Return auth headers for trusted same-host service-to-service API calls."""
if is_disable_auth_enabled():
return {}
username = get_configured_auth_username()
if not username:
return {}
return {"Authorization": f"Bearer {create_session_token(username)}"}
def _is_secure_request(request: Request) -> bool:
forwarded_proto = request.headers.get("x-forwarded-proto", "")
if forwarded_proto.lower() == "https":
return True
return request.url.scheme == "https"
def set_session_cookie(response: Response, token: str, request: Request) -> None:
response.set_cookie(
key=SESSION_COOKIE_NAME,
value=token,
max_age=SESSION_TTL_SECONDS,
httponly=True,
secure=_is_secure_request(request),
samesite="lax",
path="/",
)
def clear_session_cookie(response: Response, request: Request) -> None:
response.delete_cookie(
key=SESSION_COOKIE_NAME,
httponly=True,
secure=_is_secure_request(request),
samesite="lax",
path="/",
)