add documentation
parent
fb38c97baa
commit
6dcd072d50
|
@ -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.
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue