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