Files
sshworkshop/namepicker/webserver.py
2025-11-04 21:10:31 +01:00

122 lines
4.0 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Simple HTTP server that works on both IPv4 and IPv6.
It prints the client address (src) and the local address (dst) for each request.
"""
import logging
import socket
from http.server import BaseHTTPRequestHandler, HTTPServer
from socketserver import TCPServer
## wrangled together by stackoverflow & duck.ai GPT-OSS 120B to add dualstack support
class S(BaseHTTPRequestHandler):
def _set_response(self):
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
def do_GET(self):
logging.info(
"GET request,\nPath: %s\nHeaders:\n%s\n",
self.path,
self.headers,
)
self._set_response()
self.wfile.write("<body><pre>".encode())
# echo request info
self.wfile.write(
f"GET request for {self.path} from {self.headers}\n".encode()
)
self.wfile.write(
f"client (src_ip, src_port) = {self.client_address}\n".encode()
)
self.wfile.write(
f"local (dst_ip, dst_port) = {self.request.getsockname()}\n".encode()
)
self.wfile.write("</pre></body>".encode())
def do_POST(self):
length = int(self.headers.get('Content-Length', 0))
post_data = self.rfile.read(length)
logging.info(
"POST request,\nPath: %s\nHeaders:\n%s\n\nBody:\n%s\n",
self.path,
self.headers,
post_data.decode('utf-8', errors='replace')
)
self._set_response()
self.wfile.write(f"POST request for {self.path}".encode())
def make_dualstack_server(host: str, port: int, handler):
"""
Return a TCPServer that listens on both IPv4 and IPv6.
The socket is created, configured and bound **once** by us,
then handed to TCPServer. We only call `server_activate()`.
"""
# 1⃣ IPv6 socket
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
# 2⃣ Enable dualstack (IPv4mapped IPv6) on platforms where the
# default is IPv6only (macOS, Windows)
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
# 3⃣ Reuse address convenient during development
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 4⃣ Bind the socket ourselves
sock.bind((host, port))
# 5⃣ Build the server **without** the automatic bind/activate step
server = TCPServer(
server_address=None, # not used because we bind manually
RequestHandlerClass=handler,
bind_and_activate=False,
)
server.socket = sock # replace the internal socket
# server.server_address is optional; we set it for nice repr()
server.server_address = (host, port)
# 6⃣ Only activate (listen) do NOT call server_bind() again
server.server_activate()
return server
# ----------------------------------------------------------------------
# Main entry point
# ----------------------------------------------------------------------
def run(host: str = "", port: int = 8080):
"""
Starts the HTTP server.
* host = '' → listen on all interfaces (IPv4 & IPv6)
* host = '::' → same as '' but explicit IPv6
* host = '0.0.0.0' → IPv4 only (still works with dualstack socket)
"""
logging.basicConfig(level=logging.INFO,
format='%(asctime)s %(levelname)s %(message)s')
httpd = make_dualstack_server(host, port, S)
logging.info(f"Starting HTTP server on {host or '::'}:{port} (dualstack)")
try:
httpd.serve_forever()
except KeyboardInterrupt:
logging.info("Keyboard interrupt shutting down")
finally:
httpd.server_close()
logging.info("Server stopped")
if __name__ == '__main__':
import sys
if len(sys.argv) == 3:
# usage: script.py <host> <port>
run(host=sys.argv[1], port=int(sys.argv[2]))
elif len(sys.argv) == 2:
# usage: script.py <port>
run(port=int(sys.argv[1]))
else:
run()