From 55f5d15b5af10c72ae926a2f5bb52a62f5a8f995 Mon Sep 17 00:00:00 2001 From: Matt Arnold Date: Fri, 5 Sep 2025 22:52:22 -0400 Subject: [PATCH] Draft for next blog post --- http.py | 259 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 http.py diff --git a/http.py b/http.py new file mode 100644 index 0000000..a901f01 --- /dev/null +++ b/http.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +## This Blog post is executable python code, it requires the gevent modules to run +# pip as gevent, Debian as python3-gevent. + +import os +import sys +import gevent +import gevent.socket as socket +import signal +from io import StringIO +from email.utils import formatdate + +Fork = False +# The next post in this series,which now has the name +# __The Network Programing Purgatorio__ by the way. +# Was originally going to be about how to use TLS +# But it turns out just kinda faking your http implementation +# Makes browsers really angry. When combined with encryption +# and security. + +# Thus we have to devote some time and energy to making +# a better fake http. We're still going to employ some +# dirty tricks here. +# At this point there if you didn't read Greg the Manokit's +# Warning at the top of the page +# I will say it again +# DON'T use this code in production, it is for educational purposes only +# While i make my best effort on security and programing defensively +# I can't make any garuntees. There are better http servers out there +# Use Them. + +# we need two helper classes to do a better job at http +# i suspect this one might be ai generated. +# It's a subclass of dict with key access via the dot +# operator. Similar to how ruby does things +# I don't know why this isn't the default +# i've coded this dozens of times but i couldn't use +# client's code in a public project. +# Thus cut 'n' paste. + + +class AccessDict(dict): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Convert nested dicts to AccessDict + for key, value in self.items(): + if isinstance(value, dict) and not isinstance(value, AccessDict): + self[key] = AccessDict(value) + + def __getattr__(self, key): + try: + return self[key] + except KeyError: + raise AttributeError( + f"'{type(self).__name__}' object has no attribute '{key}'" + ) + + def __setattr__(self, key, value): + # Convert nested dicts to AccessDict + if isinstance(value, dict) and not isinstance(value, AccessDict): + value = AccessDict(value) + self[key] = value + + def __delattr__(self, key): + try: + del self[key] + except KeyError: + raise AttributeError( + f"'{type(self).__name__}' object has no attribute '{key}'" + ) + + +# These are request and response objects similar to Go's standard +# net/http package. Again this is still a toy implementation. +# In a future part. I will go over how to make a more full featured +# implementation. This is just making us ssl ready and laying the ground work +class HttpRequest(AccessDict): + def __init__( + self, method="GET", path="/", headers={}, body="goodbye\r\n", *args, **kwargs + ): + super().__init__(*args, **kwargs) + self["method"] = method + self["headers"] = headers + self["body"] = StringIO(body) + self["path"] = path + + def read(self, seek): + return self["body"].read(seek) + + def __str__(self): + buf = StringIO() + buf.write(f"{self.method} {self.path} HTTP/1.1") + for k, v in self["headers"].items(): + buf.write(f"{k}: {v}\r\n") + buf.write(self["body"].getvalue() + "\r\n") + return buf.getvalue() + "\r\n" + + +class HttpResponse(AccessDict): + def __init__(self, status="404", headers={}, body="goodbye\r\n", *args, **kwargs): + super().__init__(*args, **kwargs) + self["status"] = status + self["headers"] = headers + self["body"] = StringIO() + # We Must have date and host headers set correctly to use tls + # so we unconditionally set them here + if "host" not in kwargs: + self["headers"]["Host"] = "localhost" + else: + self["headers"]["Host"] = kwargs["host"] + + self["headers"]["Date"] = formatdate(timeval=None, localtime=False, usegmt=True) + self["headers"]["Content-Type"] = "text/plain; charset=UTF-8" + + def write(self, stuff): + return self.body.write(stuff) + + def __str__(self): + buf = StringIO() + print(self.headers) + buf.write(f"HTTP/1.1 {self.status}\r\n ") + length = len(self["body"].getvalue()) + for k, v in self["headers"].items(): + buf.write(f"{k}: {v}\r\n") + buf.write(f"Content-Length: {length}\r\n") + buf.write(self["body"].getvalue() + "\r\n") + return buf.getvalue() + "\r\n" + + +RICKROLL_LYRICS = """ + +We're no strangers to love +You know the rules and so do I +A full commitment's what I'm thinkin' of +You wouldn't get this from any other guy + +I just wanna tell you how I'm feeling +Gotta make you understand + +Never gonna give you up, never gonna let you down +Never gonna run around and desert you +Never gonna make you cry, never gonna say goodbye +Never gonna tell a lie and hurt you + +We've known each other for so long +Your heart's been aching, but you're too shy to say it +Inside, we both know what's been going on +We know the game and we're gonna play it + +And if you ask me how I'm feeling +Don't tell me you're too blind to see + +Never gonna give you up, never gonna let you down +Never gonna run around and desert you +Never gonna make you cry, never gonna say goodbye +Never gonna tell a lie and hurt you + +Never gonna give you up, never gonna let you down +Never gonna run around and desert you +Never gonna make you cry, never gonna say goodbye +Never gonna tell a lie and hurt you + +""" +good_response = HttpResponse() +good_response.status = 200 +good_response.headers["Last-Modified"] = "Mon, 27 July 1987 00:00 GMT" +good_response.write(RICKROLL_LYRICS) + +error_response = HttpResponse() +error_response.status = 405 # a 405 here is closer to RFC compliant +error_response.write("Together forever and never to part Together forever we two") + + +client_procs = [] + +svr_proc = None + + +# You've Seen all this before, in the last Part 1. I will shorten commentary +class NullDevice: + def write(self, s): + pass + + +def hup_handle(sig, fr): + sys.exit() + + +def server_handler(): + serversock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + serversock.bind(("", 1337)) + serversock.listen(10) + while True: + client, addr = serversock.accept() + print(addr) + client_procs.append(gevent.spawn(client_handler, client)) + gevent.sleep(0.25) + + serversock.close() + return + + +# I made two simple changes to make it use our new http objects + + +def client_handler(sock): + print("Client handler spawn") + junk_counter = 0 + while True: + if junk_counter > 3: + sock.close() + return + data = sock.recv(4096) + dstring = data.decode("UTF-8") + if dstring.startswith("GET"): + break + else: + error = str(error_response) + sock.send(error.encode("utf-8")) + junk_counter += 1 + gevent.sleep(0.25) + default = str(good_response) + sock.send(default.encode("utf-8")) + sock.close() + return + + +def daemon_main(): + svr_proc = gevent.spawn(server_handler) + client_procs.append(svr_proc) + gevent.joinall(client_procs) + sys.exit(0) + + +# so things will not fork while i'm debbuging +if not Fork: + daemon_main() + +pid = os.fork() +if pid: + os._exit(0) + + +else: + os.setpgrp() + os.umask(0) + + print(os.getpid()) + sys.stdout = NullDevice() + sys.stderr = NullDevice() + + signal.signal(signal.SIGHUP, hup_handle) + signal.signal(signal.SIGTERM, hup_handle) + daemon_main() + +# +# References +# MDN Web Docs +#