292 lines
8.7 KiB
Python
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
|
|
#
|