-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathHTTPServer.py
More file actions
114 lines (105 loc) · 4.77 KB
/
Copy pathHTTPServer.py
File metadata and controls
114 lines (105 loc) · 4.77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import os
import mimetypes
from datetime import datetime
from httpserver.TCPServer import TCPServer
from httpserver.HTTPConnectionHandler import HTTPConnectionHandler, HTTPResponse, BadRequestError, RecvTimeoutError
from httpserver.utils import debugprint
def get_last_modified_formatted_string(requested_path):
timestamp = os.path.getmtime(requested_path)
time_format = '%a, %d %b %y %T %z'
return datetime.fromtimestamp(timestamp).strftime(time_format)
class HTTPServer(TCPServer):
"""
Wrapper for TCPServer that implements HTTP protocol
"""
def __init__(self, port):
# We support only GET, so use daemon threads
TCPServer.__init__(self, port, self.handle_tcp_connection, use_daemon_threads=True)
self.serve_docroot = None
self.serve_config = {}
def handle_tcp_connection(self, connection, client_address):
http_connection = HTTPConnectionHandler(connection, client_address)
try:
# Continue to get request(s) over the socket
# Until:
# client closes the connection,
# client timeout,
# request header is not keep-alive,
# or bad request from client.
while True:
request = http_connection.get_request()
if not request:
break
# TODO: Allow choosing to handle request or serve file
self.__serve_file(request, http_connection)
if not request.is_connection_keep_alive():
break
except RecvTimeoutError:
if len(http_connection.unprocessed_data) > 0:
# There's an incomplete request on timeout
http_connection.send_response(HTTPResponse.client_error_400())
except BadRequestError as e:
debugprint('Bad Request Error', e)
http_connection.send_response(HTTPResponse.client_error_400())
finally:
http_connection.close()
def serve(self, docroot, serve_config={}):
"""
Serve files from `docroot`.
Can pass config as dict with keys mapped to html pages, i.e.:
{
'index': 'custom_index.html',
'400': '400.html',
'404': '404.html'
}
Note that paths must be relative to `docroot`. Also 'index.html' is the default mapping for 'index'.
"""
self.serve_docroot = docroot
self.serve_config = serve_config
def get_index_html_path(self):
"""
Get index.html (or any from config) path.
"""
return os.path.join('/', self.serve_config.get('index', 'index.html'))
def get_abspath_relative_to_docroot(self, path):
assert self.serve_docroot is not None, 'Must setup `serve_docroot` first.'
return os.path.abspath(os.path.join(self.serve_docroot, path))
def __serve_file(self, request, http_connection):
"""
Return HTTPResponse for serving file.
"""
requested_path = self.get_index_html_path() if request.path == '/' else request.path
if requested_path.startswith('/'):
requested_path = requested_path[1:]
abs_requested_path = self.get_abspath_relative_to_docroot(requested_path)
abs_docroot_path = os.path.abspath(self.serve_docroot)
del requested_path
# Won't serve out of docroot
if not abs_requested_path.startswith(abs_docroot_path):
if '400' in self.serve_config:
http_connection.send_response(HTTPResponse.client_error_400())
http_connection.send_body(self.get_abspath_relative_to_docroot(self.serve_config['400']))
else:
http_connection.send_response(HTTPResponse.client_error_400())
return
# File not exists
if not os.path.exists(abs_requested_path):
if '404' in self.serve_config:
http_connection.send_response(HTTPResponse.not_found_404())
http_connection.send_body(self.get_abspath_relative_to_docroot(self.serve_config['404']))
else:
http_connection.send_response(HTTPResponse.not_found_404())
return
mimetype, _ = mimetypes.guess_type(abs_requested_path)
file_size = os.path.getsize(abs_requested_path)
last_modified_time = get_last_modified_formatted_string(abs_requested_path)
response_headers = {
'Content-Type': mimetype,
'Content-Length': file_size,
'Last-Modified': last_modified_time
}
# Send first-line and headers
http_connection.send_response(HTTPResponse(200, headers=response_headers))
# Send body
n_bytes_sent = http_connection.send_file(abs_requested_path)
assert n_bytes_sent == file_size, 'Incomplete file sent.'