#!/usr/bin/env python3 """Simple HTTP server that responds 200 with a test message including client IP. Behavior: - GET returns HTTP/1.1 200 with a message including the requester IP - HEAD returns the same headers as GET but no body - Displays whether any incoming X-* headers were detected and lists them - If User-Agent contains "curl" or "wget" (case-insensitive) the server responds with Content-Type: text/plain; otherwise Content-Type: text/html. - --look controls which HTML variant is returned for non-CLI agents: * basic - plain HTML with no external references * nice - includes Google Font "Noto Sans" (default) * bootstrap - Bootstrap 5 layout - The URL query parameter "look" (e.g. /?look=nice) overrides the command-line --look for that request only. Values are case-insensitive and must be one of basic,nice,bootstrap. Usage: python3 ok_server.py # binds 0.0.0.0:8000, nice look python3 ok_server.py --look basic python3 ok_server.py -b 127.0.0.1 -p 8080 --look bootstrap Test: curl -i http://localhost:8000/ # text/plain for curl curl -i "http://localhost:8000/?look=basic" # request-level override curl -I http://localhost:8000/ # HEAD (headers only) wget -S -O - http://localhost:8000/ # text/plain for wget open http://localhost:8000/ # browser gets HTML variant per look """ import argparse import sys from html import escape from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from typing import Tuple from urllib.parse import urlparse, parse_qs PLAIN_TEMPLATE = ( "Hello, This is a test HTTP server.\n\n" "Your request came from {ip}.\n\n" "{proxy_headers_block}" "Have a nice day!\n" ) HTML_BASIC = """ Test HTTP server

Hello,

This is a test HTTP server.

Your request came from {ip}.

{proxy_headers_html}

Have a nice day!

""" HTML_NICE = """ Test HTTP server

Hello,

This is a test HTTP server.

Your request came from {ip}.

{proxy_headers_html}

Have a nice day!

""" HTML_BOOTSTRAP = """ Test HTTP server

Hello,

This is a test HTTP server.

Your request came from {ip}.

{proxy_headers_html}

Have a nice day!

""" VALID_LOOKS = ("basic", "nice", "bootstrap") class OkHandler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" def _collect_x_headers(self): x_headers = [] for name, value in self.headers.items(): if name.lower().startswith("x-"): x_headers.append((name, value)) return sorted(x_headers, key=lambda item: item[0].lower()) def _proxy_markup(self, x_headers): if not x_headers: return "", "" plain_lines = [f" - {name}: {value}" for name, value in x_headers] proxy_headers_block = ( "Reverse proxy condition: detected via X-* headers.\n" "X-* headers:\n" + "\n".join(plain_lines) + "\n\n" ) html_items = "".join( [f"
  • {escape(name)}: {escape(value)}
  • " for name, value in x_headers] ) proxy_headers_html = ( "

    Reverse proxy condition: detected via X-* headers.

    " "

    X-* headers:

    " ) return proxy_headers_block, proxy_headers_html def _is_cli_agent(self, ua: str) -> bool: if not ua: return False ua_l = ua.lower() return ("curl" in ua_l) or ("wget" in ua_l) def _get_request_look(self) -> str: """Return look from URL query (if valid) or None.""" # parse query string from the request path parsed = urlparse(self.path) qs = parse_qs(parsed.query) look_vals = qs.get("look") if not look_vals: return None # use first value, case-insensitive look = look_vals[0].strip().lower() if look in VALID_LOOKS: return look return None def _html_for_look(self, look: str, ip: str, proxy_headers_html: str) -> str: if look == "basic": return HTML_BASIC.format( ip=ip, proxy_headers_html=proxy_headers_html, ) if look == "nice": return HTML_NICE.format( ip=ip, proxy_headers_html=proxy_headers_html, ) # fallback to bootstrap return HTML_BOOTSTRAP.format( ip=ip, proxy_headers_html=proxy_headers_html, ) def _make_body_and_type(self, client_ip: str, user_agent: str) -> Tuple[bytes, str]: x_headers = self._collect_x_headers() proxy_headers_block, proxy_headers_html = self._proxy_markup(x_headers) if self._is_cli_agent(user_agent): body = PLAIN_TEMPLATE.format( ip=client_ip, proxy_headers_block=proxy_headers_block, ).encode("utf-8") ctype = "text/plain; charset=utf-8" return body, ctype # Check request-level override first request_look = self._get_request_look() if request_look: look = request_look else: # fallback to server-level default (now: nice) look = getattr(self.server, "look", "nice") html = self._html_for_look(look, client_ip, proxy_headers_html) body = html.encode("utf-8") ctype = "text/html; charset=utf-8" return body, ctype def _send_ok_headers(self, content_type: str, body_len: int): self.send_response(200) self.send_header("Content-Type", content_type) self.send_header("Content-Length", str(body_len)) self.end_headers() def do_GET(self): client_ip = self.client_address[0] user_agent = self.headers.get("User-Agent", "") body, ctype = self._make_body_and_type(client_ip, user_agent) try: self._send_ok_headers(ctype, len(body)) self.wfile.write(body) except BrokenPipeError: # client closed connection early; ignore pass def do_HEAD(self): client_ip = self.client_address[0] user_agent = self.headers.get("User-Agent", "") body, ctype = self._make_body_and_type(client_ip, user_agent) # send same headers as GET but no body self._send_ok_headers(ctype, len(body)) # minimal single-line log to stderr def log_message(self, format, *args): sys.stderr.write("%s - - [%s] %s\n" % (self.client_address[0], self.log_date_time_string(), format % args)) def run(bind: str, port: int, look: str): addr = (bind, port) try: with ThreadingHTTPServer(addr, OkHandler) as httpd: # attach chosen look to server so handlers can use it as default httpd.look = look print(f"Serving on {bind}:{port} (default look={look}) (Ctrl-C to stop)") httpd.serve_forever() except KeyboardInterrupt: print("\nShutting down server") except Exception as e: print("Server error:", e, file=sys.stderr) raise if __name__ == "__main__": parser = argparse.ArgumentParser(description="HTTP OK test server") parser.add_argument("-b", "--bind", default="0.0.0.0", help="Address to bind to (default: 0.0.0.0)") parser.add_argument("-p", "--port", type=int, default=8000, help="Port to listen on (default: 8000)") parser.add_argument("--look", choices=VALID_LOOKS, default=VALID_LOOKS[1], help="Default HTML look for non-cli agents (basic, nice, bootstrap). Default: nice") args = parser.parse_args() run(args.bind, args.port, args.look)