201 lines
7.9 KiB
Python
201 lines
7.9 KiB
Python
#!/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'
|
|
|
|
|
|
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};
|
|
access_log off;
|
|
location ^~ /.well-known/acme-challenge/ {{
|
|
root {ACME};
|
|
try_files $uri =404;
|
|
}}
|
|
location / {{ {fallback} }}
|
|
}}
|
|
'''
|
|
|
|
|
|
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;
|
|
access_log off;
|
|
error_log /var/log/nginx/gateway-admin-proxy.error.log warn;
|
|
{trusted}{content}
|
|
}}
|
|
'''
|
|
|
|
|
|
def render_gateway(stage, legacy_path, white_ip):
|
|
files = {'00-gateway-admin-proxy-maps.conf': '''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 = '''map $http_upgrade $gateway_admin_proxy_connection {
|
|
default upgrade;
|
|
'' close;
|
|
}
|
|
map $host $gateway_admin_origin_host {
|
|
tomshell.online gemcook.net;
|
|
~^(?<gnew>[a-z0-9-]+)\\.tomshell\\.online$ $gnew.gemcook.net;
|
|
tomshell.com.de diaodu.us;
|
|
~^(?<gold>[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 {
|
|
"~^(?<grequest_origin>[a-z0-9.-]+)\\|https://\\k<grequest_origin>$" https://$gateway_admin_origin_host;
|
|
default $http_origin;
|
|
}
|
|
map "$host|$http_referer" $gateway_admin_referer {
|
|
"~^(?<grequest_referer>[a-z0-9.-]+)\\|https://\\k<grequest_referer>(?<grefpath>/.*)$" 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};
|
|
'''
|
|
elif key == 'fuc-site':
|
|
extras = " sub_filter '福云.com' 'fuc.tomtojapan.online';\n"
|
|
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 "";
|
|
sub_filter_once off;
|
|
sub_filter_types text/css application/javascript application/json;
|
|
sub_filter '{source}' '{alias}';
|
|
}}
|
|
'''
|
|
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()
|