Put staging and prod behind one shared password
The game is not public yet. parse_request is the one place every method passes through, so the gate sits there rather than in each do_*. /api and /ws stay open because the iOS shell talks to prod from capacitor://localhost and has no credentials to send. SPLAT_AUTH overrides it; empty opens the site again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a09856f8c9
commit
b53fc07dd1
1 file changed
+32
-1
@@ -17,7 +17,7 @@ board is a wall for writing your name on, and the client is public JS that could
|
||||
anything regardless. What IS enforced is that nothing here can be made to crash, hang or grow
|
||||
without bound by a hostile request — see clean() and the caps below.
|
||||
"""
|
||||
import base64, hashlib, json, os, random, re, threading, time
|
||||
import base64, hashlib, hmac, json, os, random, re, threading, time
|
||||
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
@@ -25,6 +25,12 @@ DATA = os.environ.get('SPLAT_DATA') or ('/data' if os.path.isdir('/data') else H
|
||||
FILE = os.path.join(DATA, 'scores.json')
|
||||
PORT = int(os.environ.get('PORT', 80))
|
||||
|
||||
# ---- THE DOOR (2026-09-08). The game is not public yet, so staging and prod both sit behind one
|
||||
# shared password. SPLAT_AUTH is 'user:pass'; set it EMPTY in the environment to open the site again
|
||||
# (that is also how local dev and the browser-pane automation stay unprompted).
|
||||
AUTH = os.environ.get('SPLAT_AUTH', 'tennis:BummBummBoris')
|
||||
AUTH = ('Basic ' + base64.b64encode(AUTH.encode()).decode()) if AUTH else ''
|
||||
|
||||
KEEP = 200 # rows kept on disk PER BOARD; the board only ever shows the top slice of this
|
||||
MAX_BODY = 2048 # a submission is ~60 bytes. Anything larger is not one.
|
||||
NAME_MAX = 12
|
||||
@@ -230,6 +236,31 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
def log_message(self, *a):
|
||||
pass # one line per sprite request is not a log, it is noise
|
||||
|
||||
# ---- the door ----
|
||||
def parse_request(self):
|
||||
"""The password gate. It lives here, not in do_GET/do_POST, because parse_request is the
|
||||
ONE place every method passes through — GET, POST, HEAD and the websocket upgrade — and
|
||||
returning False from it drops the request before anything is dispatched."""
|
||||
if not super().parse_request():
|
||||
return False
|
||||
if not AUTH:
|
||||
return True
|
||||
path = self.path.split('?')[0]
|
||||
# /api and /ws STAY OPEN and this is not an oversight: the iOS shell serves the page out of
|
||||
# its own bundle and talks to prod by name from capacitor://localhost, with no credentials
|
||||
# it could ever send — gating them would kill the app's board, profile mirror and netplay
|
||||
# while leaving the thing we actually want private (the game itself) no more private.
|
||||
if path == '/ws' or path.startswith('/api/'):
|
||||
return True
|
||||
if hmac.compare_digest(self.headers.get('Authorization', ''), AUTH):
|
||||
return True
|
||||
self.send_response(401)
|
||||
self.send_header('WWW-Authenticate', 'Basic realm="splatter tennis"')
|
||||
self.send_header('Content-Length', '0')
|
||||
self.end_headers()
|
||||
self.close_connection = True # a POST body is still unread — do not parse it as the next request
|
||||
return False
|
||||
|
||||
# ---- the API ----
|
||||
def reply(self, code, obj):
|
||||
body = json.dumps(obj).encode()
|
||||
|
||||
Reference in new issue
Block a user