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>
137 lines
3.9 KiB
Python
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()
|