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) - support HTTP/1.1 (no HTTP/1.0 needed)
- urls without / at the end (and %xx codes) - urls without / at the end (and %xx codes)
@ -9,3 +12,10 @@
implementation: implementation:
implemented using sockets and select 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 import os.path, mimetypes
class Server(object): class Server(object):
"""
Asynchronous server class. specify a client handler class and port in the constructor.
"""
def __init__(self, connection_class, port=8080): def __init__(self, connection_class, port=8080):
self.port = port self.port = port
self.connection_class = connection_class self.connection_class = connection_class
@ -13,6 +16,11 @@ class Server(object):
self.handlers = {self.listening_socket: self} self.handlers = {self.listening_socket: self}
self.clients = [] self.clients = []
def run(self): 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] reads = [self.listening_socket] + [c.sock for c in self.clients]
writes = [c.sock for c in self.clients if c.should_write()] writes = [c.sock for c in self.clients if c.should_write()]
others = [c.sock for c in self.clients] others = [c.sock for c in self.clients]
@ -30,19 +38,27 @@ class Server(object):
for c in self.clients: for c in self.clients:
c.try_timeout() c.try_timeout()
def do_read(self): 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() (sock, addrinfo) = self.listening_socket.accept()
c = self.connection_class(self, sock, addrinfo) c = self.connection_class(self, sock, addrinfo)
self.clients.append(c) self.clients.append(c)
self.handlers[sock] = c self.handlers[sock] = c
def client_closed(self, c): def client_closed(self, c):
"""
remove a client from the list and handlers
"""
self.clients.remove(c) self.clients.remove(c)
del self.handlers[c.sock] del self.handlers[c.sock]
print "%d open connections" % len(self.clients) # print "%d open connections" % len(self.clients)
def close(self): def close(self):
""" close the server down and stop listening """
self.listening_socket.close() self.listening_socket.close()
class Client(object): 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): def __init__(self, parent, sock, addrinfo):
self.parent = parent self.parent = parent
self.sock = sock self.sock = sock
@ -56,11 +72,13 @@ class Client(object):
def should_write(self): def should_write(self):
return len(self.write_buf) > 0 or self.to_close return len(self.write_buf) > 0 or self.to_close
def do_read(self): def do_read(self):
"""called by the event loop when the socket is readable"""
data = self.sock.recv(4096) data = self.sock.recv(4096)
if not data: return self.close() if not data: return self.close()
self.read_buf += data self.read_buf += data
self.on_data() self.on_data()
def do_write(self): def do_write(self):
"""called by the event loop when the socket is writable"""
try: try:
if self.write_buf: if self.write_buf:
no_written = self.sock.send(self.write_buf) no_written = self.sock.send(self.write_buf)
@ -83,7 +101,7 @@ class Client(object):
self.to_close = True self.to_close = True
class EchoClient(Client): class EchoClient(Client):
"""docstring for EchoClient""" """EchoClient is an example Client handler that implements an echo server"""
def __init__(self, parent, sock, addrinfo): def __init__(self, parent, sock, addrinfo):
super(EchoClient, self).__init__(parent, sock, addrinfo) super(EchoClient, self).__init__(parent, sock, addrinfo)
def on_data(self): def on_data(self):

View File

@ -10,7 +10,7 @@ import asyncserver
codes = { codes = {
200: "OK", 200: "OK",
301: "Moved Permanently", # should this be a 301 301: "Moved Permanently",
304: "Not Modified", 304: "Not Modified",
404: "Not Found" 404: "Not Found"
} }
@ -22,11 +22,15 @@ def ETag(data):
return hashlib.sha1(data).hexdigest() return hashlib.sha1(data).hexdigest()
class HTTPClient(asyncserver.Client): class HTTPClient(asyncserver.Client):
"""docstring for HTTPClient""" """HTTPClient implements a HTTP handler class for asyncserver.Server"""
def __init__(self, parent, sock, addrinfo): def __init__(self, parent, sock, addrinfo):
super(HTTPClient, self).__init__(parent, sock, addrinfo) super(HTTPClient, self).__init__(parent, sock, addrinfo)
self.set_timeout() self.set_timeout()
def on_data(self): 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() self.set_timeout()
try: try:
idx = self.read_buf.index("\r\n\r\n") idx = self.read_buf.index("\r\n\r\n")
@ -38,8 +42,10 @@ class HTTPClient(asyncserver.Client):
self.on_request(headers) self.on_request(headers)
self.on_data() # there might be more in the read_buf. self.on_data() # there might be more in the read_buf.
def set_timeout(self): def set_timeout(self):
"""reset the timeout to 10 seconds"""
self.timeout = datetime.today() + timedelta(seconds=10) self.timeout = datetime.today() + timedelta(seconds=10)
def on_request(self, req): def on_request(self, req):
"""method called when there is a http request"""
headers = req.split('\r\n') headers = req.split('\r\n')
if not (headers[0].startswith("GET ") and headers[0].endswith("HTTP/1.1")): if not (headers[0].startswith("GET ") and headers[0].endswith("HTTP/1.1")):
print(headers[0]) print(headers[0])
@ -57,18 +63,22 @@ class HTTPClient(asyncserver.Client):
except KeyError: except KeyError:
pass pass
def on_GET(self, url, headers): def on_GET(self, url, headers):
"""GET doesn't do anything in this class"""
print("got GET", url, headers) print("got GET", url, headers)
def next_timeout(self): def next_timeout(self):
"""called from the event loop to figure out when the next timeout should be"""
if self.timeout: if self.timeout:
return (self.timeout - datetime.today()).total_seconds() return (self.timeout - datetime.today()).total_seconds()
else: else:
return None return None
def try_timeout(self): def try_timeout(self):
"""called by the event loop on every run, it doesn't keep track of timeouts."""
if self.timeout < datetime.today(): if self.timeout < datetime.today():
print("connection timed out") print("connection timed out")
self.timeout = None self.timeout = None
self.close() self.close()
def send_file(self, code, headers, fname, orig_etag): 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) (type, encoding) = mimetypes.guess_type(fname, strict = False)
if type: if type:
headers["Content-Type"] = type headers["Content-Type"] = type
@ -81,6 +91,7 @@ class HTTPClient(asyncserver.Client):
headers["ETag"] = etag headers["ETag"] = etag
self.send_response(code, headers, data) self.send_response(code, headers, data)
def send_response(self, 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["Content-Length"] = len(data)
headers["Date"] = httpdate() headers["Date"] = httpdate()
name = codes[code] name = codes[code]
@ -93,7 +104,7 @@ class HTTPClient(asyncserver.Client):
class HTTPDirClient(HTTPClient): 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): def __init__(self, parent, sock, addrinfo):
super(HTTPDirClient, self).__init__(parent, sock, addrinfo) super(HTTPDirClient, self).__init__(parent, sock, addrinfo)
self.directory = "content" self.directory = "content"
@ -108,11 +119,15 @@ class HTTPDirClient(HTTPClient):
return return
print "GET", url print "GET", url
if os.path.isdir(reqpath) and not url.endswith('/'): 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 + '/'}, "") self.send_response(301, {"Location": url + '/'}, "")
reqpath += "/index.html" return
if os.path.isdir(reqpath) and url.endswith('/'): if os.path.isdir(reqpath) and url.endswith('/'):
# try and serve index.html
reqpath += "/index.html" reqpath += "/index.html"
if not os.path.isfile(reqpath): if not os.path.isfile(reqpath):
# 404 response if not found
self.send_response(404, {}, "Not Found, sorry\n") self.send_response(404, {}, "Not Found, sorry\n")
else: else:
etag = None etag = None