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
|
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.
|
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
|
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
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')
|
FILE = os.path.join(DATA, 'scores.json')
|
||||||
PORT = int(os.environ.get('PORT', 80))
|
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
|
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.
|
MAX_BODY = 2048 # a submission is ~60 bytes. Anything larger is not one.
|
||||||
NAME_MAX = 12
|
NAME_MAX = 12
|
||||||
@@ -230,6 +236,31 @@ class Handler(SimpleHTTPRequestHandler):
|
|||||||
def log_message(self, *a):
|
def log_message(self, *a):
|
||||||
pass # one line per sprite request is not a log, it is noise
|
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 ----
|
# ---- the API ----
|
||||||
def reply(self, code, obj):
|
def reply(self, code, obj):
|
||||||
body = json.dumps(obj).encode()
|
body = json.dumps(obj).encode()
|
||||||
|
|||||||
Reference in new issue
Block a user