-
Notifications
You must be signed in to change notification settings - Fork 0
Concurrency #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Concurrency #13
Changes from all commits
625e458
64fd0ac
8563ed8
aa70528
bff0a04
5bfe7cd
323776d
50f53cb
087ea6c
ae6635d
7705777
3f192a3
2f814d9
72f44dd
24d106f
ae5524a
942c4d3
4988e35
174db8e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,24 @@ | ||
| # code_401_wk2_http | ||
| Code 401 week 2 HTTP server project | ||
|
|
||
| Step 1: | ||
| - open listening socket using socket library | ||
| - server loop to take incoming requests and send them back | ||
| - client loop to receive requests and return them to console | ||
|
|
||
|
|
||
| Step 2: | ||
| - response_ok function to send HTTP 200 code response | ||
| - response_error function to send HTTP 500 server error response | ||
|
|
||
|
|
||
| Step 3: | ||
| - added request validation | ||
| - added error codes for bad requests | ||
| - extract URI from valid response. | ||
| - return appropriate HTTP response to client | ||
|
|
||
|
|
||
| Step Final: | ||
| - added gevent to support server concurrency | ||
| - modified tests to be OS independent (in parallel with step 3) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| # -*- coding: utf-8 -*- | ||
| """Gevent powered concurrency server.""" | ||
| import sys | ||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
| from gevent.server import StreamServer | ||
| from gevent.monkey import patch_all | ||
| from server import http_server | ||
| patch_all() | ||
| server = StreamServer(('127.0.0.1', 5000), http_server) | ||
| print('Starting http server on port 5000') | ||
| try: | ||
| server.serve_forever() | ||
| except Exception as e: | ||
| print(e.msg) | ||
| finally: | ||
| print('\nShutting down the server...\n') | ||
| server.close() | ||
| sys.exit() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,24 +1,59 @@ | ||
| # -*- coding: utf-8 -*- | ||
| """Server module to to take HTTP requests.""" | ||
| import io | ||
| import mimetypes | ||
| import os | ||
| import socket | ||
| import sys | ||
| import time | ||
| from contextlib import closing | ||
|
|
||
| OK_200 = b'200 OK' | ||
| ERR_400 = b'400: Bad Request' | ||
| ERR_405 = b'405: Method Not Allowed' | ||
| ERR_505 = b'505: HTTP Version Not Supported' | ||
| BUFFER_LENGTH = 8 | ||
| ADDRINFO = ('127.0.0.1', 5000) | ||
|
|
||
|
|
||
| # TODO: update response assemble and tests for more sophisticated | ||
| # testing of each component. Assert for required parts while disrgarding | ||
| # optional components (but making sure they are in the right place). | ||
| # split on \r\n\r\n assert there are 2 pieces | ||
| # for first line of response, use maxsplit arg to split | ||
| # e.g. "HTTP/1.1 500 Internal Server Error".split(' ', maxsplit=2) | ||
| # >>> ["HTTP/1.1", "500", "Internal Server Error"] | ||
| PARENT_DIR = os.path.dirname(__file__) | ||
| GRANDPARENT_DIR = os.path.join(PARENT_DIR, '..') | ||
| WEBROOT_PATH = os.path.join(GRANDPARENT_DIR, 'webroot', '') | ||
|
|
||
| # Unicode constants for standard re-usable parts. | ||
| CRLF = '\r\n' | ||
| GET = 'GET' | ||
| HTTP1_1 = 'HTTP/1.1' | ||
| HOST = 'Host: ' | ||
| CONTENT_TYPE = 'Content-Type: {}' | ||
| CONTENT_LENGTH = 'Content-Length: {}' | ||
|
|
||
| TEXT_HTML = 'text/html; charset=utf-8' | ||
| TEXT_PLAIN = 'text/plain; charset=utf-8' | ||
|
|
||
| # Easy Reference dictionary with int keys for code/message part. | ||
| HTTP_CODES = { | ||
| 200: '200 OK', | ||
| 400: '400 Bad Request', | ||
| 404: '404 Not Found', | ||
| 405: '405 Method Not Allowed', | ||
| 505: '505 HTTP Version Not Supported', | ||
| } | ||
|
|
||
| # updated HTML templates to use unicode strings | ||
| HTML_TEMPLATE = ''' | ||
| <!DOCTYPE html> | ||
| <html> | ||
| <head> | ||
| <meta charset="utf-8"> | ||
| <title></title> | ||
| </head> | ||
| <body> | ||
| <h3>{header}</h3> | ||
| <ul> | ||
| {list_items} | ||
| </ul> | ||
| </body> | ||
| </html> | ||
| ''' | ||
| HTML_LI_TEMPLATE = ''' | ||
| <li><a href="{full_path}">{short_path}</a></li> | ||
| ''' | ||
|
|
||
|
|
||
| def server(): | ||
|
|
@@ -33,94 +68,155 @@ def server(): | |
| try: | ||
| while True: | ||
| conn, addr = serv_sock.accept() | ||
| request = [] | ||
| while True: | ||
| part = conn.recv(BUFFER_LENGTH) | ||
| request.append(part) | ||
| # conn.sendall(part) | ||
| if len(part) < BUFFER_LENGTH: | ||
| break | ||
| request = b''.join(request) | ||
| print('Request received:') | ||
| print(request.decode('utf-8')) | ||
| try: | ||
| uri = parse_request(request) | ||
| response = response_ok() | ||
| except ValueError as e: | ||
| response = response_error(*e.args) | ||
| conn.sendall(response) | ||
| conn.close() | ||
| time.sleep(0.01) | ||
| except KeyboardInterrupt: | ||
| pass | ||
| http_server(conn, addr) | ||
| except Exception as e: | ||
| print(e.msg) | ||
| finally: | ||
| try: | ||
| conn.close() | ||
| except NameError: | ||
| pass | ||
| print('\nShutting down the server...\n') | ||
| serv_sock.close() | ||
| sys.exit() | ||
|
|
||
|
|
||
| def parse_request(request): | ||
| """TOOD: Docstring.""" | ||
| # only accept GET | ||
| # only HTTP/1.1 | ||
| # validate that proper Host: header was specified | ||
| # other requests raise appropriate Python error | ||
| # if no conditions arise, should return the URI | ||
| method = b'' | ||
| uri = b'' | ||
| protocol = b'' | ||
| headers = b'' | ||
| def http_server(conn, addr): | ||
| """Take HTTP requests and return appropriate HTTP response.""" | ||
| with closing(conn): | ||
| request_parts = [] | ||
| while True: | ||
| part = conn.recv(BUFFER_LENGTH) | ||
| request_parts.append(part) | ||
| if len(part) < BUFFER_LENGTH: | ||
| break | ||
| # Immediately decode all of incoming request into unicode. | ||
| # In future, may need to check Content-type of incoming request? | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. absolutely yes. If the incoming request has a body, then you'd want to check both the content type and the encoding before decoding. |
||
| request = b''.join(request_parts).decode('utf-8') | ||
| print('Request received:\n{}'.format(request)) | ||
| try: | ||
| uri = parse_request(request) | ||
| # Here body might be a bytestring. | ||
| body, content_type = resolve_uri(uri) | ||
| body_length = len(body) | ||
| response_headers = response_ok(content_type, body_length) | ||
|
|
||
| try: | ||
| headers, body = request.split(b'\r\n\r\n') | ||
| except ValueError: | ||
| raise ValueError(ERR_400) | ||
| except ValueError as e: | ||
| err_code = e.args[0] | ||
| response_headers = response_error(err_code) | ||
| body = HTTP_CODES[err_code].encode('utf-8') | ||
|
|
||
| # Re-encode into bytes on the way out. | ||
| response_headers = response_headers.encode('utf-8') | ||
|
|
||
| response = b''.join([response_headers, body]) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like that the implicit contract here is that your |
||
|
|
||
| conn.sendall(response) | ||
| conn.close() | ||
| time.sleep(0.01) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. out of curiosity, what's this about? |
||
|
|
||
| try: | ||
| headers = headers.split(b'\r\n') | ||
| first_line = headers[0] | ||
| except (ValueError, IndexError): | ||
| raise ValueError(ERR_400) | ||
|
|
||
| def parse_request(request): | ||
| """Parse client request.""" | ||
| method = '' | ||
| uri = '' | ||
| protocol = '' | ||
| headers = '' | ||
|
|
||
| # I was able to simplify this into one try/except block. | ||
| # The key was to organize it by error type. | ||
| try: | ||
| headers, body = request.split(CRLF * 2) | ||
| headers = headers.split(CRLF) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I really like this approach. Split the headers away from the body, then divide up the headers into lines. |
||
| try: | ||
| first_line = headers[0] | ||
| except IndexError: | ||
| raise ValueError(400) | ||
| method, uri, protocol = first_line.split() | ||
| headers = headers[1:] | ||
| for h in headers: | ||
| if h.startswith(HOST): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Be a little careful here. Remember that the names of headers are not case sensitive. I would suggest that you actually want to do something a bit more like this: if h.lower().startswith(HOST.lower()):
break |
||
| break | ||
| else: | ||
| raise ValueError(400) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. very nice use of the else clause of a loop! |
||
| except ValueError: | ||
| raise ValueError(ERR_400) | ||
| raise ValueError(400) | ||
| if method != GET: | ||
| raise ValueError(405) | ||
| if protocol != HTTP1_1: | ||
| raise ValueError(505) | ||
|
|
||
| return uri | ||
|
|
||
|
|
||
| def resolve_uri(uri): | ||
| """Return tuple of content and content type, or raise 404 error.""" | ||
| print('Requested URI: ', uri) | ||
| uri = full_uri(uri) | ||
| print('URI after join: ', uri) | ||
|
|
||
| headers = headers[1:] | ||
| for h in headers: | ||
| if h.startswith(b'Host: '): | ||
| break | ||
| if os.path.isfile(uri): | ||
| content_type = mimetypes.guess_type(uri)[0] | ||
| body = read_file_bytes(uri) | ||
|
|
||
| elif os.path.isdir(uri): | ||
| content_type = TEXT_HTML | ||
| body = display(next(os.walk(uri))) | ||
| body = body.encode('utf-8') | ||
| else: | ||
| raise ValueError(ERR_400) | ||
| print(uri, 'is not a file or dir.') | ||
| raise ValueError(404) | ||
| return (body, content_type) | ||
|
|
||
| if method != b'GET': | ||
| raise ValueError(ERR_405) | ||
| if protocol != b'HTTP/1.1': | ||
| raise ValueError(ERR_505) | ||
|
|
||
| def full_uri(uri): | ||
| """Take a unicode uri from webroot and return the absolute path.""" | ||
| uri = os.path.join(*uri.split('/')) | ||
| uri = os.path.join(WEBROOT_PATH, uri) | ||
| return uri | ||
|
|
||
|
|
||
| def response_ok(): | ||
| def web_uri(uri): | ||
| """Take a unicode uri and return the uri from webroot.""" | ||
| return uri.replace(WEBROOT_PATH, '') | ||
|
|
||
|
|
||
| def read_file_bytes(path): | ||
| """Return the data in bytestring format from the file at a give path.""" | ||
| f = io.open(path, 'rb') | ||
| data = f.read() | ||
| f.close() | ||
| return data | ||
|
|
||
|
|
||
| # Response_ok doesn't parse body (might be bytes as for an image). | ||
| # References key 200 from HTTP_CODES reference dictionary. | ||
| def response_ok(content_type, body_length): | ||
| """Return 'HTTP/1.1 200 OK' for when connection ok.""" | ||
| return (b'HTTP/1.1 %s\r\n' | ||
| b'Content-Type: text/plain\r\n' | ||
| b'\r\n' | ||
| b'Welcome to Imperial Space, rebel scum.\n' | ||
| b'|-o-| <-o-> |-o-|') % OK_200 | ||
| return CRLF.join([' '.join([HTTP1_1, HTTP_CODES[200]]), | ||
| CONTENT_TYPE.format(content_type), | ||
| CONTENT_LENGTH.format(body_length), | ||
| CRLF]) | ||
|
|
||
|
|
||
| def response_error(err_msg): | ||
| # Response_error take an int as err_code, to reference HTTP_CODES dict. | ||
| def response_error(err_code): | ||
| """Return 'Internal Server Error' for when problem occurs.""" | ||
| return (b'HTTP/1.1 %s\r\n' | ||
| b'Content-Type: text/plain\r\n' | ||
| b'\r\n' | ||
| b'Death Star Error. Please build again.') % err_msg | ||
| return CRLF.join([' '.join([HTTP1_1, HTTP_CODES[err_code]]), | ||
| CONTENT_TYPE.format(TEXT_PLAIN), | ||
| CRLF]) | ||
|
|
||
|
|
||
| def display(threeple): | ||
| """Split dir threeple into components and return as HTML.""" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| cur_dir, dir_subdir, dir_files = threeple | ||
| cur_dir = web_uri(cur_dir) | ||
| dir_list = [] | ||
| for i in dir_subdir + dir_files: | ||
| if i.startswith('._'): | ||
| continue | ||
| full_path = os.path.join(cur_dir, i) | ||
| html_li = HTML_LI_TEMPLATE.format(full_path=full_path, short_path=i) | ||
| dir_list.append(html_li) | ||
|
|
||
| return HTML_TEMPLATE.format(header=cur_dir, | ||
| list_items=''.join(dir_list)) | ||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice use of "templating" to solve the problem of building a good HTML representation of your directories. Well thought out!