From 6dcd072d500b1ee60f6bba9e61089a4fb94f74bc Mon Sep 17 00:00:00 2001 From: Yorick van Pelt Date: Fri, 3 Apr 2015 00:52:40 +0200 Subject: [PATCH] add documentation --- documentation_file.txt | 10 ++++++++++ webserver/asyncserver.py | 24 +++++++++++++++++++++--- webserver/server.py | 23 +++++++++++++++++++---- 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/documentation_file.txt b/documentation_file.txt index c5cceed..c966e19 100644 --- a/documentation_file.txt +++ b/documentation_file.txt @@ -1,3 +1,6 @@ +Yorick van Pelt +s4503678 + - support HTTP/1.1 (no HTTP/1.0 needed) - urls without / at the end (and %xx codes) @@ -9,3 +12,10 @@ implementation: implemented using sockets and select +asyncserver.py contains the basic asynchronous server implementation; +clients are stored in a list, and the event loop calls select() with a list of their sockets. Then, the clients are notified if there are any readable/writable/etc sockets that they own. The client connection class that is used can be specified when making the Server object. + +server.py builds on it to implement a http server, with HTTPClient implementing the basic HTTP server, and HTTPDirClient implementing the static file server. + +difficulties: +Testing took more time than desired, I wasn't sure if I was allowed to use the standard http library (or if it supports the neccesary tests without too much trouble), so I implemented some client functions. diff --git a/webserver/asyncserver.py b/webserver/asyncserver.py index 67b4e95..6e5bbff 100644 --- a/webserver/asyncserver.py +++ b/webserver/asyncserver.py @@ -2,6 +2,9 @@ import socket, select import os.path, mimetypes class Server(object): + """ + Asynchronous server class. specify a client handler class and port in the constructor. + """ def __init__(self, connection_class, port=8080): self.port = port self.connection_class = connection_class @@ -13,6 +16,11 @@ class Server(object): self.handlers = {self.listening_socket: self} self.clients = [] def run(self): + """ + run the event loop + - collect sockets and min timeout + - do the select call and notify clients + """ reads = [self.listening_socket] + [c.sock for c in self.clients] writes = [c.sock for c in self.clients if c.should_write()] others = [c.sock for c in self.clients] @@ -30,19 +38,27 @@ class Server(object): for c in self.clients: c.try_timeout() def do_read(self): + """ + do_read method on the server itself (it's also in the select). + accepts a connection. + """ (sock, addrinfo) = self.listening_socket.accept() c = self.connection_class(self, sock, addrinfo) self.clients.append(c) self.handlers[sock] = c def client_closed(self, c): + """ + remove a client from the list and handlers + """ self.clients.remove(c) del self.handlers[c.sock] - print "%d open connections" % len(self.clients) + # print "%d open connections" % len(self.clients) def close(self): + """ close the server down and stop listening """ self.listening_socket.close() class Client(object): - """docstring for Client""" + """Client is the generic Server handler class. look at HTTPClient for more documentation""" def __init__(self, parent, sock, addrinfo): self.parent = parent self.sock = sock @@ -56,11 +72,13 @@ class Client(object): def should_write(self): return len(self.write_buf) > 0 or self.to_close def do_read(self): + """called by the event loop when the socket is readable""" data = self.sock.recv(4096) if not data: return self.close() self.read_buf += data self.on_data() def do_write(self): + """called by the event loop when the socket is writable""" try: if self.write_buf: no_written = self.sock.send(self.write_buf) @@ -83,7 +101,7 @@ class Client(object): self.to_close = True class EchoClient(Client): - """docstring for EchoClient""" + """EchoClient is an example Client handler that implements an echo server""" def __init__(self, parent, sock, addrinfo): super(EchoClient, self).__init__(parent, sock, addrinfo) def on_data(self): diff --git a/webserver/server.py b/webserver/server.py index a854e02..ffb2c0f 100644 --- a/webserver/server.py +++ b/webserver/server.py @@ -10,7 +10,7 @@ import asyncserver codes = { 200: "OK", - 301: "Moved Permanently", # should this be a 301 + 301: "Moved Permanently", 304: "Not Modified", 404: "Not Found" } @@ -22,11 +22,15 @@ def ETag(data): return hashlib.sha1(data).hexdigest() class HTTPClient(asyncserver.Client): - """docstring for HTTPClient""" + """HTTPClient implements a HTTP handler class for asyncserver.Server""" def __init__(self, parent, sock, addrinfo): super(HTTPClient, self).__init__(parent, sock, addrinfo) self.set_timeout() def on_data(self): + """method called when there is data available""" + # resetting the timeout everytime there is data makes it vulnerable + # to the slowloris attack, but connections are lightweight, so there + # can be a lot of them easily. self.set_timeout() try: idx = self.read_buf.index("\r\n\r\n") @@ -38,8 +42,10 @@ class HTTPClient(asyncserver.Client): self.on_request(headers) self.on_data() # there might be more in the read_buf. def set_timeout(self): + """reset the timeout to 10 seconds""" self.timeout = datetime.today() + timedelta(seconds=10) def on_request(self, req): + """method called when there is a http request""" headers = req.split('\r\n') if not (headers[0].startswith("GET ") and headers[0].endswith("HTTP/1.1")): print(headers[0]) @@ -57,18 +63,22 @@ class HTTPClient(asyncserver.Client): except KeyError: pass def on_GET(self, url, headers): + """GET doesn't do anything in this class""" print("got GET", url, headers) def next_timeout(self): + """called from the event loop to figure out when the next timeout should be""" if self.timeout: return (self.timeout - datetime.today()).total_seconds() else: return None def try_timeout(self): + """called by the event loop on every run, it doesn't keep track of timeouts.""" if self.timeout < datetime.today(): print("connection timed out") self.timeout = None self.close() def send_file(self, code, headers, fname, orig_etag): + """send a file or 304 response if the etag matches""" (type, encoding) = mimetypes.guess_type(fname, strict = False) if type: headers["Content-Type"] = type @@ -81,6 +91,7 @@ class HTTPClient(asyncserver.Client): headers["ETag"] = etag self.send_response(code, headers, data) def send_response(self, code, headers, data): + """send a http response. generate content-length and date headers here.""" headers["Content-Length"] = len(data) headers["Date"] = httpdate() name = codes[code] @@ -93,7 +104,7 @@ class HTTPClient(asyncserver.Client): class HTTPDirClient(HTTPClient): - """docstring for HTTPDirClient""" + """HTTPDirClient implements a static file server from a directory on top of HTTPClient""" def __init__(self, parent, sock, addrinfo): super(HTTPDirClient, self).__init__(parent, sock, addrinfo) self.directory = "content" @@ -108,11 +119,15 @@ class HTTPDirClient(HTTPClient): return print "GET", url if os.path.isdir(reqpath) and not url.endswith('/'): + # http redirect to add a / + # should this be a 301? self.send_response(301, {"Location": url + '/'}, "") - reqpath += "/index.html" + return if os.path.isdir(reqpath) and url.endswith('/'): + # try and serve index.html reqpath += "/index.html" if not os.path.isfile(reqpath): + # 404 response if not found self.send_response(404, {}, "Not Found, sorry\n") else: etag = None