#!/usr/bin/env python3 """Render the user-approved admin hostnames and bounded white-machine proxies.""" import argparse import ipaddress import os import pathlib import re CA = '/etc/ssl/certs/ca-certificates.crt' ACME = '/var/lib/gateway-admin-proxy/acme' CF_TRUST = '/etc/nginx/white-proxy-cloudflare-realip.conf' HEALTH_MAP = '''log_format gateway_admin_proxy_health '$time_iso8601 host=$host status=$status upstream_status=$upstream_status duration=$request_time'; map $status $gateway_admin_proxy_log_failure { ~^[45] 1; default 0; } ''' def private_logs(): # Nginx error lines include the entire request URI, including subscription tokens. # Keep critical diagnostics plus status-only failure records for these new vhosts. return ''' access_log /var/log/nginx/gateway-admin-proxy.health.log gateway_admin_proxy_health if=$gateway_admin_proxy_log_failure; error_log /var/log/nginx/gateway-admin-proxy.error.log crit; ''' def proxy_lines(upstream, tls_name, host, alias=None): result = f''' proxy_pass https://{upstream}$request_uri; proxy_ssl_server_name on; proxy_ssl_name {tls_name}; proxy_ssl_verify on; proxy_ssl_verify_depth 4; proxy_ssl_trusted_certificate {CA}; proxy_http_version 1.1; proxy_set_header Host {host}; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto https; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $gateway_admin_proxy_connection; proxy_connect_timeout 15s; proxy_read_timeout 120s; proxy_cache_bypass 1; proxy_no_cache 1; ''' if alias: result += f''' proxy_set_header X-Forwarded-Host $host; proxy_set_header Origin $gateway_admin_origin; proxy_set_header Referer $gateway_admin_referer; proxy_redirect https://{host}/ https://$host/; proxy_cookie_domain {host} $host; ''' else: result += ' proxy_set_header X-Forwarded-Host $host;\n' return result def http_server(names, redirect=True): fallback = 'return 301 https://$host$request_uri;' if redirect else 'return 404;' return f'''server {{ listen 80; listen [::]:80; server_name {names}; {private_logs()} location ^~ /.well-known/acme-challenge/ {{ root {ACME}; try_files $uri =404; }} location / {{ {fallback} }} }} ''' def body_rewrites(source, alias, key): # Rewrite URL hosts, preserving plain brand names such as DP-LINK.NET. rules = f''' sub_filter_once off; sub_filter_types text/css application/javascript application/json; sub_filter 'https://$gateway_admin_origin_host' 'https://$host'; sub_filter 'http://$gateway_admin_origin_host' 'https://$host'; sub_filter '//$gateway_admin_origin_host' '//$host'; ''' if key in ('new', 'old'): rules += f''' sub_filter 'https://{source}' 'https://{alias}'; sub_filter 'http://{source}' 'https://{alias}'; sub_filter '//{source}' '//{alias}'; sub_filter '.{source}/' '.{alias}/'; ''' elif key == 'fuc-site': rules += ''' sub_filter 'https://福云.com' 'https://fuc.tomtojapan.online'; sub_filter 'http://福云.com' 'https://fuc.tomtojapan.online'; sub_filter '//福云.com' '//fuc.tomtojapan.online'; ''' return rules def https_server(names, certificate, content, cloudflare=False, white_ip=None, key_name='privatekey.pem'): trusted = f' include {CF_TRUST};\n' if cloudflare else '' if white_ip: trusted += f''' set_real_ip_from {white_ip}; real_ip_header X-Forwarded-For; real_ip_recursive on; ''' return f'''server {{ listen 443 ssl; listen [::]:443 ssl; server_name {names}; ssl_certificate {certificate}/fullchain.pem; ssl_certificate_key {certificate}/{key_name}; ssl_protocols TLSv1.2 TLSv1.3; client_max_body_size 20m; {private_logs()} {trusted}{content} }} ''' def render_gateway(stage, legacy_path, white_ip): files = {'00-gateway-admin-proxy-maps.conf': HEALTH_MAP + '''map $http_upgrade $gateway_admin_proxy_connection { default upgrade; '' close; } '''} for kind, port, tls in [('fuc', 64444, 'fu101.gembox-sub.com'), ('dpl', 16444, 'c2.deeplink-subscribe.com')]: name = f'getway-{kind}.gemcook.net' content = http_server(name, stage == 'https') if stage == 'https': if kind == 'fuc': location = f''' location = / {{ return 302 {legacy_path}; }} location ^~ {legacy_path} {{ {proxy_lines('127.0.0.1:'+str(port), tls, name)} }} location / {{ return 404; }} ''' else: location = f''' location / {{ {proxy_lines('127.0.0.1:'+str(port), tls, name)} }} ''' # The admin upstreams retain their own certificates and authentication. location += ' add_header Cache-Control "private, no-store" always;\n' content += https_server(name, '/etc/gateway-admin-letsencrypt/live/gateway-admin', location, white_ip=white_ip, key_name='privkey.pem') files[f'gateway-admin-{kind}.conf'] = content return files def render_white(stage, legacy_certificate, skip_legacy=False): files = {} maps = HEALTH_MAP + '''map $http_upgrade $gateway_admin_proxy_connection { default upgrade; '' close; } map $host $gateway_admin_origin_host { tomshell.online gemcook.net; ~^(?[a-z0-9-]+)\\.tomshell\\.online$ $gnew.gemcook.net; tomshell.com.de diaodu.us; ~^(?[a-z0-9-]+)\\.tomshell\\.com\\.de$ $gold.diaodu.us; dpl.tomtojapan.online dp-link.net; fuc.tomtojapan.online xn--9kqy16h.com; default ''; } map "$host|$http_origin" $gateway_admin_origin { "~^(?[a-z0-9.-]+)\\|https://\\k$" https://$gateway_admin_origin_host; default $http_origin; } map "$host|$http_referer" $gateway_admin_referer { "~^(?[a-z0-9.-]+)\\|https://\\k(?/.*)$" https://$gateway_admin_origin_host$grefpath; default $http_referer; } ''' files['00-gateway-admin-proxy-maps.conf'] = maps specs = [ ('new', 'tomshell.online *.tomshell.online', '/etc/white-proxy-origin/tomshell.online', 'gemcook.net', 'tomshell.online', True), ('old', 'tomshell.com.de *.tomshell.com.de', legacy_certificate, 'diaodu.us', 'tomshell.com.de', False), ('dpl-site', 'dpl.tomtojapan.online', '/etc/white-proxy-origin/tomtojapan.online', 'dp-link.net', 'dpl.tomtojapan.online', True), ('fuc-site', 'fuc.tomtojapan.online', '/etc/white-proxy-origin/tomtojapan.online', 'xn--9kqy16h.com', 'fuc.tomtojapan.online', True), ] for key, names, cert, source, alias, cf in specs: if key == 'old' and skip_legacy: continue data = http_server(names, stage == 'https') if stage == 'https': extras = '' if key in ('new', 'old'): source_regex = re.escape(source) extras = f''' proxy_redirect ~^https?://([a-z0-9-]+\\.)?{source_regex}(/.*)$ https://$1{alias}$2; proxy_cookie_domain {source} {alias}; ''' location = f''' resolver 1.1.1.1 1.0.0.1 valid=300s ipv6=off; resolver_timeout 5s; if ($gateway_admin_origin_host = '') {{ return 444; }} location / {{ {proxy_lines('$gateway_admin_origin_host', '$gateway_admin_origin_host', '$gateway_admin_origin_host', alias)}{extras} proxy_set_header Accept-Encoding ""; {body_rewrites(source, alias, key)} }} ''' data += https_server(names, cert, location, cloudflare=cf, key_name='privatekey.pem' if cf else 'privkey.pem') files[f'white-{key}-proxy.conf'] = data return files def main(): p = argparse.ArgumentParser() p.add_argument('role', choices=['gateway', 'white']) p.add_argument('--stage', choices=['http', 'https'], required=True) p.add_argument('--output', type=pathlib.Path, required=True) p.add_argument('--white-ip', default='104.168.30.40') p.add_argument('--legacy-certificate', default='/etc/white-proxy-letsencrypt/live/tomshell.com.de') p.add_argument('--skip-legacy', action='store_true', help='Leave legacy proxy untouched until its wildcard certificate is ready') a = p.parse_args() ipaddress.ip_address(a.white_ip) if not re.fullmatch(r'/[A-Za-z0-9/._-]+', a.legacy_certificate): p.error('Invalid certificate directory') legacy_path = os.environ.get('LEGACY_ADMIN_PATH', '') if a.role == 'gateway' and a.stage == 'https' and not re.fullmatch(r'/[A-Za-z0-9_-]+/', legacy_path): p.error('Supply the existing legacy admin prefix through LEGACY_ADMIN_PATH') files = render_gateway(a.stage, legacy_path, a.white_ip) if a.role == 'gateway' else render_white(a.stage, a.legacy_certificate, a.skip_legacy) a.output.mkdir(parents=True, exist_ok=True) for name, content in files.items(): (a.output / name).write_text(content) print(f'Rendered {len(files)} {a.role}/{a.stage} files; no service configuration changed') if __name__ == '__main__': main()