net-httpserver/webserver/server.py

152 lines
4.4 KiB
Python
Raw Normal View History

2015-03-01 21:52:24 +01:00
import os.path, mimetypes
from urllib import unquote
import hashlib
2015-03-01 22:14:10 +01:00
from datetime import datetime, timedelta
2015-02-28 00:05:33 +01:00
2015-03-01 23:07:23 +01:00
import sys
2015-03-01 21:52:24 +01:00
import asyncserver
2015-02-28 00:05:33 +01:00
2015-02-28 00:45:59 +01:00
codes = {
200: "OK",
2015-04-03 00:52:40 +02:00
301: "Moved Permanently",
2015-03-01 21:52:24 +01:00
304: "Not Modified",
2015-02-28 00:45:59 +01:00
404: "Not Found"
}
2015-03-01 21:52:24 +01:00
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):
2015-04-03 00:52:40 +02:00
"""HTTPClient implements a HTTP handler class for asyncserver.Server"""
2015-02-28 00:45:59 +01:00
def __init__(self, parent, sock, addrinfo):
super(HTTPClient, self).__init__(parent, sock, addrinfo)
2015-03-01 22:14:10 +01:00
self.set_timeout()
2015-02-28 00:45:59 +01:00
def on_data(self):
2015-04-03 00:52:40 +02:00
"""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.
2015-03-01 22:14:10 +01:00
self.set_timeout()
2015-02-28 00:45:59 +01:00
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)
2015-04-03 00:27:45 +02:00
self.on_data() # there might be more in the read_buf.
2015-03-01 22:14:10 +01:00
def set_timeout(self):
2015-04-03 00:52:40 +02:00
"""reset the timeout to 10 seconds"""
2015-03-01 22:14:10 +01:00
self.timeout = datetime.today() + timedelta(seconds=10)
2015-02-28 00:45:59 +01:00
def on_request(self, req):
2015-04-03 00:52:40 +02:00
"""method called when there is a http request"""
2015-02-28 00:45:59 +01:00
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)
2015-03-01 21:52:24 +01:00
try:
if hdr['Connection'] == 'close':
self.close()
except KeyError:
pass
2015-02-28 00:45:59 +01:00
def on_GET(self, url, headers):
2015-04-03 00:52:40 +02:00
"""GET doesn't do anything in this class"""
2015-02-28 00:45:59 +01:00
print("got GET", url, headers)
2015-03-01 22:14:10 +01:00
def next_timeout(self):
2015-04-03 00:52:40 +02:00
"""called from the event loop to figure out when the next timeout should be"""
2015-03-01 22:14:10 +01:00
if self.timeout:
return (self.timeout - datetime.today()).total_seconds()
else:
return None
def try_timeout(self):
2015-04-03 00:52:40 +02:00
"""called by the event loop on every run, it doesn't keep track of timeouts."""
2015-03-01 22:14:10 +01:00
if self.timeout < datetime.today():
print("connection timed out")
self.timeout = None
self.close()
2015-03-01 21:52:24 +01:00
def send_file(self, code, headers, fname, orig_etag):
2015-04-03 00:52:40 +02:00
"""send a file or 304 response if the etag matches"""
2015-03-01 21:52:24 +01:00
(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)
2015-02-28 00:45:59 +01:00
def send_response(self, code, headers, data):
2015-04-03 00:52:40 +02:00
"""send a http response. generate content-length and date headers here."""
2015-02-28 00:45:59 +01:00
headers["Content-Length"] = len(data)
2015-03-01 21:52:24 +01:00
headers["Date"] = httpdate()
2015-02-28 00:45:59 +01:00
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)
2015-03-01 21:52:24 +01:00
print "%s %s" % (code, codes[code])
2015-02-28 00:45:59 +01:00
class HTTPDirClient(HTTPClient):
2015-04-03 00:52:40 +02:00
"""HTTPDirClient implements a static file server from a directory on top of HTTPClient"""
2015-02-28 00:45:59 +01:00
def __init__(self, parent, sock, addrinfo):
super(HTTPDirClient, self).__init__(parent, sock, addrinfo)
self.directory = "content"
2015-03-01 21:52:24 +01:00
print "Opened connection"
2015-02-28 00:45:59 +01:00
def on_GET(self, url, headers):
2015-03-01 21:52:24 +01:00
if not url: url = "/"
else: url = unquote(url)
2015-02-28 00:45:59 +01:00
reqpath = os.path.abspath("./content" + url)
2015-03-01 21:52:24 +01:00
# make sure that we can't go outside the content dir
2015-02-28 00:45:59 +01:00
if not reqpath.startswith(os.path.abspath("./content/")):
self.send_response(404, {}, "Not Found, sorry\n")
return
2015-03-01 22:14:10 +01:00
print "GET", url
2015-03-01 21:52:24 +01:00
if os.path.isdir(reqpath) and not url.endswith('/'):
2015-04-03 00:52:40 +02:00
# http redirect to add a /
# should this be a 301?
2015-03-01 21:52:24 +01:00
self.send_response(301, {"Location": url + '/'}, "")
2015-04-03 00:52:40 +02:00
return
2015-03-01 21:52:24 +01:00
if os.path.isdir(reqpath) and url.endswith('/'):
2015-04-03 00:52:40 +02:00
# try and serve index.html
2015-02-28 00:45:59 +01:00
reqpath += "/index.html"
if not os.path.isfile(reqpath):
2015-04-03 00:52:40 +02:00
# 404 response if not found
2015-02-28 00:45:59 +01:00
self.send_response(404, {}, "Not Found, sorry\n")
else:
2015-03-01 21:52:24 +01:00
etag = None
if "If-None-Match" in headers:
etag = headers['If-None-Match']
self.send_file(200, {}, reqpath, etag)
2015-02-28 00:45:59 +01:00
2015-02-28 00:05:33 +01:00
if __name__ == '__main__':
try:
2015-03-01 23:07:23 +01:00
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