forked from kenzuya/unshackle
chore(api): remove remote services
This commit is contained in:
@@ -1,145 +0,0 @@
|
|||||||
"""API key tier management for remote services."""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import Any, Dict, List, Optional
|
|
||||||
|
|
||||||
from aiohttp import web
|
|
||||||
|
|
||||||
log = logging.getLogger("api.keys")
|
|
||||||
|
|
||||||
|
|
||||||
def get_api_key_from_request(request: web.Request) -> Optional[str]:
|
|
||||||
"""
|
|
||||||
Extract API key from request headers.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
request: aiohttp request object
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
API key string or None
|
|
||||||
"""
|
|
||||||
api_key = request.headers.get("X-API-Key")
|
|
||||||
if api_key:
|
|
||||||
return api_key
|
|
||||||
|
|
||||||
auth_header = request.headers.get("Authorization", "")
|
|
||||||
if auth_header.startswith("Bearer "):
|
|
||||||
return auth_header[7:] # len("Bearer ") == 7
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_api_key_config(app: web.Application, api_key: str) -> Optional[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Get configuration for a specific API key.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
app: aiohttp application
|
|
||||||
api_key: API key to look up
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
API key configuration dict or None if not found
|
|
||||||
"""
|
|
||||||
config = app.get("config", {})
|
|
||||||
|
|
||||||
# Check new-style tiered API keys
|
|
||||||
api_keys = config.get("api_keys", [])
|
|
||||||
for key_config in api_keys:
|
|
||||||
if isinstance(key_config, dict) and key_config.get("key") == api_key:
|
|
||||||
return key_config
|
|
||||||
|
|
||||||
# Check legacy users list (backward compatibility)
|
|
||||||
users = config.get("users", [])
|
|
||||||
if api_key in users:
|
|
||||||
return {
|
|
||||||
"key": api_key,
|
|
||||||
"tier": "basic",
|
|
||||||
"allowed_cdms": []
|
|
||||||
}
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def is_premium_user(app: web.Application, api_key: str) -> bool:
|
|
||||||
"""
|
|
||||||
Check if an API key belongs to a premium user.
|
|
||||||
|
|
||||||
Premium users can use server-side CDM for decryption.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
app: aiohttp application
|
|
||||||
api_key: API key to check
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if premium user, False otherwise
|
|
||||||
"""
|
|
||||||
key_config = get_api_key_config(app, api_key)
|
|
||||||
if not key_config:
|
|
||||||
return False
|
|
||||||
|
|
||||||
tier = key_config.get("tier", "basic")
|
|
||||||
return tier == "premium"
|
|
||||||
|
|
||||||
|
|
||||||
def get_allowed_cdms(app: web.Application, api_key: str) -> List[str]:
|
|
||||||
"""
|
|
||||||
Get list of CDMs that an API key is allowed to use.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
app: aiohttp application
|
|
||||||
api_key: API key to check
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of allowed CDM names, or empty list if not premium
|
|
||||||
"""
|
|
||||||
key_config = get_api_key_config(app, api_key)
|
|
||||||
if not key_config:
|
|
||||||
return []
|
|
||||||
|
|
||||||
allowed_cdms = key_config.get("allowed_cdms", [])
|
|
||||||
|
|
||||||
# Handle wildcard
|
|
||||||
if allowed_cdms == "*" or allowed_cdms == ["*"]:
|
|
||||||
return ["*"]
|
|
||||||
|
|
||||||
return allowed_cdms if isinstance(allowed_cdms, list) else []
|
|
||||||
|
|
||||||
|
|
||||||
def get_default_cdm(app: web.Application, api_key: str) -> Optional[str]:
|
|
||||||
"""
|
|
||||||
Get default CDM for an API key.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
app: aiohttp application
|
|
||||||
api_key: API key to check
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Default CDM name or None
|
|
||||||
"""
|
|
||||||
key_config = get_api_key_config(app, api_key)
|
|
||||||
if not key_config:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return key_config.get("default_cdm")
|
|
||||||
|
|
||||||
|
|
||||||
def can_use_cdm(app: web.Application, api_key: str, cdm_name: str) -> bool:
|
|
||||||
"""
|
|
||||||
Check if an API key can use a specific CDM.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
app: aiohttp application
|
|
||||||
api_key: API key to check
|
|
||||||
cdm_name: CDM name to check access for
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if allowed, False otherwise
|
|
||||||
"""
|
|
||||||
allowed_cdms = get_allowed_cdms(app, api_key)
|
|
||||||
|
|
||||||
# Wildcard access
|
|
||||||
if "*" in allowed_cdms:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Specific CDM access
|
|
||||||
return cdm_name in allowed_cdms
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -8,9 +8,6 @@ from unshackle.core import __version__
|
|||||||
from unshackle.core.api.errors import APIError, APIErrorCode, build_error_response, handle_api_exception
|
from unshackle.core.api.errors import APIError, APIErrorCode, build_error_response, handle_api_exception
|
||||||
from unshackle.core.api.handlers import (cancel_download_job_handler, download_handler, get_download_job_handler,
|
from unshackle.core.api.handlers import (cancel_download_job_handler, download_handler, get_download_job_handler,
|
||||||
list_download_jobs_handler, list_titles_handler, list_tracks_handler)
|
list_download_jobs_handler, list_titles_handler, list_tracks_handler)
|
||||||
from unshackle.core.api.remote_handlers import (remote_decrypt, remote_get_chapters, remote_get_license,
|
|
||||||
remote_get_manifest, remote_get_titles, remote_get_tracks,
|
|
||||||
remote_list_services, remote_search)
|
|
||||||
from unshackle.core.services import Services
|
from unshackle.core.services import Services
|
||||||
from unshackle.core.update_checker import UpdateChecker
|
from unshackle.core.update_checker import UpdateChecker
|
||||||
|
|
||||||
@@ -733,16 +730,6 @@ def setup_routes(app: web.Application) -> None:
|
|||||||
app.router.add_get("/api/download/jobs/{job_id}", download_job_detail)
|
app.router.add_get("/api/download/jobs/{job_id}", download_job_detail)
|
||||||
app.router.add_delete("/api/download/jobs/{job_id}", cancel_download_job)
|
app.router.add_delete("/api/download/jobs/{job_id}", cancel_download_job)
|
||||||
|
|
||||||
# Remote service endpoints
|
|
||||||
app.router.add_get("/api/remote/services", remote_list_services)
|
|
||||||
app.router.add_post("/api/remote/{service}/search", remote_search)
|
|
||||||
app.router.add_post("/api/remote/{service}/titles", remote_get_titles)
|
|
||||||
app.router.add_post("/api/remote/{service}/tracks", remote_get_tracks)
|
|
||||||
app.router.add_post("/api/remote/{service}/manifest", remote_get_manifest)
|
|
||||||
app.router.add_post("/api/remote/{service}/chapters", remote_get_chapters)
|
|
||||||
app.router.add_post("/api/remote/{service}/license", remote_get_license)
|
|
||||||
app.router.add_post("/api/remote/{service}/decrypt", remote_decrypt)
|
|
||||||
|
|
||||||
|
|
||||||
def setup_swagger(app: web.Application) -> None:
|
def setup_swagger(app: web.Application) -> None:
|
||||||
"""Setup Swagger UI documentation."""
|
"""Setup Swagger UI documentation."""
|
||||||
@@ -767,14 +754,5 @@ def setup_swagger(app: web.Application) -> None:
|
|||||||
web.get("/api/download/jobs", download_jobs),
|
web.get("/api/download/jobs", download_jobs),
|
||||||
web.get("/api/download/jobs/{job_id}", download_job_detail),
|
web.get("/api/download/jobs/{job_id}", download_job_detail),
|
||||||
web.delete("/api/download/jobs/{job_id}", cancel_download_job),
|
web.delete("/api/download/jobs/{job_id}", cancel_download_job),
|
||||||
# Remote service routes
|
|
||||||
web.get("/api/remote/services", remote_list_services),
|
|
||||||
web.post("/api/remote/{service}/search", remote_search),
|
|
||||||
web.post("/api/remote/{service}/titles", remote_get_titles),
|
|
||||||
web.post("/api/remote/{service}/tracks", remote_get_tracks),
|
|
||||||
web.post("/api/remote/{service}/manifest", remote_get_manifest),
|
|
||||||
web.post("/api/remote/{service}/chapters", remote_get_chapters),
|
|
||||||
web.post("/api/remote/{service}/license", remote_get_license),
|
|
||||||
web.post("/api/remote/{service}/decrypt", remote_decrypt),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,236 +0,0 @@
|
|||||||
"""Session serialization helpers for remote services."""
|
|
||||||
|
|
||||||
from http.cookiejar import CookieJar
|
|
||||||
from typing import Any, Dict, Optional
|
|
||||||
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from unshackle.core.credential import Credential
|
|
||||||
|
|
||||||
|
|
||||||
def serialize_session(session: requests.Session) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Serialize a requests.Session into a JSON-serializable dictionary.
|
|
||||||
|
|
||||||
Extracts cookies, headers, and other session data that can be
|
|
||||||
transferred to a remote client for downloading.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
session: The requests.Session to serialize
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary containing serialized session data
|
|
||||||
"""
|
|
||||||
session_data = {
|
|
||||||
"cookies": {},
|
|
||||||
"headers": {},
|
|
||||||
"proxies": session.proxies.copy() if session.proxies else {},
|
|
||||||
}
|
|
||||||
|
|
||||||
# Serialize cookies
|
|
||||||
if session.cookies:
|
|
||||||
for cookie in session.cookies:
|
|
||||||
session_data["cookies"][cookie.name] = {
|
|
||||||
"value": cookie.value,
|
|
||||||
"domain": cookie.domain,
|
|
||||||
"path": cookie.path,
|
|
||||||
"secure": cookie.secure,
|
|
||||||
"expires": cookie.expires,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Serialize headers (exclude proxy-authorization for security)
|
|
||||||
if session.headers:
|
|
||||||
for key, value in session.headers.items():
|
|
||||||
# Skip proxy-related headers as they're server-specific
|
|
||||||
if key.lower() not in ["proxy-authorization"]:
|
|
||||||
session_data["headers"][key] = value
|
|
||||||
|
|
||||||
return session_data
|
|
||||||
|
|
||||||
|
|
||||||
def deserialize_session(
|
|
||||||
session_data: Dict[str, Any], target_session: Optional[requests.Session] = None
|
|
||||||
) -> requests.Session:
|
|
||||||
"""
|
|
||||||
Deserialize session data into a requests.Session.
|
|
||||||
|
|
||||||
Applies cookies, headers, and other session data from a remote server
|
|
||||||
to a local session for downloading.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
session_data: Dictionary containing serialized session data
|
|
||||||
target_session: Optional existing session to update (creates new if None)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
requests.Session with applied session data
|
|
||||||
"""
|
|
||||||
if target_session is None:
|
|
||||||
target_session = requests.Session()
|
|
||||||
|
|
||||||
# Apply cookies
|
|
||||||
if "cookies" in session_data:
|
|
||||||
for cookie_name, cookie_data in session_data["cookies"].items():
|
|
||||||
target_session.cookies.set(
|
|
||||||
name=cookie_name,
|
|
||||||
value=cookie_data["value"],
|
|
||||||
domain=cookie_data.get("domain"),
|
|
||||||
path=cookie_data.get("path", "/"),
|
|
||||||
secure=cookie_data.get("secure", False),
|
|
||||||
expires=cookie_data.get("expires"),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Apply headers
|
|
||||||
if "headers" in session_data:
|
|
||||||
target_session.headers.update(session_data["headers"])
|
|
||||||
|
|
||||||
# Note: We don't apply proxies from remote as the local client
|
|
||||||
# should use its own proxy configuration
|
|
||||||
|
|
||||||
return target_session
|
|
||||||
|
|
||||||
|
|
||||||
def extract_session_tokens(session: requests.Session) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Extract authentication tokens and similar data from a session.
|
|
||||||
|
|
||||||
Looks for common authentication patterns like Bearer tokens,
|
|
||||||
API keys in headers, etc.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
session: The requests.Session to extract tokens from
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary containing extracted tokens
|
|
||||||
"""
|
|
||||||
tokens = {}
|
|
||||||
|
|
||||||
# Check for Authorization header
|
|
||||||
if "Authorization" in session.headers:
|
|
||||||
tokens["authorization"] = session.headers["Authorization"]
|
|
||||||
|
|
||||||
# Check for common API key headers
|
|
||||||
for key in ["X-API-Key", "Api-Key", "X-Auth-Token"]:
|
|
||||||
if key in session.headers:
|
|
||||||
tokens[key.lower().replace("-", "_")] = session.headers[key]
|
|
||||||
|
|
||||||
return tokens
|
|
||||||
|
|
||||||
|
|
||||||
def apply_session_tokens(tokens: Dict[str, Any], target_session: requests.Session) -> None:
|
|
||||||
"""
|
|
||||||
Apply authentication tokens to a session.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
tokens: Dictionary containing tokens to apply
|
|
||||||
target_session: Session to apply tokens to
|
|
||||||
"""
|
|
||||||
# Apply Authorization header
|
|
||||||
if "authorization" in tokens:
|
|
||||||
target_session.headers["Authorization"] = tokens["authorization"]
|
|
||||||
|
|
||||||
# Apply other token headers
|
|
||||||
token_header_map = {
|
|
||||||
"x_api_key": "X-API-Key",
|
|
||||||
"api_key": "Api-Key",
|
|
||||||
"x_auth_token": "X-Auth-Token",
|
|
||||||
}
|
|
||||||
|
|
||||||
for token_key, header_name in token_header_map.items():
|
|
||||||
if token_key in tokens:
|
|
||||||
target_session.headers[header_name] = tokens[token_key]
|
|
||||||
|
|
||||||
|
|
||||||
def serialize_cookies(cookie_jar: Optional[CookieJar]) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Serialize a CookieJar into a JSON-serializable dictionary.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
cookie_jar: The CookieJar to serialize
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary containing serialized cookies
|
|
||||||
"""
|
|
||||||
if not cookie_jar:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
cookies = {}
|
|
||||||
for cookie in cookie_jar:
|
|
||||||
cookies[cookie.name] = {
|
|
||||||
"value": cookie.value,
|
|
||||||
"domain": cookie.domain,
|
|
||||||
"path": cookie.path,
|
|
||||||
"secure": cookie.secure,
|
|
||||||
"expires": cookie.expires,
|
|
||||||
}
|
|
||||||
|
|
||||||
return cookies
|
|
||||||
|
|
||||||
|
|
||||||
def deserialize_cookies(cookies_data: Dict[str, Any]) -> CookieJar:
|
|
||||||
"""
|
|
||||||
Deserialize cookies into a CookieJar.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
cookies_data: Dictionary containing serialized cookies
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
CookieJar with cookies
|
|
||||||
"""
|
|
||||||
import http.cookiejar
|
|
||||||
|
|
||||||
cookie_jar = http.cookiejar.CookieJar()
|
|
||||||
|
|
||||||
for cookie_name, cookie_data in cookies_data.items():
|
|
||||||
cookie = http.cookiejar.Cookie(
|
|
||||||
version=0,
|
|
||||||
name=cookie_name,
|
|
||||||
value=cookie_data["value"],
|
|
||||||
port=None,
|
|
||||||
port_specified=False,
|
|
||||||
domain=cookie_data.get("domain", ""),
|
|
||||||
domain_specified=bool(cookie_data.get("domain")),
|
|
||||||
domain_initial_dot=cookie_data.get("domain", "").startswith("."),
|
|
||||||
path=cookie_data.get("path", "/"),
|
|
||||||
path_specified=True,
|
|
||||||
secure=cookie_data.get("secure", False),
|
|
||||||
expires=cookie_data.get("expires"),
|
|
||||||
discard=False,
|
|
||||||
comment=None,
|
|
||||||
comment_url=None,
|
|
||||||
rest={},
|
|
||||||
)
|
|
||||||
cookie_jar.set_cookie(cookie)
|
|
||||||
|
|
||||||
return cookie_jar
|
|
||||||
|
|
||||||
|
|
||||||
def serialize_credential(credential: Optional[Credential]) -> Optional[Dict[str, str]]:
|
|
||||||
"""
|
|
||||||
Serialize a Credential into a JSON-serializable dictionary.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
credential: The Credential to serialize
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary containing username and password, or None
|
|
||||||
"""
|
|
||||||
if not credential:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return {"username": credential.username, "password": credential.password}
|
|
||||||
|
|
||||||
|
|
||||||
def deserialize_credential(credential_data: Optional[Dict[str, str]]) -> Optional[Credential]:
|
|
||||||
"""
|
|
||||||
Deserialize credential data into a Credential object.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
credential_data: Dictionary containing username and password
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Credential object or None
|
|
||||||
"""
|
|
||||||
if not credential_data:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return Credential(username=credential_data["username"], password=credential_data["password"])
|
|
||||||
Reference in New Issue
Block a user