|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# ==================================================================== |
| 3 | +# Licensed to the Apache Software Foundation (ASF) under one |
| 4 | +# or more contributor license agreements. See the NOTICE file |
| 5 | +# distributed with this work for additional information |
| 6 | +# regarding copyright ownership. The ASF licenses this file |
| 7 | +# to you under the Apache License, Version 2.0 (the |
| 8 | +# "License"); you may not use this file except in compliance |
| 9 | +# with the License. You may obtain a copy of the License at |
| 10 | +# |
| 11 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 12 | +# |
| 13 | +# Unless required by applicable law or agreed to in writing, |
| 14 | +# software distributed under the License is distributed on an |
| 15 | +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| 16 | +# KIND, either express or implied. See the License for the |
| 17 | +# specific language governing permissions and limitations |
| 18 | +# under the License. |
| 19 | +# ==================================================================== |
| 20 | + |
| 21 | +""" |
| 22 | +A simple threaded HTTP server that requires HTTP Basic auth for all |
| 23 | +requests, but doesn't bother to check credentials. Generates random |
| 24 | +text data for GET and fakes random response size for HEAD requests. |
| 25 | +""" |
| 26 | + |
| 27 | +from base64 import standard_b64encode |
| 28 | +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer |
| 29 | +from random import randbytes, randint |
| 30 | +from time import sleep |
| 31 | +from typing import Generator |
| 32 | + |
| 33 | + |
| 34 | +class Handler(BaseHTTPRequestHandler): |
| 35 | + LATENCY = 1.0 # Average request latency in seconds |
| 36 | + LENGTH = 68 # Base-64 response line length |
| 37 | + MIN_SIZE = 2000 # Decoded response data min and max sizes |
| 38 | + MAX_SIZE = 7000 |
| 39 | + |
| 40 | + def handle_one_request(self) -> None: |
| 41 | + self.close_connection = False |
| 42 | + self.protocol_version = "HTTP/1.1" |
| 43 | + self.server_version = "AuthServer/36" |
| 44 | + return super().handle_one_request() |
| 45 | + |
| 46 | + def do_HEAD(self) -> None: |
| 47 | + """like do_GET() but don't generate response data.""" |
| 48 | + if self._check_auth(): |
| 49 | + self._add_latency() |
| 50 | + length = randint(self.MIN_SIZE, self.MAX_SIZE) |
| 51 | + self.send_response(200) |
| 52 | + self._send_headers(length) |
| 53 | + |
| 54 | + def do_GET(self) -> None: |
| 55 | + """Return a random-sized text response.""" |
| 56 | + if self._check_auth(): |
| 57 | + self._add_latency() |
| 58 | + data = self._make_random_data() |
| 59 | + self.send_response(200) |
| 60 | + self._send_headers(len(data)) |
| 61 | + self.wfile.write(data) |
| 62 | + |
| 63 | + def _check_auth(self) -> bool: |
| 64 | + """Require that authentication data is present.""" |
| 65 | + if self.headers.get("Authorization") is None: |
| 66 | + message = b"Authentication required\n" |
| 67 | + self.send_response(401) |
| 68 | + self.send_header("WWW-Authenticate", "Basic realm=AuthServer") |
| 69 | + self._send_headers(len(message), error=True) |
| 70 | + self.wfile.write(message) |
| 71 | + return False |
| 72 | + return True |
| 73 | + |
| 74 | + def _send_headers(self, length: int, error: bool = False) -> None: |
| 75 | + """Send a standard set of response headers.""" |
| 76 | + if error: |
| 77 | + self.close_connection = True |
| 78 | + self.send_header("Connection", "close") |
| 79 | + self.send_header("Content-Type", "text/plain") |
| 80 | + self.send_header("Content-Length", str(length)) |
| 81 | + if not error: |
| 82 | + self.send_header("Last-Modified", self.date_time_string()) |
| 83 | + self.end_headers() |
| 84 | + |
| 85 | + @classmethod |
| 86 | + def _add_latency(cls) -> None: |
| 87 | + """Add random response latency.""" |
| 88 | + sleep(cls.LATENCY * randint(50, 150) / 100) |
| 89 | + |
| 90 | + @classmethod |
| 91 | + def _make_random_data(cls) -> bytes: |
| 92 | + """Generate Base64-encoded random data with constraind line length.""" |
| 93 | + def splitlines(data: bytes) -> Generator[bytes, None, None]: |
| 94 | + for start in range(0, len(data), cls.LENGTH): |
| 95 | + if len(data) - cls.LENGTH < start: |
| 96 | + end = len(data) |
| 97 | + else: |
| 98 | + end = start + cls.LENGTH |
| 99 | + yield data[start:end] + b"\n" |
| 100 | + |
| 101 | + data = randbytes(randint(cls.MIN_SIZE, cls.MAX_SIZE)) |
| 102 | + return b"".join(splitlines(standard_b64encode(data))) |
| 103 | + |
| 104 | + |
| 105 | +def serve() -> None: |
| 106 | + """Run the server.""" |
| 107 | + addr, port = "127.0.0.1", 8087 |
| 108 | + print(f"Listening on http://{addr}:{port}. Press ^C to stop.") |
| 109 | + ThreadingHTTPServer((addr, port), Handler).serve_forever() |
| 110 | + |
| 111 | + |
| 112 | +if __name__ == "__main__": |
| 113 | + serve() |
0 commit comments