#!/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() # To recap we just did a bunch of work, for no user visible change # This is not a bad thing, often the first drafts of programs. # Will fit the requirements of the moment. But when the requirements # change the program must be adapted to fit.abs # This process of iteration and redesign, # is called "paying down technical debt", and it should be done whenever # possible. # # # References # MDN Web Docs #