1
|
|
|
"""Handles the HTTP frontend (ie. answers to requests from a |
2
|
|
|
UI.""" |
3
|
|
|
|
4
|
1 |
|
import time |
5
|
1 |
|
import json |
6
|
1 |
|
import logging |
7
|
|
|
|
8
|
1 |
|
from ppp_datamodel.exceptions import AttributeNotProvided |
9
|
1 |
|
from ppp_datamodel.communication import Request |
10
|
|
|
|
11
|
1 |
|
from .config import Config |
12
|
1 |
|
from .exceptions import ClientError, BadGateway, InvalidConfig |
13
|
|
|
|
14
|
1 |
|
DOC_URL = 'https://github.com/ProjetPP/Documentation/blob/master/' \ |
15
|
|
|
'module-communication.md#frontend' |
16
|
|
|
|
17
|
1 |
|
class HttpRequestHandler: |
18
|
|
|
"""Handles one request.""" |
19
|
1 |
|
def __init__(self, environ, start_response, router_class): |
20
|
1 |
|
self.environ = environ |
21
|
1 |
|
self.start_response = start_response |
22
|
1 |
|
self.router_class = router_class |
23
|
1 |
|
def make_response(self, status, content_type, response): |
24
|
|
|
"""Shortcut for making a response to the client's request.""" |
25
|
1 |
|
headers = [('Access-Control-Allow-Origin', '*'), |
26
|
|
|
('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'), |
27
|
|
|
('Access-Control-Allow-Headers', 'Content-Type'), |
28
|
|
|
('Access-Control-Max-Age', '86400'), |
29
|
|
|
('Content-type', content_type) |
30
|
|
|
] |
31
|
1 |
|
self.start_response(status, headers) |
32
|
1 |
|
return [response.encode()] |
33
|
|
|
|
34
|
1 |
|
def on_bad_method(self): |
35
|
|
|
"""Returns a basic response to GET requests (probably sent by humans |
36
|
|
|
trying to open the link in a web browser.""" |
37
|
1 |
|
text = 'Bad method, only POST is supported. See: ' + DOC_URL |
38
|
1 |
|
return self.make_response('405 Method Not Allowed', |
39
|
|
|
'text/plain', |
40
|
|
|
text |
41
|
|
|
) |
42
|
|
|
|
43
|
1 |
|
def on_unknown_uri(self): |
44
|
|
|
"""Returns a basic response to GET requests (probably sent by humans |
45
|
|
|
trying to open the link in a web browser.""" |
46
|
|
|
text = 'URI not found, only / is supported. See: ' + DOC_URL |
47
|
|
|
return self.make_response('404 Not Found', |
48
|
|
|
'text/plain', |
49
|
|
|
text |
50
|
|
|
) |
51
|
|
|
|
52
|
1 |
|
def on_bad_request(self, hint): |
53
|
|
|
"""Returns a basic response to invalid requests.""" |
54
|
1 |
|
return self.make_response('400 Bad Request', |
55
|
|
|
'text/plain', |
56
|
|
|
hint |
57
|
|
|
) |
58
|
|
|
|
59
|
1 |
|
def on_bad_gateway(self, exc): |
60
|
|
|
"""Returns a basic response when a module is buggy.""" |
61
|
|
|
return self.make_response('502 Bad Gateway', |
62
|
|
|
'text/plain', |
63
|
|
|
exc.args[0] |
64
|
|
|
) |
65
|
|
|
|
66
|
1 |
|
def on_client_error(self, exc): |
67
|
|
|
"""Handler for any error in the request detected by the module.""" |
68
|
1 |
|
return self.on_bad_request(exc.args[0]) |
69
|
|
|
|
70
|
|
|
def on_internal_error(self): # pragma: no cover |
71
|
|
|
"""Returns a basic response when the module crashed""" |
72
|
|
|
return self.make_response('500 Internal Server Error', |
73
|
|
|
'text/plain', |
74
|
|
|
'Internal server error. Sorry :/' |
75
|
|
|
) |
76
|
|
|
|
77
|
1 |
|
def _get_times(self): |
78
|
1 |
|
wall_time = time.time() |
79
|
1 |
|
get_process_time = getattr(time, 'process_time', None) |
80
|
1 |
|
if get_process_time: # Python ≥ 3.3 only |
81
|
1 |
|
process_time = get_process_time() |
82
|
|
|
else: |
83
|
|
|
process_time = None |
84
|
1 |
|
return (wall_time, process_time) |
85
|
|
|
|
86
|
1 |
|
def _add_times_to_answers(self, answers, start_wall_time, start_process_time): |
87
|
1 |
|
(end_wall_time, end_process_time) = self._get_times() |
88
|
1 |
|
times_dict = {'start': start_wall_time, 'end': end_wall_time} |
89
|
1 |
|
if start_wall_time and end_process_time: |
90
|
1 |
|
times_dict['cpu'] = end_process_time - start_process_time |
91
|
1 |
|
for answer in answers: |
92
|
1 |
|
if not answer.trace: |
93
|
|
|
continue |
94
|
1 |
|
if answer.trace[0].times == {}: |
95
|
1 |
|
answer.trace[0].times.update(times_dict) |
96
|
|
|
|
97
|
1 |
|
def process_request(self, request): |
98
|
|
|
"""Processes a request.""" |
99
|
1 |
|
try: |
100
|
1 |
|
request = Request.from_json(request.read().decode()) |
101
|
1 |
|
except ValueError: |
102
|
1 |
|
raise ClientError('Data is not valid JSON.') |
103
|
1 |
|
except KeyError: |
104
|
|
|
raise ClientError('Missing mandatory field in request object.') |
105
|
1 |
|
except AttributeNotProvided as exc: |
106
|
1 |
|
raise ClientError('Attribute not provided: %s.' % exc.args[0]) |
107
|
|
|
|
108
|
1 |
|
(start_wall_time, start_process_time) = self._get_times() |
109
|
1 |
|
answers = self.router_class(request).answer() |
110
|
1 |
|
self._add_times_to_answers(answers, start_wall_time, start_process_time) |
111
|
|
|
|
112
|
1 |
|
answers = [x.as_dict() for x in answers] |
113
|
1 |
|
return self.make_response('200 OK', |
114
|
|
|
'application/json', |
115
|
|
|
json.dumps(answers) |
116
|
|
|
) |
117
|
|
|
|
118
|
1 |
|
def on_post(self): |
119
|
|
|
"""Extracts the request, feeds the module, and returns the response.""" |
120
|
1 |
|
request = self.environ['wsgi.input'] |
121
|
1 |
|
try: |
122
|
1 |
|
return self.process_request(request) |
123
|
1 |
|
except ClientError as exc: |
124
|
1 |
|
return self.on_client_error(exc) |
125
|
|
|
except BadGateway as exc: |
126
|
|
|
return self.on_bad_gateway(exc) |
127
|
|
|
except InvalidConfig: |
128
|
|
|
raise |
129
|
|
|
except Exception as exc: # pragma: no cover # pylint: disable=W0703 |
130
|
|
|
logging.error('Unknown exception: ', exc_info=exc) |
131
|
|
|
return self.on_internal_error() |
132
|
|
|
|
133
|
1 |
|
def on_options(self): |
134
|
|
|
"""Tells the client we allow requests from any Javascript script.""" |
135
|
|
|
return self.make_response('200 OK', 'text/html', '') |
136
|
|
|
|
137
|
1 |
|
def dispatch(self): |
138
|
|
|
"""Handles dispatching of the request.""" |
139
|
1 |
|
method_name = 'on_' + self.environ['REQUEST_METHOD'].lower() |
140
|
1 |
|
method = getattr(self, method_name, None) |
141
|
1 |
|
if method: |
142
|
1 |
|
return method() |
143
|
|
|
else: |
144
|
1 |
|
return self.on_bad_method() |
145
|
|
|
|
146
|
|
|
|