-
Notifications
You must be signed in to change notification settings - Fork 26
Expand file tree
/
Copy pathwebframework.py
More file actions
365 lines (278 loc) · 12 KB
/
Copy pathwebframework.py
File metadata and controls
365 lines (278 loc) · 12 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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
# noinspection PyCompatibility
import http.server
# noinspection PyCompatibility
from socketserver import ThreadingMixIn
try:
# noinspection PyCompatibility
import urlparse
import Cookie as cookies
except ImportError:
#py3
# noinspection PyCompatibility
import urllib.parse as urlparse
from http import cookies
import json
import logging
logger = logging.getLogger(__name__)
WEBSOCKET_MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
def register_endpoint(path, output_is_json=True, mimetype='application/json', compress=True, cookies=False, authenticate=False):
def _reg_ep(func):
#_endpoints[path] = func
func._expose_path = path
func._jsonify = not output_is_json
func._mimetype = mimetype
func._compress = compress
func._parse_cookies = cookies
func._authenticate = authenticate
return func
return _reg_ep
class HTTPResponse(object):
""" permit setting headers etc"""
def __init__(self, body, headers=None, response_code=200):
self.body = body
self.headers= headers
self.response_code = response_code
def write_response(self, method, handler):
resp = self.body
if method._jsonify:
resp = json.dumps(resp)
compress_output = method._compress and ('gzip' in handler.headers.get('Accept-Encoding', ''))
handler.send_response(self.response_code)
handler.send_header("Content-Type", method._mimetype)
if compress_output:
handler.send_header('Content-Encoding', 'gzip')
resp = handler._gzip_compress(resp) #FIXME - write directly to wfile rather than to a BytesIO object - how do we find the content-length?
handler.send_header("Content-Length", "%d" % len(resp))
if self.headers is not None:
for k, v in self.headers:
handler.send_header(k, v)
handler.end_headers()
handler.wfile.write(resp)
class HTTPRedirectResponse(HTTPResponse):
def __init__(self, redirect_to, headers=None, response_code=303):
new_headers = [('Location', redirect_to)]
if not headers is None:
new_headers.extend(headers)
HTTPResponse.__init__(self, '', new_headers, response_code)
import mimetypes
class StaticFileHandler(object):
""" Adapted from std. library SimpleHTTPServer"""
def __init__(self, static_path):
#TODO - sanity checks on static path
self.static_path = static_path
def _translate_static_path(self, path):
"""Translate a /-separated PATH to the local filename syntax.
Components that mean special things to the local file system
(e.g. drive or directory names) are ignored. (XXX They should
probably be diagnosed.)
Modified from SimpleHTTPServer.translate_path
"""
import os, posixpath
if not self.static_path:
raise RuntimeError('Static file handling not configured')
# abandon query parameters
path = path.split('?', 1)[0]
path = path.split('#', 1)[0]
# Don't forget explicit trailing slash when normalizing. Issue17324
trailing_slash = path.rstrip().endswith('/')
path = posixpath.normpath(urlparse.unquote(path))
words = path.split('/')
words = filter(None, words)
path = self.static_path
for word in words:
drive, word = os.path.splitdrive(word)
head, word = os.path.split(word)
if word in (os.curdir, os.pardir):
raise RuntimeError('Attempted to acess a file out of current tree')
path = os.path.join(path, word)
if trailing_slash:
path += '/'
return path
def _send_static_head(self, path, session):
"""Common code for GET and HEAD commands.
This sends the response code and MIME headers.
Return value is either a file object (which has to be copied
to the outputfile by the caller unless the command was HEAD,
and must be closed by the caller under all circumstances), or
None, in which case the caller has nothing further to do.
- adapted from SimpleHTTPServer
"""
import os
path = self._translate_static_path(path)
f = None
ctype = self.guess_type(path)
try:
# Always read in binary mode. Opening files in text mode may cause
# newline translations, making the actual size of the content
# transmitted *less* than the content-length!
f = open(path, 'rb')
except IOError:
session.send_error(404, "File not found")
return None
try:
session.send_response(200)
session.send_header("Content-type", ctype)
fs = os.fstat(f.fileno())
session.send_header("Content-Length", str(fs[6]))
session.send_header("Last-Modified", session.date_time_string(fs.st_mtime))
session.end_headers()
return f
except:
f.close()
raise
def guess_type(self, path):
"""Guess the type of a file.
Argument is a PATH (a filename).
Return value is a string of the form type/subtype,
usable for a MIME Content-type header.
The default implementation looks the file's extension
up in the table self.extensions_map, using application/octet-stream
as a default; however it would be permissible (if
slow) to look inside the data to make a better guess.
"""
import posixpath
base, ext = posixpath.splitext(path)
if ext in self.extensions_map:
return self.extensions_map[ext]
ext = ext.lower()
if ext in self.extensions_map:
return self.extensions_map[ext]
else:
return self.extensions_map['']
if not mimetypes.inited:
mimetypes.init() # try to read system mime.types
extensions_map = mimetypes.types_map.copy()
extensions_map.update({
'': 'application/octet-stream', # Default
'.py': 'text/plain',
'.c': 'text/plain',
'.h': 'text/plain',
})
def get_file(self, path, session):
import shutil
f = self._send_static_head(path, session)
if f:
try:
shutil.copyfileobj(f, session.wfile)
finally:
f.close()
class JSONAPIRequestHandler(http.server.BaseHTTPRequestHandler):
protocol_version='HTTP/1.1'
logrequests = False
def _gzip_compress(self, data):
import gzip
from io import BytesIO
if not isinstance(data, bytes):
data = data.encode()
zbuf = BytesIO()
zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=3)
zfile.write(data)
zfile.close()
return zbuf.getvalue()
def _gzip_decompress(self, data):
import gzip
from io import BytesIO
zbuf = BytesIO(data)
zfile = gzip.GzipFile(mode='rb', fileobj=zbuf)#, compresslevel=9)
out = zfile.read()
zfile.close()
return out
def _process_request(self):
#import gzip
up = urlparse.urlparse(self.path)
kwargs = urlparse.parse_qs(up.query)
kwargs = {k : v[0] for k, v in kwargs.items()}
cl = int(self.headers.get('Content-Length', 0))
if cl > 0:
body = self.rfile.read(cl)
if self.headers.get('Content-Encoding') == 'gzip':
body = self._gzip_decompress(body)
kwargs['body'] = body
#logger.debug('Request path: ' + up.path)
#logger.debug('Requests args: ' + repr(kwargs))
try:
handler = self.server._endpoints[up.path]
except KeyError:
# handle static file requests
for prefix, handler in self.server.static_handlers.items():
relpath = up.path.lstrip('/')
if relpath.startswith(prefix):
return handler.get_file(relpath[len(prefix):], self)
self.send_error(404, 'No handler for %s' % up.path)
return
try:
kwargs.pop('authenticated_as') #if anything fails we are not authenticated NB - this stops people from passing authenticated_as on the query string.
except KeyError:
pass
if handler._parse_cookies or handler._authenticate:
req_cookies = cookies.SimpleCookie(self.headers.get('Cookie'))
if handler._parse_cookies:
kwargs['cookies'] = req_cookies
if handler._authenticate:
from PYME.util import authenticate
try:
auth_token = req_cookies.get('auth').value
kwargs['authenticated_as'] = authenticate.validate_token(auth_token)['email']
except:
pass
if self.headers.get('Upgrade', None) == 'websocket':
self._websocket_upgrade(handler, kwargs)
return
try:
resp = handler(**kwargs)
except Exception as e:
logger.exception('Exception in handler %s' % handler)
import traceback
explain = f''' {handler.__module__}.{handler.__name__}({', '.join(['%s=%s' % (k, repr(v)) for k, v in kwargs.items()])})
{e.__class__.__name__}: {e}
{traceback.format_exc()}
'''
self.send_error(500, message='Server Error', explain=explain)
return
if isinstance(resp, HTTPResponse):
resp.write_response(handler, self)
return
if handler._jsonify:
resp = json.dumps(resp)
compress_output = handler._compress and ('gzip' in self.headers.get('Accept-Encoding', ''))
self.send_response(200)
self.send_header("Content-Type", handler._mimetype)
if compress_output:
self.send_header('Content-Encoding', 'gzip')
resp = self._gzip_compress(resp) #FIXME - write directly to wfile rather than to a BytesIO object - how do we find the content-length?
self.send_header("Content-Length", "%d" % len(resp))
self.end_headers()
self.wfile.write(resp)
return
def _websocket_upgrade(self, handler, kwargs):
ws_key = self.headers.get('Sec-WebSocket-Key')
ws_version = self.headers.get('Sec-WebSocket-Version', 0)
self.send_response(101)
def do_GET(self):
return self._process_request()
def do_POST(self):
return self._process_request()
def log_request(self, code='-', size='-'):
"""Log an accepted request.
This is called by send_response().
"""
if self.logrequests:
self.log_message('"%s" %s %s', self.requestline, str(code), str(size))
class APIHTTPServer(ThreadingMixIn, http.server.HTTPServer):
def __init__(self, server_address, static_handlers = None):
http.server.HTTPServer.__init__(self, server_address, JSONAPIRequestHandler)
#make a mapping of endpoints to functions
self._endpoints = {}
self.add_endpoints(self)
logging.debug('Registered endpoints: %s' % self._endpoints.keys())
self.static_handlers = {}
if not static_handlers is None:
self.static_handlers.update(static_handlers)
def add_static_handler(self, prefix, handler):
self.static_handlers[prefix] = handler
def add_endpoints(self, cls, prefix=''):
for a in dir(cls):
func = getattr(cls, a, None)
endpoint_path = getattr(func, '_expose_path', None)
if not endpoint_path is None:
self._endpoints[prefix + endpoint_path] = func