From 6fe6c6c7a972753d894d3107cdeb4cdd28b22125 Mon Sep 17 00:00:00 2001 From: Camil Staps Date: Thu, 7 Apr 2016 17:12:57 +0200 Subject: Project 1: fix line endings --- project1/proj1_s4498062/README.md | 126 +++---- project1/proj1_s4498062/content/test/index.html | 18 +- project1/proj1_s4498062/webhttp/__init__.py | 18 +- project1/proj1_s4498062/webhttp/message.py | 418 ++++++++++++------------ project1/proj1_s4498062/webhttp/parser.py | 124 +++---- project1/proj1_s4498062/webhttp/resource.py | 226 ++++++------- project1/proj1_s4498062/webhttp/server.py | 236 ++++++------- project1/proj1_s4498062/webserver.py | 102 +++--- project1/proj1_s4498062/webtests.py | 374 ++++++++++----------- 9 files changed, 821 insertions(+), 821 deletions(-) (limited to 'project1') diff --git a/project1/proj1_s4498062/README.md b/project1/proj1_s4498062/README.md index adc2a2d..13b38e0 100644 --- a/project1/proj1_s4498062/README.md +++ b/project1/proj1_s4498062/README.md @@ -1,63 +1,63 @@ -# Project 1 HTTP Server Framework - -## Description - -This repo contains a framework for the first project. -It provides a class structure and several utility wrappers for the Python Standard Library (so that you don't have to dig in the Python Docs). -The project can be completed by filling in the empty methods - -## File Structure - -* proj_sn1_sn2 - * content - * test - * index.html - * webhttp - * composer.py - * \_\_init\_\_.py - * message.py - * resource.py - * parser.py - * server.py - * webserver.py - * webtests.py - -The content directory contains the content for the website. -The test subfolder is meant for resources that are used by the tests which are defined in webtests.py. - -The webhttp directory contains a package for HTTP. -Most methods are unimplemented. -The file message.py contains classes for HTTP messages. -The base class Message implements a basic HTTP message, which is specialized by Request and Response, which are for HTTP requests and HTTP responses respectively. -The file resource.py contains a handler class for resources. -The file parser.py contains classes for parsing HTTP requests and responses. -The file composer.py contains a class for composing responses to request. -The file server.py contains the main HTTP server. - -The webserver.py file is Python file for starting the HTTP server, this file is fully implemented. - -Finally webtests.py contains the tests for your HTTP server. -You manually have to start the HTTP server before running the tests. -Currently only one of the tests is implemented, you will have to implement the other tests. - -## Suggested Implementation Order - -1. Implement "run" in the class "Server" in "webhttp/server.py". This method should listen for incoming connections and create to a "ConnectionHandler" to handle the connection. -2. Implement "handle_connection" in the class "ConnectionHandler" in "webhttp/server.py". For now this method should receive a response from a client, send "Hello World" back and close the connection. Test that this works using the Python shell. -3. Implement "parse_requests" in the class "RequestParser" in "webhttp/parser.py". You can test your implementation by using to parse a few Requests in the Python shell. -4. Implement "\_\_str\_\_" for "Message", "Response" and "Request" in "webhttp/message.py". This function should return a string representation of the message according to the RFC. In order to test this you should create a few Responses and Requests in the Python shell and test if they comply with the RFC. -5. Reimplement "handle_connection" using "Response", "Request", "RequestParser" and "ResponseComposer". -6. Implement "parse_response" in "ResponseParser" in "webhttp/parser.py". At this point you should be able to pass the test in "webtests.py". -7. Replace the stub code in "compose_response" in "ResponseComposer" in "webhttp/composer.py". The composer should now be able to create the correct response to a request. You can ignore persistent connections and client side caching for now, but the response should have the right code and body. -8. Write additional tests in "webtests.py" for the following scenarios: - * GET for a single resource that does not exist - * GET for a directory with an existing index.html file - * GET for a directory with non-existing index.html file -Your code should be able to pass these tests at this point. -9. Implement client side caching using ETags in "compose_response" and "generate_etag" in the class "Resource" in "resource.py". The "os.stat" module might be useful for generating an ETag. You should also implement the following test (which your server should pass at this point): - * GET for an existing resource followed by a GET for that same resource, with caching utilized on the client/tester side. -10. Implement persistent connections in "compose_response" and "handle_connection", and implement the following tests: - * multiple GETs over the same (persistent) connection with the last GET prompting closing the connection, the connection should be closed. - * multiple GETs over the same (persistent) connection, followed by a wait during which the connection times out, the connection should be closed. -11. Implement content encoding. You may need to add extra methods to class "Resource" for this. You should also implement the following test: - * GET which requests an existing resource gzip encoding, which is accepted by the server. +# Project 1 HTTP Server Framework + +## Description + +This repo contains a framework for the first project. +It provides a class structure and several utility wrappers for the Python Standard Library (so that you don't have to dig in the Python Docs). +The project can be completed by filling in the empty methods + +## File Structure + +* proj_sn1_sn2 + * content + * test + * index.html + * webhttp + * composer.py + * \_\_init\_\_.py + * message.py + * resource.py + * parser.py + * server.py + * webserver.py + * webtests.py + +The content directory contains the content for the website. +The test subfolder is meant for resources that are used by the tests which are defined in webtests.py. + +The webhttp directory contains a package for HTTP. +Most methods are unimplemented. +The file message.py contains classes for HTTP messages. +The base class Message implements a basic HTTP message, which is specialized by Request and Response, which are for HTTP requests and HTTP responses respectively. +The file resource.py contains a handler class for resources. +The file parser.py contains classes for parsing HTTP requests and responses. +The file composer.py contains a class for composing responses to request. +The file server.py contains the main HTTP server. + +The webserver.py file is Python file for starting the HTTP server, this file is fully implemented. + +Finally webtests.py contains the tests for your HTTP server. +You manually have to start the HTTP server before running the tests. +Currently only one of the tests is implemented, you will have to implement the other tests. + +## Suggested Implementation Order + +1. Implement "run" in the class "Server" in "webhttp/server.py". This method should listen for incoming connections and create to a "ConnectionHandler" to handle the connection. +2. Implement "handle_connection" in the class "ConnectionHandler" in "webhttp/server.py". For now this method should receive a response from a client, send "Hello World" back and close the connection. Test that this works using the Python shell. +3. Implement "parse_requests" in the class "RequestParser" in "webhttp/parser.py". You can test your implementation by using to parse a few Requests in the Python shell. +4. Implement "\_\_str\_\_" for "Message", "Response" and "Request" in "webhttp/message.py". This function should return a string representation of the message according to the RFC. In order to test this you should create a few Responses and Requests in the Python shell and test if they comply with the RFC. +5. Reimplement "handle_connection" using "Response", "Request", "RequestParser" and "ResponseComposer". +6. Implement "parse_response" in "ResponseParser" in "webhttp/parser.py". At this point you should be able to pass the test in "webtests.py". +7. Replace the stub code in "compose_response" in "ResponseComposer" in "webhttp/composer.py". The composer should now be able to create the correct response to a request. You can ignore persistent connections and client side caching for now, but the response should have the right code and body. +8. Write additional tests in "webtests.py" for the following scenarios: + * GET for a single resource that does not exist + * GET for a directory with an existing index.html file + * GET for a directory with non-existing index.html file +Your code should be able to pass these tests at this point. +9. Implement client side caching using ETags in "compose_response" and "generate_etag" in the class "Resource" in "resource.py". The "os.stat" module might be useful for generating an ETag. You should also implement the following test (which your server should pass at this point): + * GET for an existing resource followed by a GET for that same resource, with caching utilized on the client/tester side. +10. Implement persistent connections in "compose_response" and "handle_connection", and implement the following tests: + * multiple GETs over the same (persistent) connection with the last GET prompting closing the connection, the connection should be closed. + * multiple GETs over the same (persistent) connection, followed by a wait during which the connection times out, the connection should be closed. +11. Implement content encoding. You may need to add extra methods to class "Resource" for this. You should also implement the following test: + * GET which requests an existing resource gzip encoding, which is accepted by the server. diff --git a/project1/proj1_s4498062/content/test/index.html b/project1/proj1_s4498062/content/test/index.html index a5d8c3d..8f5923f 100644 --- a/project1/proj1_s4498062/content/test/index.html +++ b/project1/proj1_s4498062/content/test/index.html @@ -1,9 +1,9 @@ - -
-Test Text
- - + + +Test Text
+ + diff --git a/project1/proj1_s4498062/webhttp/__init__.py b/project1/proj1_s4498062/webhttp/__init__.py index d501a0e..0d5a9ab 100644 --- a/project1/proj1_s4498062/webhttp/__init__.py +++ b/project1/proj1_s4498062/webhttp/__init__.py @@ -1,9 +1,9 @@ -"""HTTP Package - -This package contains the following modules: - * message: Module for HTTP responses/requests - * composer: Module for composing responses to requests - * parser: Module for parsing HTTP responses/requests - * util: Module with utility functions - * server: Module which contains a HTTP server -""" +"""HTTP Package + +This package contains the following modules: + * message: Module for HTTP responses/requests + * composer: Module for composing responses to requests + * parser: Module for parsing HTTP responses/requests + * util: Module with utility functions + * server: Module which contains a HTTP server +""" diff --git a/project1/proj1_s4498062/webhttp/message.py b/project1/proj1_s4498062/webhttp/message.py index 2e1a962..393236e 100644 --- a/project1/proj1_s4498062/webhttp/message.py +++ b/project1/proj1_s4498062/webhttp/message.py @@ -1,209 +1,209 @@ -"""HTTP Messages - -This modules contains classes for representing HTTP responses and requests. -""" - -import re - -import encodings -import regexes as r -import weblogging as logging - -reasondict = { - 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', - 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', -} - - -class Message(object): - """Class that stores an HTTP Message""" - - def __init__(self): - """Initialize the Message""" - self.version = "HTTP/1.1" - self.body = "" - self.headers = dict() - - def set_header(self, name, value): - """Add a header and its value - - Args: - name (str): name of header - value (str): value of header - """ - self.headers[name] = str(value) - - def get_header(self, name): - """Get the value of a header - - Args: - name (str): name of header - - Returns: - str: value of header, empty if header does not exist - """ - if name in self.headers: - return self.headers[name] - else: - return None - - def parse_headers(self, msg): - for name, value in re.findall(r.MessageHeader, msg): - self.set_header(name, value.strip()) - logging.debug("%s: %s" % (name, value.strip())) - - def startline(self): - return '' - - def __str__(self): - """Convert the Message to a string - - Returns: - str: representation the can be sent over socket - """ - msg = '' - msg += '%s\r\n' % self.startline() - msg += '\r\n'.join([k + ": " + v for k, v in self.headers.iteritems()]) - msg += '\r\n\r\n' + self.body - return msg - - def __eq__(self, other): - return self.__dict__ == other.__dict__ - - -class Request(Message): - """Class that stores an HTTP request""" - - def __init__(self): - """Initialize the Request""" - super(Request, self).__init__() - self.method = "" - self.uri = "" - - def parse(self, msg): - [reqline, rest] = msg.split('\r\n', 1) - reqline = re.match(r.RequestLine, reqline + '\r\n') - self.method, self.uri, self.version = reqline.groups() - - [headers, body] = rest.split('\r\n\r\n', 1) - self.parse_headers(headers) - self.body = body - - def encodings(self): - requested = self.get_header('Accept-Encoding') - if requested == None: - return encodings.all - - encs = [] - requested = re.split(r.EncodingSplit, requested) - for value in requested: - try: - match = re.match(r.AcceptEncodingValue, value) - if match == None: - return [encodings.IDENTITY] - enc, q = match.groups() - # Unclear what should happen when some qvalues are omitted - q = q or '0.001' - if enc == '*': - for enc in encodings.all: - encs.append((encodings.get(enc), float(q))) - else: - encs.append((encodings.get(enc), float(q))) - except encodings.UnknownEncodingError: - pass - - rejected = [e[0] for e in encs if e[1] == 0] - accepted = [e for e in encs if e[1] != 0] - - if not encodings.IDENTITY in rejected + [a[0] for a in accepted]: - accepted.append((encodings.IDENTITY, 0.001)) - - accepted.sort(key=lambda x:x[1], reverse=True) - return [a[0] for a in accepted] - - def startline(self): - return "%s %s %s" % (self.method, self.uri, self.version) - - def __str__(self): - """Convert the Request to a string - - Returns: - str: representation the can be sent over socket - """ - return super(Request, self).__str__() - - -class Response(Message): - """Class that stores an HTTP Response""" - - def __init__(self): - """Initialize the Response""" - super(Response, self).__init__() - self.set_header('Server', 'WebPy') - - def parse(self, msg): - [respline, rest] = msg.split('\r\n', 1) - respline = re.match(r.StatusLine, respline + '\r\n') - self.version = respline.group(1) - self.code = int(respline.group(2)) - - [headers, body] = rest.split('\r\n\r\n', 1) - self.parse_headers(headers) - self.body = body - - 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 decompress(self): - self.body = encodings.decode( - self.get_header('Content-Encoding'), self.body) - - def __str__(self): - """Convert the Response to a string - - Returns: - str: representation the can be sent over socket - """ - return super(Response, self).__str__() - +"""HTTP Messages + +This modules contains classes for representing HTTP responses and requests. +""" + +import re + +import encodings +import regexes as r +import weblogging as logging + +reasondict = { + 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', + 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', +} + + +class Message(object): + """Class that stores an HTTP Message""" + + def __init__(self): + """Initialize the Message""" + self.version = "HTTP/1.1" + self.body = "" + self.headers = dict() + + def set_header(self, name, value): + """Add a header and its value + + Args: + name (str): name of header + value (str): value of header + """ + self.headers[name] = str(value) + + def get_header(self, name): + """Get the value of a header + + Args: + name (str): name of header + + Returns: + str: value of header, empty if header does not exist + """ + if name in self.headers: + return self.headers[name] + else: + return None + + def parse_headers(self, msg): + for name, value in re.findall(r.MessageHeader, msg): + self.set_header(name, value.strip()) + logging.debug("%s: %s" % (name, value.strip())) + + def startline(self): + return '' + + def __str__(self): + """Convert the Message to a string + + Returns: + str: representation the can be sent over socket + """ + msg = '' + msg += '%s\r\n' % self.startline() + msg += '\r\n'.join([k + ": " + v for k, v in self.headers.iteritems()]) + msg += '\r\n\r\n' + self.body + return msg + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + +class Request(Message): + """Class that stores an HTTP request""" + + def __init__(self): + """Initialize the Request""" + super(Request, self).__init__() + self.method = "" + self.uri = "" + + def parse(self, msg): + [reqline, rest] = msg.split('\r\n', 1) + reqline = re.match(r.RequestLine, reqline + '\r\n') + self.method, self.uri, self.version = reqline.groups() + + [headers, body] = rest.split('\r\n\r\n', 1) + self.parse_headers(headers) + self.body = body + + def encodings(self): + requested = self.get_header('Accept-Encoding') + if requested == None: + return encodings.all + + encs = [] + requested = re.split(r.EncodingSplit, requested) + for value in requested: + try: + match = re.match(r.AcceptEncodingValue, value) + if match == None: + return [encodings.IDENTITY] + enc, q = match.groups() + # Unclear what should happen when some qvalues are omitted + q = q or '0.001' + if enc == '*': + for enc in encodings.all: + encs.append((encodings.get(enc), float(q))) + else: + encs.append((encodings.get(enc), float(q))) + except encodings.UnknownEncodingError: + pass + + rejected = [e[0] for e in encs if e[1] == 0] + accepted = [e for e in encs if e[1] != 0] + + if not encodings.IDENTITY in rejected + [a[0] for a in accepted]: + accepted.append((encodings.IDENTITY, 0.001)) + + accepted.sort(key=lambda x:x[1], reverse=True) + return [a[0] for a in accepted] + + def startline(self): + return "%s %s %s" % (self.method, self.uri, self.version) + + def __str__(self): + """Convert the Request to a string + + Returns: + str: representation the can be sent over socket + """ + return super(Request, self).__str__() + + +class Response(Message): + """Class that stores an HTTP Response""" + + def __init__(self): + """Initialize the Response""" + super(Response, self).__init__() + self.set_header('Server', 'WebPy') + + def parse(self, msg): + [respline, rest] = msg.split('\r\n', 1) + respline = re.match(r.StatusLine, respline + '\r\n') + self.version = respline.group(1) + self.code = int(respline.group(2)) + + [headers, body] = rest.split('\r\n\r\n', 1) + self.parse_headers(headers) + self.body = body + + 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 decompress(self): + self.body = encodings.decode( + self.get_header('Content-Encoding'), self.body) + + def __str__(self): + """Convert the Response to a string + + Returns: + str: representation the can be sent over socket + """ + return super(Response, self).__str__() + diff --git a/project1/proj1_s4498062/webhttp/parser.py b/project1/proj1_s4498062/webhttp/parser.py index 7dbb3b5..35f01e8 100644 --- a/project1/proj1_s4498062/webhttp/parser.py +++ b/project1/proj1_s4498062/webhttp/parser.py @@ -1,62 +1,62 @@ -"""HTTP response and request parsers - -This module contains parses for HTTP response and HTTP requests. -""" - -import webhttp.message - - -class RequestParser: - """Class that parses a HTTP request""" - - def __init__(self): - """Initialize the RequestParser""" - self.buff = '' - - def parse_requests(self, buff): - """Parse requests in a buffer - - Args: - buff (str): the buffer contents received from socket - - Returns: - list of webhttp.Request - """ - self.buff += buff - requests = self.get_requests() - - for request in requests: - http_request = webhttp.message.Request() - http_request.parse(request) - yield http_request - - def get_requests(self): - """Split multiple requests from buffer - - Returns: - list of str - """ - requests = self.buff.split('\r\n\r\n') - self.buff = requests[-1] - for req in requests[:-1]: - yield req.lstrip() + '\r\n\r\n' - - -class ResponseParser: - """Class that parses a HTTP response""" - def __init__(self): - """Initialize the ResponseParser""" - pass - - def parse_response(self, buff): - """Parse responses in buffer - - Args: - buff (str): the buffer contents received from socket - - Returns: - webhttp.Response - """ - response = webhttp.message.Response() - response.parse(buff) - return response +"""HTTP response and request parsers + +This module contains parses for HTTP response and HTTP requests. +""" + +import webhttp.message + + +class RequestParser: + """Class that parses a HTTP request""" + + def __init__(self): + """Initialize the RequestParser""" + self.buff = '' + + def parse_requests(self, buff): + """Parse requests in a buffer + + Args: + buff (str): the buffer contents received from socket + + Returns: + list of webhttp.Request + """ + self.buff += buff + requests = self.get_requests() + + for request in requests: + http_request = webhttp.message.Request() + http_request.parse(request) + yield http_request + + def get_requests(self): + """Split multiple requests from buffer + + Returns: + list of str + """ + requests = self.buff.split('\r\n\r\n') + self.buff = requests[-1] + for req in requests[:-1]: + yield req.lstrip() + '\r\n\r\n' + + +class ResponseParser: + """Class that parses a HTTP response""" + def __init__(self): + """Initialize the ResponseParser""" + pass + + def parse_response(self, buff): + """Parse responses in buffer + + Args: + buff (str): the buffer contents received from socket + + Returns: + webhttp.Response + """ + response = webhttp.message.Response() + response.parse(buff) + return response diff --git a/project1/proj1_s4498062/webhttp/resource.py b/project1/proj1_s4498062/webhttp/resource.py index 9509f82..ee05fa7 100644 --- a/project1/proj1_s4498062/webhttp/resource.py +++ b/project1/proj1_s4498062/webhttp/resource.py @@ -1,113 +1,113 @@ -"""Resources - -This module contains a handler class for resources. -""" - -import binascii -import gzip -import hashlib -import mimetypes -import os -import re -import StringIO -import urlparse - -from config import config -import encodings -import regexes as r - - -class FileExistError(Exception): - """Exception which is raised when file does not exist""" - pass - - -class FileAccessError(Exception): - """Exception which is raised when file exists, but cannot be accessed""" - pass - - -class Resource: - """Class for representing a Resource (file)""" - - def __init__(self, uri): - """Initialize the resource" - - Raises: - FileExistError: if resource does not exist - FileAccessError: if resource exists, but cannot be accessed - - Args: - uri (str): Uniform Resource Identifier - """ - if uri == None: - raise FileExistError - self.uri = uri - out = urlparse.urlparse(uri) - self.path = os.path.join(config('root', default='content'), - out.path.lstrip("/")) - if not os.path.exists(self.path): - raise FileExistError - if os.path.isdir(self.path): - self.path = os.path.join(self.path, - config('index', default='index.html')) - if not os.path.exists(self.path): - raise FileAccessError - if not os.path.isfile(self.path): - raise FileExistError - if not os.access(self.path, os.R_OK): - raise FileAccessError - - def generate_etag(self): - """Generate the ETag for the resource - - Returns: - str: ETag for the resource - """ - stat = os.stat(self.path) - 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, encoding=encodings.IDENTITY): - """Get the contents of the resource - - Returns: - str: Contents of the resource - """ - content = open(self.path).read() - return encodings.encode(encoding, content) - - def get_content_type(self): - """Get the content type, i.e "text/html" - - Returns: - str: type of content in the resource - """ - mimetype = mimetypes.guess_type(self.path) - return mimetype[0] - - def get_content_encoding(self): - """Get the content encoding, i.e "gzip" - - Returns: - str: encoding used for the resource - """ - mimetype = mimetypes.guess_type(self.path) - return mimetype[1] - - def get_content_length(self): - """Get the length of the resource - - Returns: - int: length of resource in bytes - """ - return os.path.getsize(self.path) +"""Resources + +This module contains a handler class for resources. +""" + +import binascii +import gzip +import hashlib +import mimetypes +import os +import re +import StringIO +import urlparse + +from config import config +import encodings +import regexes as r + + +class FileExistError(Exception): + """Exception which is raised when file does not exist""" + pass + + +class FileAccessError(Exception): + """Exception which is raised when file exists, but cannot be accessed""" + pass + + +class Resource: + """Class for representing a Resource (file)""" + + def __init__(self, uri): + """Initialize the resource" + + Raises: + FileExistError: if resource does not exist + FileAccessError: if resource exists, but cannot be accessed + + Args: + uri (str): Uniform Resource Identifier + """ + if uri == None: + raise FileExistError + self.uri = uri + out = urlparse.urlparse(uri) + self.path = os.path.join(config('root', default='content'), + out.path.lstrip("/")) + if not os.path.exists(self.path): + raise FileExistError + if os.path.isdir(self.path): + self.path = os.path.join(self.path, + config('index', default='index.html')) + if not os.path.exists(self.path): + raise FileAccessError + if not os.path.isfile(self.path): + raise FileExistError + if not os.access(self.path, os.R_OK): + raise FileAccessError + + def generate_etag(self): + """Generate the ETag for the resource + + Returns: + str: ETag for the resource + """ + stat = os.stat(self.path) + 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, encoding=encodings.IDENTITY): + """Get the contents of the resource + + Returns: + str: Contents of the resource + """ + content = open(self.path).read() + return encodings.encode(encoding, content) + + def get_content_type(self): + """Get the content type, i.e "text/html" + + Returns: + str: type of content in the resource + """ + mimetype = mimetypes.guess_type(self.path) + return mimetype[0] + + def get_content_encoding(self): + """Get the content encoding, i.e "gzip" + + Returns: + str: encoding used for the resource + """ + mimetype = mimetypes.guess_type(self.path) + return mimetype[1] + + def get_content_length(self): + """Get the length of the resource + + Returns: + int: length of resource in bytes + """ + return os.path.getsize(self.path) diff --git a/project1/proj1_s4498062/webhttp/server.py b/project1/proj1_s4498062/webhttp/server.py index 5db8620..70d1dd6 100644 --- a/project1/proj1_s4498062/webhttp/server.py +++ b/project1/proj1_s4498062/webhttp/server.py @@ -1,118 +1,118 @@ -"""HTTP Server - -This module contains a HTTP server -""" - -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""" - - def __init__(self, conn_socket, addr, timeout): - """Initialize the HTTP Connection Handler - - Args: - conn_socket (socket): socket used for connection with client - addr (str): ip address of client - timeout (int): seconds until timeout - """ - super(ConnectionHandler, self).__init__() - self.daemon = True - self.conn_socket = conn_socket - self.addr = addr - self.timeout = timeout - self.done = False - - def handle_connection(self): - """Handle a new connection""" - self.rp = RequestParser() - self.rc = ResponseComposer(timeout=self.timeout) - - self.conn_socket.settimeout(self.timeout) - - try: - while not self.done: - data = self.conn_socket.recv(32) - if len(data) == 0: - break - self.handle_data(data) - except socket.timeout: - logging.debug('%s connection timed out.' % self.addr[0]) - finally: - self.conn_socket.shutdown(socket.SHUT_RDWR) - self.conn_socket.close() - logging.debug('%s connection closed.' % self.addr[0]) - - 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) - - if resp.get_header('Connection') == 'close': - self.done = True - return - - 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""" - try: - self.handle_connection() - except socket.error, e: - logging.error('Error in handling connection with %s: %s' % - (self.addr[0], str(e))) - - -class Server: - """HTTP Server""" - - def __init__(self, configfile, **kwargs): - """Initialize the HTTP server - - Args: - hostname (str): hostname of the server - server_port (int): port that the server is listening on - timeout (int): seconds until timeout - """ - self.read_config(configfile, **kwargs) - self.done = False - - 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""" - self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.socket.bind((config('hostname', default='localhost'), - config('port', type=int, default=8001))) - self.socket.listen(config('max_connections', type=int, default=1000)) - while not self.done: - (csocket, addr) = self.socket.accept() - logging.debug('%s connection accepted.' % addr[0]) - 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 - +"""HTTP Server + +This module contains a HTTP server +""" + +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""" + + def __init__(self, conn_socket, addr, timeout): + """Initialize the HTTP Connection Handler + + Args: + conn_socket (socket): socket used for connection with client + addr (str): ip address of client + timeout (int): seconds until timeout + """ + super(ConnectionHandler, self).__init__() + self.daemon = True + self.conn_socket = conn_socket + self.addr = addr + self.timeout = timeout + self.done = False + + def handle_connection(self): + """Handle a new connection""" + self.rp = RequestParser() + self.rc = ResponseComposer(timeout=self.timeout) + + self.conn_socket.settimeout(self.timeout) + + try: + while not self.done: + data = self.conn_socket.recv(32) + if len(data) == 0: + break + self.handle_data(data) + except socket.timeout: + logging.debug('%s connection timed out.' % self.addr[0]) + finally: + self.conn_socket.shutdown(socket.SHUT_RDWR) + self.conn_socket.close() + logging.debug('%s connection closed.' % self.addr[0]) + + 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) + + if resp.get_header('Connection') == 'close': + self.done = True + return + + 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""" + try: + self.handle_connection() + except socket.error, e: + logging.error('Error in handling connection with %s: %s' % + (self.addr[0], str(e))) + + +class Server: + """HTTP Server""" + + def __init__(self, configfile, **kwargs): + """Initialize the HTTP server + + Args: + hostname (str): hostname of the server + server_port (int): port that the server is listening on + timeout (int): seconds until timeout + """ + self.read_config(configfile, **kwargs) + self.done = False + + 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""" + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.bind((config('hostname', default='localhost'), + config('port', type=int, default=8001))) + self.socket.listen(config('max_connections', type=int, default=1000)) + while not self.done: + (csocket, addr) = self.socket.accept() + logging.debug('%s connection accepted.' % addr[0]) + 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 c01df4e..44152a0 100644 --- a/project1/proj1_s4498062/webserver.py +++ b/project1/proj1_s4498062/webserver.py @@ -1,51 +1,51 @@ -import argparse -import logging -import os.path -import sys - -import webhttp.server -import webhttp.weblogging - -# Create and start the HTTP Server -# Use `python webserver.py --help` to display command line options -if __name__ == '__main__': - # Parse command line arguments - 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=None, - help='address to listen on (default localhost)') - 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] %(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) - handler = logging.FileHandler(args.log_file) - handler.setLevel(getattr(logging, args.log.upper())) - logger.addHandler(handler) - - # Start server - config = os.path.expanduser(os.path.expandvars(args.config)) - server = webhttp.server.Server(configfile=config, - hostname=args.address, port=args.port, timeout=args.timeout) - try: - server.run() - except KeyboardInterrupt: - server.shutdown() - print ("") - - - +import argparse +import logging +import os.path +import sys + +import webhttp.server +import webhttp.weblogging + +# Create and start the HTTP Server +# Use `python webserver.py --help` to display command line options +if __name__ == '__main__': + # Parse command line arguments + 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=None, + help='address to listen on (default localhost)') + 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] %(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) + handler = logging.FileHandler(args.log_file) + handler.setLevel(getattr(logging, args.log.upper())) + logger.addHandler(handler) + + # Start server + config = os.path.expanduser(os.path.expandvars(args.config)) + server = webhttp.server.Server(configfile=config, + hostname=args.address, port=args.port, timeout=args.timeout) + try: + server.run() + except KeyboardInterrupt: + server.shutdown() + print ("") + + + diff --git a/project1/proj1_s4498062/webtests.py b/project1/proj1_s4498062/webtests.py index d047526..f2f6fbd 100644 --- a/project1/proj1_s4498062/webtests.py +++ b/project1/proj1_s4498062/webtests.py @@ -1,187 +1,187 @@ -import os.path -import unittest -import socket -import sys -import time - -from webhttp.config import config -import webhttp.message -import webhttp.parser - - -portnr = 8001 - - -class TestGetRequests(unittest.TestCase): - """Test cases for GET requests""" - - def setUp(self): - """Prepare for testing""" - self.client_skt = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.client_skt.connect(("localhost", portnr)) - self.parser = webhttp.parser.ResponseParser() - self.default_headers = [ - ('Host', 'localhost:%d' % portnr), - ('Connection', 'close') - ] - self.default_headers_pers = [ - ('Host', 'localhost:%d' % portnr), - ('Connection', 'keep-alive') - ] - - config().read(os.path.expanduser('~/.webpy.ini')) - - def tearDown(self): - """Clean up after testing""" - try: - self.client_skt.shutdown(socket.SHUT_RDWR) - self.client_skt.close() - except: - pass - - def refresh_socket(self): - try: - self.client_skt.shutdown(socket.SHUT_RDWR) - self.client_skt.close() - except: - pass - self.client_skt = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.client_skt.connect(("localhost", portnr)) - - def request(self, method, uri, headers, persistent=False): - request = webhttp.message.Request() - request.method = method - request.uri = uri - for name, value in headers: - request.set_header(name, value) - - sent = self.client_skt.send(str(request)) - if sent == 0: - return None - - message = self.client_skt.recv(1024) - - if not persistent: - self.refresh_socket() - - if message == '': - return None - else: - response = self.parser.parse_response(message) - return response - - def test_existing_file(self): - """GET for a single resource that exists""" - response = self.request( - 'GET', '/test/index.html', self.default_headers) - self.assertEqual(response.code, 200) - self.assertTrue(response.body) - - def test_nonexistant_file(self): - """GET for a single resource that does not exist""" - response = self.request( - 'GET', '/test/nonexistant.html', self.default_headers) - self.assertEqual(response.code, 404) - self.assertTrue(response.body) - - def test_caching(self): - """GET for an existing single resource followed by a GET for that same - resource with caching utilized on the client/tester side - """ - 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""" - self.assertEqual(self.request('GET', '/test', self.default_headers), - self.request('GET', '/test/index.html', self.default_headers)) - - def test_nonexistant_index_file(self): - """GET for a directory with a non-existant index.html file""" - response = self.request('GET', '/test/no-index', self.default_headers) - 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. - """ - try: - self.request('GET', '/test', self.default_headers_pers, True) - self.request('GET', '/test', self.default_headers_pers, True) - self.request('GET', '/test', self.default_headers_pers, True) - except socket.error: - self.fail('Persistent connection closed prematurely') - - self.request('GET', '/test', self.default_headers, True) - self.assertIsNone(self.request('GET', '/test', - self.default_headers_pers, True)) - - self.refresh_socket() - - def test_persistent_timeout(self): - """Multiple GETs over the same (persistent) connection, followed by a - wait during which the connection times out, the connection should be - closed. - """ - self.request('GET', '/test', self.default_headers_pers, True) - time.sleep(20) - self.assertIsNone(self.request('GET', '/test', - self.default_headers_pers, True)) - - self.refresh_socket() - - def test_encoding(self): - """GET which requests an existing resource using gzip encodign, which - is accepted by the server. - """ - r1 = self.request('GET', '/test', self.default_headers + \ - [('Accept-Encoding', 'gzip;q=1, identity;q=0')]) - self.assertEqual(r1.get_header('Content-Encoding'), 'gzip') - r1.decompress() - r2 = self.request('GET', '/test', self.default_headers + \ - [('Accept-Encoding', '')]) - self.assertEqual(r1.body, r2.body) - - def test_doubledot(self): - response = self.request('GET', '/../test', self.default_headers) - self.assertEquals(response.code, 403) - - -if __name__ == "__main__": - # Parse command line arguments - import argparse - parser = argparse.ArgumentParser(description="HTTP Tests") - parser.add_argument("-p", "--port", type=int, default=8001) - - # Arguments for the unittest framework - parser.add_argument('unittest_args', nargs='*') - args = parser.parse_args() - - portnr = args.port - - # Only pass the unittest arguments to unittest - sys.argv[1:] = args.unittest_args - - # Start test suite - unittest.main() +import os.path +import unittest +import socket +import sys +import time + +from webhttp.config import config +import webhttp.message +import webhttp.parser + + +portnr = 8001 + + +class TestGetRequests(unittest.TestCase): + """Test cases for GET requests""" + + def setUp(self): + """Prepare for testing""" + self.client_skt = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.client_skt.connect(("localhost", portnr)) + self.parser = webhttp.parser.ResponseParser() + self.default_headers = [ + ('Host', 'localhost:%d' % portnr), + ('Connection', 'close') + ] + self.default_headers_pers = [ + ('Host', 'localhost:%d' % portnr), + ('Connection', 'keep-alive') + ] + + config().read(os.path.expanduser('~/.webpy.ini')) + + def tearDown(self): + """Clean up after testing""" + try: + self.client_skt.shutdown(socket.SHUT_RDWR) + self.client_skt.close() + except: + pass + + def refresh_socket(self): + try: + self.client_skt.shutdown(socket.SHUT_RDWR) + self.client_skt.close() + except: + pass + self.client_skt = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.client_skt.connect(("localhost", portnr)) + + def request(self, method, uri, headers, persistent=False): + request = webhttp.message.Request() + request.method = method + request.uri = uri + for name, value in headers: + request.set_header(name, value) + + sent = self.client_skt.send(str(request)) + if sent == 0: + return None + + message = self.client_skt.recv(1024) + + if not persistent: + self.refresh_socket() + + if message == '': + return None + else: + response = self.parser.parse_response(message) + return response + + def test_existing_file(self): + """GET for a single resource that exists""" + response = self.request( + 'GET', '/test/index.html', self.default_headers) + self.assertEqual(response.code, 200) + self.assertTrue(response.body) + + def test_nonexistant_file(self): + """GET for a single resource that does not exist""" + response = self.request( + 'GET', '/test/nonexistant.html', self.default_headers) + self.assertEqual(response.code, 404) + self.assertTrue(response.body) + + def test_caching(self): + """GET for an existing single resource followed by a GET for that same + resource with caching utilized on the client/tester side + """ + 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""" + self.assertEqual(self.request('GET', '/test', self.default_headers), + self.request('GET', '/test/index.html', self.default_headers)) + + def test_nonexistant_index_file(self): + """GET for a directory with a non-existant index.html file""" + response = self.request('GET', '/test/no-index', self.default_headers) + 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. + """ + try: + self.request('GET', '/test', self.default_headers_pers, True) + self.request('GET', '/test', self.default_headers_pers, True) + self.request('GET', '/test', self.default_headers_pers, True) + except socket.error: + self.fail('Persistent connection closed prematurely') + + self.request('GET', '/test', self.default_headers, True) + self.assertIsNone(self.request('GET', '/test', + self.default_headers_pers, True)) + + self.refresh_socket() + + def test_persistent_timeout(self): + """Multiple GETs over the same (persistent) connection, followed by a + wait during which the connection times out, the connection should be + closed. + """ + self.request('GET', '/test', self.default_headers_pers, True) + time.sleep(20) + self.assertIsNone(self.request('GET', '/test', + self.default_headers_pers, True)) + + self.refresh_socket() + + def test_encoding(self): + """GET which requests an existing resource using gzip encodign, which + is accepted by the server. + """ + r1 = self.request('GET', '/test', self.default_headers + \ + [('Accept-Encoding', 'gzip;q=1, identity;q=0')]) + self.assertEqual(r1.get_header('Content-Encoding'), 'gzip') + r1.decompress() + r2 = self.request('GET', '/test', self.default_headers + \ + [('Accept-Encoding', '')]) + self.assertEqual(r1.body, r2.body) + + def test_doubledot(self): + response = self.request('GET', '/../test', self.default_headers) + self.assertEquals(response.code, 403) + + +if __name__ == "__main__": + # Parse command line arguments + import argparse + parser = argparse.ArgumentParser(description="HTTP Tests") + parser.add_argument("-p", "--port", type=int, default=8001) + + # Arguments for the unittest framework + parser.add_argument('unittest_args', nargs='*') + args = parser.parse_args() + + portnr = args.port + + # Only pass the unittest arguments to unittest + sys.argv[1:] = args.unittest_args + + # Start test suite + unittest.main() -- cgit v1.2.3