pi commited on
Commit
b3596ff
·
1 Parent(s): c9fb4ed

fix(gate): accept x-ael-key alongside x-respite-key; drop dead key read

Browse files

The Respite -> Ael rename renamed the app's copy of the shared-key header to
X-Ael-Key while this gate kept reading x-respite-key. Those are different
header *names*, so every protected request was rejected with 401 from a
perfectly correct key — search and audio, on every deployment, silently.
Header names are case-insensitive, so the rename looked harmless in review;
it was not. c9fb4ed already restarted the Space so SEARX_API_KEY is loaded,
which is why the only remaining fault was the header name.

- Accept either spelling, x-respite-key first as canonical. The app now sends
the canonical one; the alias keeps any build in flight during a rename from
401ing, and must not be dropped without checking deployed builds.
- Drop the module-level _SPACE_KEY read of "RESpite_SPACE_KEY" — a
case-mangled secret name that never existed and always read "". It was masked
because the middleware rebinds the same name per request, so the broken read
was dead code one deleted line away from every gated route answering 503.
- Extract _is_protected() so the prefix check exists once. str.startswith takes
a tuple here; an equivalent transcription written with an array silently
matches nothing, which is exactly how the first draft of the app-side test
passed while testing nothing.

Files changed (1) hide show
  1. app.py +34 -4
app.py CHANGED
@@ -901,10 +901,19 @@ def _patched_create_app(blocks, **kwargs):
901
  try:
902
  import hmac as _hmac
903
 
904
- _SPACE_KEY = os.environ.get("RESpite_SPACE_KEY", "")
 
 
 
 
 
 
 
 
 
905
  # Which secret guards which prefix. The searx bridge gets its own key
906
  # (SEARX_API_KEY) so rotating search access never touches the audio
907
- # path; everything else shares RESPIRE_SPACE_KEY.
908
  _PROTECTED_PREFIXES = (
909
  "/respite/audio/",
910
  "/respite/search",
@@ -921,15 +930,36 @@ def _patched_create_app(blocks, **kwargs):
921
  return os.environ.get("SEARX_API_KEY", "")
922
  return os.environ.get("RESPIRE_SPACE_KEY", "")
923
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
924
  @fa_app.middleware("http")
925
  async def _respite_key_gate(request, call_next):
926
- if request.url.path.startswith(_PROTECTED_PREFIXES):
927
  _SPACE_KEY = _expected_key(request.url.path)
928
  if not _SPACE_KEY:
929
  return JSONResponse(
930
  {"error": "service not configured"}, status_code=503
931
  )
932
- presented = request.headers.get("x-respite-key", "")
 
 
 
 
 
 
933
  if not _hmac.compare_digest(presented, _SPACE_KEY):
934
  return JSONResponse({"error": "unauthorized"}, status_code=401)
935
  return await call_next(request)
 
901
  try:
902
  import hmac as _hmac
903
 
904
+ # NOTE: there used to be a module-level
905
+ # _SPACE_KEY = os.environ.get("RESpite_SPACE_KEY", "")
906
+ # here — a case-mangled name left by the Respite -> Ael rename, reading
907
+ # a secret that does not exist, so it always yielded "". It looked
908
+ # harmless because the middleware rebinds `_SPACE_KEY` from
909
+ # `_expected_key(path)` on every protected request, making the broken
910
+ # read dead. It was one deleted line away from every gated route
911
+ # answering 503 "service not configured" with nothing to say why. The key
912
+ # is now read in exactly one place.
913
+ #
914
  # Which secret guards which prefix. The searx bridge gets its own key
915
  # (SEARX_API_KEY) so rotating search access never touches the audio
916
+ # path; everything else shares RESPITE_SPACE_KEY.
917
  _PROTECTED_PREFIXES = (
918
  "/respite/audio/",
919
  "/respite/search",
 
930
  return os.environ.get("SEARX_API_KEY", "")
931
  return os.environ.get("RESPIRE_SPACE_KEY", "")
932
 
933
+ # The header the shared key is presented in. `x-respite-key` is
934
+ # canonical and is what the app sends; `x-ael-key` is accepted as an
935
+ # alias because the app was renamed Respite -> Ael and briefly sent
936
+ # `X-Ael-Key`, a header this gate never read — so every search and audio
937
+ # call 401'd for days. Header names are case-insensitive, so the alias
938
+ # is the same token in lower case; the real fault was that `x-ael-key`
939
+ # and `x-respite-key` are different *names* and the two sides drifted.
940
+ # Accepting both keeps any build in flight during a rename working. Do
941
+ # not drop the alias without checking that no deployed build still sends
942
+ # it.
943
+ _KEY_HEADERS = ("x-respite-key", "x-ael-key")
944
+
945
+ def _is_protected(path: str) -> bool:
946
+ return any(path.startswith(p) for p in _PROTECTED_PREFIXES)
947
+
948
  @fa_app.middleware("http")
949
  async def _respite_key_gate(request, call_next):
950
+ if _is_protected(request.url.path):
951
  _SPACE_KEY = _expected_key(request.url.path)
952
  if not _SPACE_KEY:
953
  return JSONResponse(
954
  {"error": "service not configured"}, status_code=503
955
  )
956
+ # Either spelling satisfies the gate; the first present wins.
957
+ presented = ""
958
+ for _h in _KEY_HEADERS:
959
+ _v = request.headers.get(_h)
960
+ if _v:
961
+ presented = _v
962
+ break
963
  if not _hmac.compare_digest(presented, _SPACE_KEY):
964
  return JSONResponse({"error": "unauthorized"}, status_code=401)
965
  return await call_next(request)