summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--project1/proj1_s4498062/content/error/403.html3
-rw-r--r--project1/proj1_s4498062/content/error/404.html5
-rw-r--r--project1/proj1_s4498062/webhttp/composer.py54
-rw-r--r--project1/proj1_s4498062/webhttp/config.py12
-rw-r--r--project1/proj1_s4498062/webhttp/message.py45
-rw-r--r--project1/proj1_s4498062/webhttp/regexes.py2
-rw-r--r--project1/proj1_s4498062/webhttp/resource.py20
-rw-r--r--project1/proj1_s4498062/webhttp/server.py63
-rw-r--r--project1/proj1_s4498062/webserver.py19
-rw-r--r--project1/proj1_s4498062/webtests.py31
10 files changed, 195 insertions, 59 deletions
diff --git a/project1/proj1_s4498062/content/error/403.html b/project1/proj1_s4498062/content/error/403.html
new file mode 100644
index 0000000..0bd6083
--- /dev/null
+++ b/project1/proj1_s4498062/content/error/403.html
@@ -0,0 +1,3 @@
+<h1>403</h1>
+
+<div style='position:relative;margin:0 auto;text-align:center;width:15px;'><i>x</i><span style='position:absolute;border-top:1px solid black;top:20px;left:0;width:15px;padding-top:2px;'>0</span></div>
diff --git a/project1/proj1_s4498062/content/error/404.html b/project1/proj1_s4498062/content/error/404.html
new file mode 100644
index 0000000..c07ba10
--- /dev/null
+++ b/project1/proj1_s4498062/content/error/404.html
@@ -0,0 +1,5 @@
+<h1>404</h1>
+
+<div style='text-align:center;'>
+ <span style='position:relative;width:38px;display:inline-block;'>lim<sub style='position:absolute;left:0;top:14px;'><i>x</i> &rarr; &infin;</sub></span> <b>cos</b>(<i>x</i>)
+</div>
diff --git a/project1/proj1_s4498062/webhttp/composer.py b/project1/proj1_s4498062/webhttp/composer.py
index 5123d31..f9bc0e0 100644
--- a/project1/proj1_s4498062/webhttp/composer.py
+++ b/project1/proj1_s4498062/webhttp/composer.py
@@ -4,11 +4,13 @@ This module contains a composer, which can compose responses to
HTTP requests from a client.
"""
+import re
import time
-import webhttp.message
-from webhttp.resource import Resource, FileExistError, FileAccessError
-import webhttp.weblogging as logging
+from config import config
+from message import Response
+from resource import Resource, FileExistError, FileAccessError
+import weblogging as logging
class ResponseComposer:
"""Class that composes a HTTP response to a HTTP request"""
@@ -31,25 +33,43 @@ class ResponseComposer:
webhttp.Response: response to request
"""
- response = webhttp.message.Response()
+ response = Response()
- # Stub code
+ if re.search(r'\/\.\.?(\/|$)', request.uri):
+ response.code = 403
+ response.set_header('Connection', 'close')
+ response.body = 'Don\'t even think about it.'
+ response.set_content_length()
+ return response
+
+ return self.serve(request.uri, request=request)
+
+ def serve(self, uri, code=200, etag=None, request=None, error_page=False):
+ response = Response()
try:
- resource = Resource(request.uri)
- response.code = 200
- response.set_header('Content-Length', resource.get_content_length())
+ resource = Resource(uri)
+ req = request
+ if req != None and (
+ resource.etag_match(req.get_header('If-None-Match')) or \
+ not resource.etag_match(req.get_header('If-Match') or '*')):
+ response.code = 304
+ else:
+ response.code = code
+ response.body = resource.get_content()
+ response.set_header('ETag', resource.generate_etag())
response.set_header('Connection', 'close')
- response.body = resource.get_content()
+ response.set_header('Content-Type', resource.get_content_type())
+ response.set_content_length()
except FileExistError:
- response.code = 404
- response.set_header("Content-Length", 9)
- response.set_header("Connection", "close")
- response.body = "Not found"
+ if not error_page:
+ return self.serve(config('error404'), code=404, error_page=True)
+ else:
+ response.code = code
+ response.set_header('Connection', 'close')
+ response.body = 'Error %d' % code
+ response.set_content_length()
except FileAccessError:
- response.code = 403
- response.set_header("Content-Length", 13)
- response.set_header("Connection", "close")
- response.body = "Access denied"
+ return self.serve(config('error403'), code=403, error_page=True)
return response
diff --git a/project1/proj1_s4498062/webhttp/config.py b/project1/proj1_s4498062/webhttp/config.py
new file mode 100644
index 0000000..35d73cb
--- /dev/null
+++ b/project1/proj1_s4498062/webhttp/config.py
@@ -0,0 +1,12 @@
+from configparser import SafeConfigParser
+
+__all__ = ['config']
+
+scp = SafeConfigParser()
+
+def config(option=None, section='webhttp', type=lambda x:x):
+ if option == None:
+ return scp
+ else:
+ return type(scp.get(section, option))
+
diff --git a/project1/proj1_s4498062/webhttp/message.py b/project1/proj1_s4498062/webhttp/message.py
index 2dc4240..059c765 100644
--- a/project1/proj1_s4498062/webhttp/message.py
+++ b/project1/proj1_s4498062/webhttp/message.py
@@ -9,12 +9,46 @@ import regexes as r
import weblogging as logging
reasondict = {
- # Dictionary for code reasons
- # Format: code : "Reason"
+ 100 : 'Continue',
+ 101 : 'Switching Protocols',
200 : 'OK',
+ 201 : 'Created',
+ 202 : 'Accepted',
+ 203 : 'Non-Authoritative Information',
+ 204 : 'No Content',
+ 205 : 'Reset Content',
+ 206 : 'Partial Content',
+ 300 : 'Multiple Choices',
+ 301 : 'Moved Permanently',
+ 302 : 'Found',
+ 303 : 'See Other',
+ 304 : 'Not Modified',
+ 305 : 'Use Proxy',
+ 307 : 'Temporary Redirect',
+ 400 : 'Bad Request',
+ 401 : 'Unauthorized',
+ 402 : 'Payment Required',
403 : 'Forbidden',
404 : 'Not Found',
- 500 : 'Internal Server Error'
+ 405 : 'Method Not Allowed',
+ 406 : 'Not Acceptable',
+ 407 : 'Proxy Authentication Required',
+ 408 : 'Request Time-out',
+ 409 : 'Conflict',
+ 410 : 'Gone',
+ 411 : 'Length Required',
+ 412 : 'Precondition Failed',
+ 413 : 'Request Entity Too Large',
+ 414 : 'Request-URI Too Large',
+ 415 : 'Unsupported Media Type',
+ 416 : 'Requested range not satisfiable',
+ 417 : 'Expectation Failed',
+ 500 : 'Internal Server Error',
+ 501 : 'Not Implemented',
+ 502 : 'Bad Gateway',
+ 503 : 'Service Unavailable',
+ 504 : 'Gateway Time-out',
+ 505 : 'HTTP Version not supported',
}
@@ -48,7 +82,7 @@ class Message(object):
if name in self.headers:
return self.headers[name]
else:
- return ""
+ return None
def parse_headers(self, msg):
for name, value in re.findall(r.MessageHeader, msg):
@@ -124,6 +158,9 @@ class Response(Message):
def startline(self):
return "%s %d %s" % (self.version, self.code, reasondict[self.code])
+
+ def set_content_length(self):
+ self.set_header('Content-Length', len(self.body))
def __str__(self):
"""Convert the Response to a string
diff --git a/project1/proj1_s4498062/webhttp/regexes.py b/project1/proj1_s4498062/webhttp/regexes.py
index 755bb9f..b1cacc9 100644
--- a/project1/proj1_s4498062/webhttp/regexes.py
+++ b/project1/proj1_s4498062/webhttp/regexes.py
@@ -149,3 +149,5 @@ FieldContent = regex_opt_r([TEXT_NO_LWS + TEXT + r'*(?!' + LWS + r')'])
FieldValue = grp(regex_opt_r([grp(FieldContent), LWS]) + r'*')
MessageHeader = grp(grpm(FieldName) + r':' + grpm(FieldValue))
+ETagSplit = grp(r',' + LWS + r'*')
+
diff --git a/project1/proj1_s4498062/webhttp/resource.py b/project1/proj1_s4498062/webhttp/resource.py
index cf2b725..8fefc9c 100644
--- a/project1/proj1_s4498062/webhttp/resource.py
+++ b/project1/proj1_s4498062/webhttp/resource.py
@@ -3,10 +3,15 @@
This module contains a handler class for resources.
"""
-import os
+import binascii
+import hashlib
import mimetypes
+import os
+import re
import urlparse
+from config import config
+import regexes as r
class FileExistError(Exception):
"""Exception which is raised when file does not exist"""
@@ -37,7 +42,7 @@ class Resource:
if not os.path.exists(self.path):
raise FileExistError
if os.path.isdir(self.path):
- self.path = os.path.join(self.path, "index.html")
+ self.path = os.path.join(self.path, config('index'))
if not os.path.exists(self.path):
raise FileAccessError
if not os.path.isfile(self.path):
@@ -52,9 +57,18 @@ class Resource:
str: ETag for the resource
"""
stat = os.stat(self.path)
- etag = ""
+ m = hashlib.md5()
+ m.update(str(stat))
+ etag = binascii.hexlify(m.digest())
return etag
+ def etag_match(self, etag):
+ if etag == None:
+ return False
+ my_etag = self.generate_etag()
+ return etag == '*' or \
+ any([tag == my_etag for tag in re.split(r.ETagSplit, etag)])
+
def get_content(self):
"""Get the contents of the resource
diff --git a/project1/proj1_s4498062/webhttp/server.py b/project1/proj1_s4498062/webhttp/server.py
index b385636..7eeec27 100644
--- a/project1/proj1_s4498062/webhttp/server.py
+++ b/project1/proj1_s4498062/webhttp/server.py
@@ -3,13 +3,13 @@
This module contains a HTTP server
"""
-from configparser import SafeConfigParser
import threading
import socket
from composer import ResponseComposer
from parser import RequestParser
import weblogging as logging
+from config import config
class ConnectionHandler(threading.Thread):
"""Connection Handler for HTTP Server"""
@@ -30,22 +30,28 @@ class ConnectionHandler(threading.Thread):
def handle_connection(self):
"""Handle a new connection"""
- rp = RequestParser()
- rc = ResponseComposer(timeout=self.timeout)
+ self.rp = RequestParser()
+ self.rc = ResponseComposer(timeout=self.timeout)
while True:
data = self.conn_socket.recv(4096)
if len(data) == 0:
break
- for req in rp.parse_requests(data):
- logging.info("<-- %s" % req.startline())
- resp = rc.compose_response(req)
- logging.info("--> %s" % resp.startline())
- sent = self.conn_socket.send(str(resp))
- if sent == 0:
- raise RuntimeError('Socket broken')
+ self.handle_data(data)
self.conn_socket.close()
+
+ def handle_data(self, data):
+ for req in self.rp.parse_requests(data):
+ logging.info("<-- (%s) %s" % (self.addr[0], req.startline()))
+ resp = self.rc.compose_response(req)
+ logging.info("--> (%s) %s" % (self.addr[0], resp.startline()))
+ self.send(resp)
+
+ def send(self, data):
+ sent = self.conn_socket.send(str(data))
+ if sent == 0:
+ raise RuntimeError('Socket broken')
def run(self):
"""Run the thread of the connection handler"""
@@ -53,12 +59,12 @@ class ConnectionHandler(threading.Thread):
self.handle_connection()
except socket.error, e:
print('ERR ' + str(e))
-
+
class Server:
"""HTTP Server"""
- def __init__(self, config, **kwargs):
+ def __init__(self, configfile, **kwargs):
"""Initialize the HTTP server
Args:
@@ -66,27 +72,32 @@ class Server:
server_port (int): port that the server is listening on
timeout (int): seconds until timeout
"""
- self.read_config(config, **kwargs)
+ self.read_config(configfile, **kwargs)
self.done = False
- def read_config(self, config, **kwargs):
- self.cp = SafeConfigParser()
- self.cp.read(config)
- if not self.cp.has_section('webhttp'):
- self.cp.add_section('webhttp')
- for (name, val) in self.cp.items('webhttp') + kwargs.items():
- setattr(self, name, val)
+ def read_config(self, configfile, **kwargs):
+ config().read(configfile)
+
+ if not config().has_section('webhttp'):
+ config().add_section('webhttp')
+
+ for name, val in kwargs.items():
+ if val != None:
+ config().set('webhttp', name, str(val))
def run(self):
"""Run the HTTP Server and start listening"""
- serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- serversocket.bind((self.hostname, self.port))
- serversocket.listen(5)
+ self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.socket.bind((config('hostname'), config('port', type=int)))
+ self.socket.listen(config('max_connections', type=int))
while not self.done:
- (clientsocket, addr) = serversocket.accept()
- ch = ConnectionHandler(clientsocket, addr, self.timeout)
- ch.run()
+ (csocket, addr) = self.socket.accept()
+ ch = ConnectionHandler(csocket, addr, config('timeout', type=int))
+ ch.start()
def shutdown(self):
"""Safely shut down the HTTP server"""
+ self.socket.shutdown(socket.SHUT_RDWR)
+ self.socket.close()
self.done = True
+
diff --git a/project1/proj1_s4498062/webserver.py b/project1/proj1_s4498062/webserver.py
index a361d5b..c01df4e 100644
--- a/project1/proj1_s4498062/webserver.py
+++ b/project1/proj1_s4498062/webserver.py
@@ -13,20 +13,23 @@ if __name__ == '__main__':
parser = argparse.ArgumentParser(description="HTTP Server")
parser.add_argument('-c', '--config', type=str, default='~/.webpy.ini',
help='configuration file')
- parser.add_argument('-a', '--address', type=str, default='localhost',
+
+ parser.add_argument('-a', '--address', type=str, default=None,
help='address to listen on (default localhost)')
- parser.add_argument('-p', '--port', type=int, default=80,
- help='port to listen on (default 80)')
- parser.add_argument('-t', '--timeout', type=int, default=15,
- help='timeout for incoming connections (default 15)')
+ parser.add_argument('-p', '--port', type=int, default=None,
+ help='port to listen on (no default)')
+ parser.add_argument('-t', '--timeout', type=int, default=None,
+ help='timeout for incoming connections (no default)')
+
parser.add_argument('-l', '--log', type=str, default='info',
help='log level (debug, info, warning, error, critical)')
parser.add_argument('-lf', '--log-file', type=str, default=None,
help='log file (default stdout)')
+
args = parser.parse_args()
# Logging
- fmt = '[%(asctime)s] %(process)d %(levelname)s %(message)s'
+ fmt = '[%(asctime)s] %(levelname)s %(message)s'
logging.basicConfig(format=fmt, level=getattr(logging, args.log.upper()))
if args.log_file != None:
logger = logging.getLogger(webhttp.weblogging.name)
@@ -36,8 +39,8 @@ if __name__ == '__main__':
# Start server
config = os.path.expanduser(os.path.expandvars(args.config))
- server = webhttp.server.Server(config=config,
- address=args.address, port=args.port, timeout=args.timeout)
+ server = webhttp.server.Server(configfile=config,
+ hostname=args.address, port=args.port, timeout=args.timeout)
try:
server.run()
except KeyboardInterrupt:
diff --git a/project1/proj1_s4498062/webtests.py b/project1/proj1_s4498062/webtests.py
index a869de1..5e45acf 100644
--- a/project1/proj1_s4498062/webtests.py
+++ b/project1/proj1_s4498062/webtests.py
@@ -1,7 +1,9 @@
+import os.path
import unittest
import socket
import sys
+from webhttp.config import config
import webhttp.message
import webhttp.parser
@@ -22,6 +24,8 @@ class TestGetRequests(unittest.TestCase):
('Connection', 'close')
]
+ config().read(os.path.expanduser('~/.webpy.ini'))
+
def tearDown(self):
"""Clean up after testing"""
self.client_socket.shutdown(socket.SHUT_RDWR)
@@ -59,7 +63,23 @@ class TestGetRequests(unittest.TestCase):
"""GET for an existing single resource followed by a GET for that same
resource with caching utilized on the client/tester side
"""
- pass
+ response = self.request('GET', '/test', self.default_headers)
+
+ response = self.request('GET', '/test', self.default_headers + \
+ [('If-None-Match', 'invalid-etag')])
+ self.assertEqual(response.code, 200, 'If-None-Match returns 200')
+
+ response = self.request('GET', '/test', self.default_headers + \
+ [('If-None-Match', response.get_header('ETag'))])
+ self.assertEqual(response.code, 304, 'If-None-Match returns 304')
+
+ response = self.request('GET', '/test', self.default_headers + \
+ [('If-Match', response.get_header('ETag'))])
+ self.assertEqual(response.code, 200, 'If-Match returns 200')
+
+ response = self.request('GET', '/test', self.default_headers + \
+ [('If-Match', 'invalid-etag')])
+ self.assertEqual(response.code, 304, 'If-Match returns 304')
def test_extisting_index_file(self):
"""GET for a directory with an existing index.html file"""
@@ -72,6 +92,11 @@ class TestGetRequests(unittest.TestCase):
self.assertEqual(response.code, 403)
self.assertTrue(response.body)
+ def test_error_page(self):
+ r1 = self.request('GET', '/test/nonexistant', self.default_headers)
+ r2 = self.request('GET', config('error404'), self.default_headers)
+ self.assertEqual(r1.body, r2.body)
+
def test_persistent_close(self):
"""Multiple GETs over the same (persistent) connection with the last
GET prompting closing the connection, the connection should be closed.
@@ -91,6 +116,10 @@ class TestGetRequests(unittest.TestCase):
"""
pass
+ def test_doubledot(self):
+ response = self.request('GET', '/../test', self.default_headers)
+ self.assertEquals(response.code, 403)
+
if __name__ == "__main__":
# Parse command line arguments