upstream_py.py (6570B)
1 #!/usr/bin/env python3 2 # upstream_py: Python-based upstream HTTP server used by the 3 # paivana reverse-proxy tests. Implements the same small set 4 # of canned endpoints as upstream_mhd.c. 5 6 import sys 7 import time 8 from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer 9 10 UPSTREAM_NAME = "py" 11 12 13 class Handler(BaseHTTPRequestHandler): 14 protocol_version = "HTTP/1.1" 15 16 def log_message(self, fmt, *args): 17 sys.stderr.write("upstream_py: " + (fmt % args) + "\n") 18 19 def _send_text(self, code, body, content_type="text/plain"): 20 if isinstance(body, str): 21 body = body.encode("utf-8") 22 self.send_response(code) 23 self.send_header("X-Upstream", UPSTREAM_NAME) 24 self.send_header("Content-Type", content_type) 25 self.send_header("Content-Length", str(len(body))) 26 self.end_headers() 27 self.wfile.write(body) 28 29 def _read_body(self): 30 cl = int(self.headers.get("Content-Length", "0") or 0) 31 if cl > 0: 32 return self.rfile.read(cl) 33 return b"" 34 35 def do_OPTIONS(self): 36 self.send_response(204) 37 self.send_header("X-Upstream", UPSTREAM_NAME) 38 self.send_header("Allow", 39 "GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS") 40 self.send_header("Content-Length", "0") 41 self.end_headers() 42 43 def do_HEAD(self): 44 if self.path == "/hello" or self.path.startswith("/large/"): 45 # no body regardless 46 body_len = 0 47 if self.path.startswith("/large/"): 48 try: 49 n = int(self.path[len("/large/"):]) 50 body_len = max(0, min(n, 10 * 1024 * 1024)) 51 except ValueError: 52 body_len = 0 53 self.send_response(200) 54 self.send_header("X-Upstream", UPSTREAM_NAME) 55 self.send_header("Content-Type", "application/octet-stream") 56 self.send_header("Content-Length", str(body_len)) 57 self.end_headers() 58 return 59 self._send_text(404, "not found\n") 60 61 def do_GET(self): 62 if self.path == "/hello": 63 self._send_text(200, f"Hello from {UPSTREAM_NAME}\n") 64 return 65 if self.path.startswith("/status/"): 66 try: 67 code = int(self.path[len("/status/"):]) 68 except ValueError: 69 code = 500 70 if code < 100 or code > 599: 71 code = 500 72 self._send_text(code, f"status {code}\n") 73 return 74 if self.path.startswith("/large/"): 75 try: 76 n = int(self.path[len("/large/"):]) 77 except ValueError: 78 n = 0 79 n = max(0, min(n, 10 * 1024 * 1024)) 80 buf = bytes(((ord('A') + i % 26) for i in range(n))) 81 self._send_text(200, buf, "application/octet-stream") 82 return 83 if self.path.startswith("/slow/"): 84 try: 85 ms = int(self.path[len("/slow/"):]) 86 except ValueError: 87 ms = 0 88 ms = max(0, min(ms, 30000)) 89 time.sleep(ms / 1000.0) 90 self._send_text(200, "slept\n") 91 return 92 if self.path == "/conn-response": 93 # Name two of our own response headers in Connection: a 94 # conforming proxy must strip both (RFC 9110 ยง7.6.1) but 95 # keep X-Keep-Resp. X-Hop-Before is deliberately emitted 96 # *before* the Connection header that names it, X-Hop-After 97 # *after* it, so the proxy cannot get away with a 98 # single-pass filter. 99 body = b"conn\n" 100 self.send_response(200) 101 self.send_header("X-Upstream", UPSTREAM_NAME) 102 self.send_header("X-Hop-Before", "must-not-leak") 103 self.send_header("Connection", "X-Hop-Before, X-Hop-After") 104 self.send_header("X-Hop-After", "must-not-leak") 105 self.send_header("X-Keep-Resp", "survivor") 106 self.send_header("Content-Type", "text/plain") 107 self.send_header("Content-Length", str(len(body))) 108 self.end_headers() 109 self.wfile.write(body) 110 return 111 if self.path == "/echo-headers": 112 parts = [] 113 for k, v in self.headers.items(): 114 parts.append(f"{k}: {v}\n") 115 self._send_text(200, "".join(parts)) 116 return 117 self._send_text(404, "not found\n") 118 119 def do_POST(self): 120 body = self._read_body() 121 if self.path == "/echo": 122 self._send_text(200, body, "application/octet-stream") 123 return 124 if self.path == "/upload": 125 self._send_text(200, f"Received {len(body)} bytes\n") 126 return 127 self._send_text(404, "not found\n") 128 129 def do_PUT(self): 130 body = self._read_body() 131 if self.path == "/put": 132 self._send_text(200, f"PUT received {len(body)}\n") 133 return 134 self._send_text(404, "not found\n") 135 136 def do_PATCH(self): 137 body = self._read_body() 138 if self.path == "/patch": 139 self._send_text(200, f"PATCH received {len(body)}\n") 140 return 141 self._send_text(404, "not found\n") 142 143 def do_DELETE(self): 144 # drain any body 145 self._read_body() 146 if self.path.startswith("/item"): 147 self.send_response(204) 148 self.send_header("X-Upstream", UPSTREAM_NAME) 149 self.send_header("Content-Length", "0") 150 self.end_headers() 151 return 152 self._send_text(404, "not found\n") 153 154 155 def parse_port(arg): 156 # int("0") would bind an ephemeral port and report itself 157 # listening on 0, and int("garbage") would end in a traceback 158 # rather than in a usable message. 159 try: 160 port = int(arg, 10) 161 except ValueError: 162 port = -1 163 if port < 1 or port > 65535: 164 sys.stderr.write(f"invalid port {arg!r}\n") 165 sys.exit(1) 166 return port 167 168 169 def main(): 170 port = 8403 171 if len(sys.argv) > 1: 172 port = parse_port(sys.argv[1]) 173 # Loopback, not "": this server echoes an arbitrary POST body and 174 # hands out 10 MiB on request, and has no business being on the 175 # network for the duration of `make check'. 176 server = ThreadingHTTPServer(("127.0.0.1", port), Handler) 177 sys.stderr.write(f"upstream_py listening on port {port}\n") 178 sys.stderr.flush() 179 try: 180 server.serve_forever() 181 except KeyboardInterrupt: 182 pass 183 server.server_close() 184 185 186 if __name__ == "__main__": 187 main()