You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
98 lines
4.3 KiB
98 lines
4.3 KiB
"""Exercise the shipped Nginx template with a local header-echo upstream.
|
|
|
|
Run: python3 test/proxy_transport_test.py /path/to/nginx
|
|
No production data, credentials, TLS assumption or Rails runtime required.
|
|
"""
|
|
import http.server
|
|
import json
|
|
import os
|
|
import pwd
|
|
from pathlib import Path
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
import urllib.request
|
|
|
|
|
|
class Echo(http.server.BaseHTTPRequestHandler):
|
|
def do_GET(self):
|
|
body = json.dumps(dict(self.headers)).encode()
|
|
self.send_response(200)
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
|
|
def log_message(self, *args):
|
|
pass
|
|
|
|
|
|
def port():
|
|
with socket.socket() as sock:
|
|
sock.bind(('127.0.0.1', 0))
|
|
return sock.getsockname()[1]
|
|
|
|
|
|
def check(nginx, trusted_cidr, upstream, cases):
|
|
template = (Path(__file__).resolve().parents[1] / 'frontend/nginx.conf').read_text()
|
|
listen = port()
|
|
with tempfile.TemporaryDirectory(prefix='pcm-proxy-test-') as directory:
|
|
config = Path(directory) / 'nginx.conf'
|
|
rendered = template.replace('${TRUSTED_TLS_PROXY_CIDR}', trusted_cidr)
|
|
rendered = rendered.replace('listen 80;', f'listen 127.0.0.1:{listen};')
|
|
rendered = rendered.replace('http://api:3000', f'http://127.0.0.1:{upstream}')
|
|
current_user = pwd.getpwuid(os.geteuid()).pw_name
|
|
config.write_text(f'user {current_user}; master_process off; daemon off; pid {directory}/nginx.pid;\n'
|
|
f'error_log {directory}/error.log; events {{}}\nhttp {{ access_log off; '
|
|
f'client_body_temp_path {directory}/body; proxy_temp_path {directory}/proxy; '
|
|
f'fastcgi_temp_path {directory}/fastcgi; uwsgi_temp_path {directory}/uwsgi; '
|
|
f'scgi_temp_path {directory}/scgi; {rendered} }}')
|
|
validation = subprocess.run([nginx, '-t', '-c', str(config), '-p', directory], capture_output=True, text=True)
|
|
if validation.returncode:
|
|
raise RuntimeError(validation.stderr)
|
|
process = subprocess.Popen([nginx, '-c', str(config), '-p', directory], stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
|
|
try:
|
|
for attempt in range(100):
|
|
try:
|
|
with socket.create_connection(('127.0.0.1', listen), timeout=.1):
|
|
break
|
|
except OSError:
|
|
if process.poll() is not None:
|
|
raise RuntimeError(process.stderr.read().decode())
|
|
time.sleep(.02)
|
|
for headers, expected in cases:
|
|
for path in ['/api/v1/practice_transfer', '/admin/pghero']:
|
|
request = urllib.request.Request(f'http://127.0.0.1:{listen}{path}', headers=headers)
|
|
with urllib.request.urlopen(request, timeout=3) as response:
|
|
forwarded = json.load(response)
|
|
assert forwarded['X-Forwarded-Proto'] == expected, forwarded
|
|
assert 'Forwarded' not in forwarded, forwarded
|
|
assert 'X-Forwarded-Ssl' not in forwarded, forwarded
|
|
assert 'X-Forwarded-Scheme' not in forwarded, forwarded
|
|
finally:
|
|
process.terminate()
|
|
process.communicate(timeout=5)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
nginx = str(Path(sys.argv[1]).resolve()) if len(sys.argv) > 1 else 'nginx'
|
|
upstream = http.server.ThreadingHTTPServer(('127.0.0.1', 0), Echo)
|
|
thread = threading.Thread(target=upstream.serve_forever, daemon=True)
|
|
thread.start()
|
|
try:
|
|
check(nginx, '127.0.0.1/32', upstream.server_port, [
|
|
({'X-Forwarded-Proto': 'https'}, 'https'),
|
|
({'X-Forwarded-Proto': 'http'}, 'http'),
|
|
({}, 'http'),
|
|
({'X-Forwarded-Proto': 'https,http'}, 'http'),
|
|
({'Forwarded': 'proto=https', 'X-Forwarded-Ssl': 'on', 'X-Forwarded-Scheme': 'https'}, 'http'),
|
|
])
|
|
check(nginx, '192.0.2.1/32', upstream.server_port, [
|
|
({'X-Forwarded-Proto': 'https', 'Forwarded': 'proto=https', 'X-Forwarded-Ssl': 'on'}, 'http'),
|
|
({}, 'http'),
|
|
])
|
|
print('PASS: 14 proxy transport cases; trusted HTTPS retained, untrusted assertions rejected.')
|
|
finally:
|
|
upstream.shutdown()
|
|
upstream.server_close()
|