serve: Store Cdm per-secret, ensure session more efficiently

The Cdm is now stored per-secret due to the Cdm object's session limit. This is so one user (by secret key) cannot overload the server with too many sessions.

But this also fixes it so that the serve API will work for more than just 50 sessions for all users. Otherwise the user pool will eventually overload the Cdm with 50 sessions, even if they close it, it will eventually happen. Think of it like the server being overloaded prematurely.
This commit is contained in:
rlaphoenix 2022-08-01 22:40:55 +01:00
parent 290da707ea
commit 9501c34f60
1 changed files with 29 additions and 18 deletions

View File

@ -23,7 +23,7 @@ routes = web.RouteTableDef()
async def _startup(app: web.Application): async def _startup(app: web.Application):
app["sessions"]: dict[bytes, Cdm] = {} app["cdms"]: dict[str, Cdm] = {}
app["config"]["devices"] = { app["config"]["devices"] = {
path.stem: path path.stem: path
for x in app["config"]["devices"] for x in app["config"]["devices"]
@ -32,8 +32,8 @@ async def _startup(app: web.Application):
async def _cleanup(app: web.Application): async def _cleanup(app: web.Application):
app["sessions"].clear() app["cdms"].clear()
del app["sessions"] del app["cdms"]
app["config"].clear() app["config"].clear()
del app["config"] del app["config"]
@ -48,7 +48,8 @@ async def ping(_) -> web.Response:
@routes.get("/open/{device}") @routes.get("/open/{device}")
async def open(request: web.Request) -> web.Response: async def open(request: web.Request) -> web.Response:
user = request.app["config"]["users"][request.headers["X-Secret-Key"]] secret_key = request.headers["X-Secret-Key"]
user = request.app["config"]["users"][secret_key]
device = request.match_info["device"] device = request.match_info["device"]
if device not in user["devices"] or device not in request.app["config"]["devices"]: if device not in user["devices"] or device not in request.app["config"]["devices"]:
@ -59,9 +60,11 @@ async def open(request: web.Request) -> web.Response:
"message": f"Device '{device}' is not found or you are not authorized to use it." "message": f"Device '{device}' is not found or you are not authorized to use it."
}, status=403) }, status=403)
device = Device.load(request.app["config"]["devices"][device]) cdm = request.app["cdms"].get(secret_key)
if not cdm:
device = Device.load(request.app["config"]["devices"][device])
cdm = request.app["cdms"][secret_key] = Cdm(device)
cdm = Cdm(device)
try: try:
session_id = cdm.open() session_id = cdm.open()
except TooManySessions as e: except TooManySessions as e:
@ -70,16 +73,14 @@ async def open(request: web.Request) -> web.Response:
"message": str(e) "message": str(e)
}, status=400) }, status=400)
request.app["sessions"][session_id] = cdm
return web.json_response({ return web.json_response({
"status": 200, "status": 200,
"message": "Success", "message": "Success",
"data": { "data": {
"session_id": session_id.hex(), "session_id": session_id.hex(),
"device": { "device": {
"system_id": device.system_id, "system_id": cdm.device.system_id,
"security_level": device.security_level "security_level": cdm.device.security_level
} }
} }
}) })
@ -87,6 +88,8 @@ async def open(request: web.Request) -> web.Response:
@routes.post("/challenge/{license_type}") @routes.post("/challenge/{license_type}")
async def challenge(request: web.Request) -> web.Response: async def challenge(request: web.Request) -> web.Response:
secret_key = request.headers["X-Secret-Key"]
body = await request.json() body = await request.json()
for required_field in ("session_id", "init_data"): for required_field in ("session_id", "init_data"):
if not body.get(required_field): if not body.get(required_field):
@ -99,14 +102,17 @@ async def challenge(request: web.Request) -> web.Response:
session_id = bytes.fromhex(body["session_id"]) session_id = bytes.fromhex(body["session_id"])
# get cdm # get cdm
if session_id not in request.app["sessions"]: cdm = request.app["cdms"].get(secret_key)
# e.g., app["sessions"] being cleared on server crash, reboot, and such if not cdm or session_id not in cdm._sessions:
# or, the license message was from a challenge that was not made by our Cdm # This can happen if:
# - API server gets shutdown/restarted,
# - The user calls /challenge before /open,
# - The user called /open on a different IP Address
# - The user closed the session
return web.json_response({ return web.json_response({
"status": 400, "status": 400,
"message": "Invalid Session ID. Session ID may have Expired." "message": "Invalid Session ID. Session ID may have Expired."
}, status=400) }, status=400)
cdm = request.app["sessions"][session_id]
# set service certificate # set service certificate
service_certificate = body.get("service_certificate") service_certificate = body.get("service_certificate")
@ -137,6 +143,8 @@ async def challenge(request: web.Request) -> web.Response:
@routes.post("/keys/{key_type}") @routes.post("/keys/{key_type}")
async def keys(request: web.Request) -> web.Response: async def keys(request: web.Request) -> web.Response:
secret_key = request.headers["X-Secret-Key"]
body = await request.json() body = await request.json()
for required_field in ("session_id", "license_message"): for required_field in ("session_id", "license_message"):
if not body.get(required_field): if not body.get(required_field):
@ -165,14 +173,17 @@ async def keys(request: web.Request) -> web.Response:
}, status=400) }, status=400)
# get cdm # get cdm
if session_id not in request.app["sessions"]: cdm = request.app["cdms"].get(secret_key)
# e.g., app["sessions"] being cleared on server crash, reboot, and such if not cdm or session_id not in cdm._sessions:
# or, the license message was from a challenge that was not made by our Cdm # This can happen if:
# - API server gets shutdown/restarted,
# - The user calls /challenge before /open,
# - The user called /open on a different IP Address
# - The user closed the session
return web.json_response({ return web.json_response({
"status": 400, "status": 400,
"message": "Invalid Session ID. Session ID may have Expired." "message": "Invalid Session ID. Session ID may have Expired."
}, status=400) }, status=400)
cdm = request.app["sessions"][session_id]
# parse the license message # parse the license message
cdm.parse_license(session_id, body["license_message"]) cdm.parse_license(session_id, body["license_message"])