net-httpserver/webserver/server.py

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