misc/http.py
2025-09-06 08:54:57 -04:00

292 lines
8.7 KiB
Python

#!/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 more robust, but still 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.
CRLF = "\r\n"
LF = "\n"
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
if "Host" not in self["headers"]:
self["headers"]["Host"] = "localhost"
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(CRLF + CRLF)
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)
# Foreshadowing (n): A literary device in which an author ...
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(CRLF + CRLF) # Per RFC 9112
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
# This will still accept anything as long as the method verb
# is correct. Clients need not conform to RFC 9112 (yet).
# We however, do our best effort to conform to rfc 9112,
# when sending responses.
#
# The principal of being lienant in what you accept from
# the client, but strict in what you send back, was first
# was first forumlated by John Postel in that later half
# of the 1970s.
# Doing this, is also motivation for me to write Parts 4, 5, and 6
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.
# This process of iteration and redesign,
# is called "paying down technical debt", and it should be done whenever
# possible.
#
# And we've just moved up to the second level of the 7 story mountain
# Yay us.
#
# References
# Robustness Principal (Devopedia): https://devopedia.org/postel-s-law
# IETF RFC 9112 HTTP/1.1 https://datatracker.ietf.org/doc/html/rfc9112
#