152 lines
4.4 KiB
Python
152 lines
4.4 KiB
Python
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
|