paivana

HTTP paywall reverse proxy
Log | Files | Refs | Submodules | README | LICENSE

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