Draft for next blog post

This commit is contained in:
Matt Arnold 2025-09-05 22:52:22 -04:00
parent 6be00706aa
commit 55f5d15b5a

259
http.py Normal file
View File

@ -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
#