add documentation

master
Yorick van Pelt 2015-04-03 00:52:40 +02:00
parent fb38c97baa
commit 6dcd072d50
3 changed files with 50 additions and 7 deletions

View File

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

View File

@ -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):

View File

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