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

"""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()