spark-server/deploy/geo-guard/wyrs_geo_guard.py
DESKTOP-L8OUL9F\gfl0034 94dd2d819c Add mainland China geo gate (allow Shanghai/overseas).
Nginx auth_request plus MaxMind City lookup blocks non-Shanghai mainland CN with a fixed notice page for sparkmessenger/sparkmsg (and deploy helpers for the shared Singapore host).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-22 15:28:04 +08:00

137 lines
3.9 KiB
Python

#!/usr/bin/env python3
"""Local auth_request backend: allow overseas + Shanghai; block other mainland CN."""
import ipaddress
import os
import sys
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import Optional
from urllib.parse import parse_qs, urlparse
import geoip2.database
from geoip2.errors import AddressNotFoundError
DB_PATH = os.environ.get("GEOIP_CITY_DB", "/usr/share/GeoIP/GeoLite2-City.mmdb")
LISTEN = os.environ.get("WYRS_GEO_LISTEN", "127.0.0.1")
PORT = int(os.environ.get("WYRS_GEO_PORT", "18099"))
_reader = None # type: Optional[geoip2.database.Reader]
def get_reader():
global _reader
if _reader is None:
_reader = geoip2.database.Reader(DB_PATH)
return _reader
def client_ip(handler):
# Prefer query ?ip= for local probes; production uses X-Real-IP from nginx.
qs = parse_qs(urlparse(handler.path).query)
if "ip" in qs and qs["ip"]:
return qs["ip"][0].strip()
xri = handler.headers.get("X-Real-IP", "").strip()
if xri:
return xri.split(",")[0].strip()
return handler.client_address[0]
def is_trusted_local(ip):
try:
addr = ipaddress.ip_address(ip)
except ValueError:
return True
return bool(
addr.is_private
or addr.is_loopback
or addr.is_link_local
or addr.is_reserved
)
def is_shanghai_mainland(ip):
"""Return True if Shanghai, False if other mainland CN, None if not mainland / unknown."""
try:
rec = get_reader().city(ip)
except AddressNotFoundError:
return None
except Exception as exc: # keep site up if DB quirks
sys.stderr.write("geo lookup error ip=%s: %s\n" % (ip, exc))
return None
cc = (rec.country.iso_code or "").upper()
if cc != "CN":
return None
for sub in rec.subdivisions:
iso = (sub.iso_code or "").upper()
name = (sub.name or "").strip().lower()
if iso == "SH" or name == "shanghai":
return True
city = (rec.city.name or "").strip().lower()
if city == "shanghai":
return True
return False
def allowed(ip):
if is_trusted_local(ip):
return True
decision = is_shanghai_mainland(ip)
# None = overseas / HK-MO-TW / unknown → allow
# True = Shanghai → allow
# False = other mainland → deny
return decision is not False
class Handler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def log_message(self, fmt: str, *args) -> None: # noqa: A003
sys.stderr.write("%s - %s\n" % (self.address_string(), fmt % args))
def do_GET(self) -> None: # noqa: N802
path = self.path.split("?", 1)[0]
if path not in ("/check", "/healthz"):
self.send_response(404)
self.send_header("Content-Length", "0")
self.end_headers()
return
if path == "/healthz":
body = b"ok\n"
self.send_response(200)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
return
ip = client_ip(self)
if allowed(ip):
self.send_response(204)
self.send_header("Content-Length", "0")
self.send_header("X-Wyrs-Geo", "allow")
self.end_headers()
return
self.send_response(403)
self.send_header("Content-Length", "0")
self.send_header("X-Wyrs-Geo", "deny-cn")
self.end_headers()
def main() -> None:
if not os.path.isfile(DB_PATH):
print(f"missing GeoIP DB: {DB_PATH}", file=sys.stderr)
sys.exit(1)
get_reader()
httpd = ThreadingHTTPServer((LISTEN, PORT), Handler)
print(f"wyrs-geo-guard listening on {LISTEN}:{PORT} db={DB_PATH}", flush=True)
httpd.serve_forever()
if __name__ == "__main__":
main()