import os.path, mimetypes from urllib import unquote import hashlib from datetime import datetime, timedelta import sys import asyncserver codes = { 200: "OK", 301: "Moved Permanently", 304: "Not Modified", 404: "Not Found" } from email.utils import formatdate def httpdate(): return formatdate(timeval=None, localtime=False, usegmt=True) def ETag(data): return hashlib.sha1(data).hexdigest() class HTTPClient(asyncserver.Client): """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") except ValueError: return else: headers = self.read_buf[:idx] self.read_buf = self.read_buf[idx+4:] 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]) print("malformed request:", headers) return url = headers[0][4:-9] hdr = {} for h in headers[1:]: x = h.split(': ') hdr[x[0]] = x[1] self.on_GET(url, hdr) try: if hdr['Connection'] == 'close': self.close() 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 with open(fname) as f: data = f.read() etag = ETag(data) if etag == orig_etag: self.send_response(304, {}, "") else: 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] resp = "HTTP/1.1 %d %s\r\n" % (code, name) for n, val in headers.iteritems(): resp += "%s: %s\r\n" % (n, val) resp += "\r\n" + data self.write(resp) print "%s %s" % (code, codes[code]) class HTTPDirClient(HTTPClient): """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" print "Opened connection" def on_GET(self, url, headers): if not url: url = "/" else: url = unquote(url) reqpath = os.path.abspath("./content" + url) # make sure that we can't go outside the content dir if not reqpath.startswith(os.path.abspath("./content/")): self.send_response(404, {}, "Not Found, sorry\n") 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 + '/'}, "") 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 if "If-None-Match" in headers: etag = headers['If-None-Match'] self.send_file(200, {}, reqpath, etag) if __name__ == '__main__': try: port = int(sys.argv[1]) except (ValueError, IndexError): print "invalid port given" else: s = asyncserver.Server(HTTPDirClient, port = port) try: while 1: s.run() except: s.close() raise