httpdis 0.6.27__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- httpdis/__init__.py +1 -0
- httpdis/config.py +24 -0
- httpdis/ext/__init__.py +1 -0
- httpdis/ext/httpdis_json.py +73 -0
- httpdis/httpdis.py +1254 -0
- httpdis-0.6.27.dist-info/METADATA +150 -0
- httpdis-0.6.27.dist-info/RECORD +10 -0
- httpdis-0.6.27.dist-info/WHEEL +6 -0
- httpdis-0.6.27.dist-info/licenses/LICENSE +674 -0
- httpdis-0.6.27.dist-info/top_level.txt +1 -0
httpdis/httpdis.py
ADDED
|
@@ -0,0 +1,1254 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright 2008-2019 The Wazo Authors
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
"""httpsdis.httpsdis"""
|
|
5
|
+
|
|
6
|
+
# TODO: a configuration option to send the backtraces
|
|
7
|
+
# or not to the client in error report
|
|
8
|
+
|
|
9
|
+
# TODO: locks and implements SIGHUP (by reloading the configuration)
|
|
10
|
+
|
|
11
|
+
# TODO: add some teardown callbacks?
|
|
12
|
+
# maybe two stages:
|
|
13
|
+
# - cb teardown stage 1
|
|
14
|
+
# - wait for tread completion in this module
|
|
15
|
+
# - cb teardown stage 2
|
|
16
|
+
|
|
17
|
+
# TODO: split backtraces in syslog when they are too long
|
|
18
|
+
|
|
19
|
+
import email.utils
|
|
20
|
+
import binascii
|
|
21
|
+
import threading
|
|
22
|
+
from hmac import compare_digest
|
|
23
|
+
import errno
|
|
24
|
+
import json
|
|
25
|
+
import logging
|
|
26
|
+
import os
|
|
27
|
+
import select
|
|
28
|
+
import signal
|
|
29
|
+
import socket
|
|
30
|
+
import sys
|
|
31
|
+
import time
|
|
32
|
+
import traceback
|
|
33
|
+
#import weakref
|
|
34
|
+
|
|
35
|
+
import cgi
|
|
36
|
+
try:
|
|
37
|
+
from cgi import escape as html_escape
|
|
38
|
+
except ImportError:
|
|
39
|
+
from html import escape as html_escape
|
|
40
|
+
|
|
41
|
+
import re
|
|
42
|
+
try:
|
|
43
|
+
from re import _pattern_type as RePatternType
|
|
44
|
+
except ImportError:
|
|
45
|
+
from re import Pattern as RePatternType
|
|
46
|
+
|
|
47
|
+
from base64 import b64encode, b64decode
|
|
48
|
+
try:
|
|
49
|
+
from crypt import crypt
|
|
50
|
+
except ImportError:
|
|
51
|
+
from legacycrypt import crypt
|
|
52
|
+
from hashlib import sha1
|
|
53
|
+
|
|
54
|
+
from six import BytesIO, binary_type, ensure_binary, ensure_text, iteritems
|
|
55
|
+
from six.moves import http_cookies
|
|
56
|
+
from six.moves.urllib import parse as urlparse, request as urlrequest
|
|
57
|
+
from six.moves.BaseHTTPServer import BaseHTTPRequestHandler
|
|
58
|
+
|
|
59
|
+
import magic
|
|
60
|
+
|
|
61
|
+
from sonicprobe import helpers
|
|
62
|
+
from sonicprobe.libs import urisup
|
|
63
|
+
from sonicprobe.libs.threading_tcp_server import KillableThreadingHTTPServer
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
from rfc6266_parser import build_header, parse_headers
|
|
67
|
+
except ImportError:
|
|
68
|
+
from rfc6266 import build_header, parse_headers
|
|
69
|
+
|
|
70
|
+
from .config import (BUFFER_SIZE, # pylint: disable=unused-import
|
|
71
|
+
DEFAULT_CHARSET,
|
|
72
|
+
get_default_options)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
LOG = logging.getLogger('httpdis') # pylint: disable-msg=C0103
|
|
76
|
+
|
|
77
|
+
_METHODS = ('HEAD',
|
|
78
|
+
'GET',
|
|
79
|
+
'DELETE',
|
|
80
|
+
'PATCH',
|
|
81
|
+
'POST',
|
|
82
|
+
'PUT')
|
|
83
|
+
|
|
84
|
+
_END_EXC_HEADERS = ('Cache-control',
|
|
85
|
+
'Connection',
|
|
86
|
+
'Content-type',
|
|
87
|
+
'Content-length',
|
|
88
|
+
'Pragma',
|
|
89
|
+
'Server')
|
|
90
|
+
|
|
91
|
+
_AUTH = None
|
|
92
|
+
_COMMANDS = {}
|
|
93
|
+
_NCMD = {}
|
|
94
|
+
_RCMD = {}
|
|
95
|
+
_HTTP_SERVER = None
|
|
96
|
+
_KILLED = False
|
|
97
|
+
_OPTIONS = {}
|
|
98
|
+
DEFAULT_OPTIONS = {'auth_basic': None,
|
|
99
|
+
'auth_basic_file': None,
|
|
100
|
+
'testmethods': False,
|
|
101
|
+
'max_body_size': 1 * 1024 * 1024,
|
|
102
|
+
'max_workers': 1,
|
|
103
|
+
'max_requests': 0,
|
|
104
|
+
'max_life_time': 0,
|
|
105
|
+
'listen_addr': None,
|
|
106
|
+
'listen_port': None,
|
|
107
|
+
'server_version': None,
|
|
108
|
+
'sys_version': None}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class Command(object): # pylint: disable=too-few-public-methods,useless-object-inheritance
|
|
112
|
+
"""
|
|
113
|
+
Each registration results in an instance of this class being created.
|
|
114
|
+
"""
|
|
115
|
+
def __init__(self,
|
|
116
|
+
name,
|
|
117
|
+
handler,
|
|
118
|
+
op,
|
|
119
|
+
safe_init,
|
|
120
|
+
at_start,
|
|
121
|
+
at_stop,
|
|
122
|
+
static,
|
|
123
|
+
root,
|
|
124
|
+
replacement,
|
|
125
|
+
charset,
|
|
126
|
+
content_type,
|
|
127
|
+
to_auth,
|
|
128
|
+
to_log):
|
|
129
|
+
self.handler = handler
|
|
130
|
+
self.name = name
|
|
131
|
+
self.op = op
|
|
132
|
+
self.safe_init = safe_init
|
|
133
|
+
self.at_start = at_start
|
|
134
|
+
self.at_stop = at_stop
|
|
135
|
+
self.static = static
|
|
136
|
+
self.root = root
|
|
137
|
+
self.replacement = replacement
|
|
138
|
+
self.charset = charset
|
|
139
|
+
self.content_type = content_type
|
|
140
|
+
self.to_log = to_log
|
|
141
|
+
|
|
142
|
+
if isinstance(to_auth, (list, tuple)):
|
|
143
|
+
self.auth_users = list(filter(helpers.has_len, to_auth))
|
|
144
|
+
self.to_auth = True
|
|
145
|
+
else:
|
|
146
|
+
self.auth_users = []
|
|
147
|
+
self.to_auth = bool(to_auth)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class HttpResponse(urlrequest.Request):
|
|
151
|
+
def __init__(self, code=200, data="", headers=None, message=None, send_body=True):
|
|
152
|
+
if headers is None:
|
|
153
|
+
headers = {}
|
|
154
|
+
|
|
155
|
+
urlrequest.Request.__init__(self, "http://127.0.0.1", data=data, headers=headers)
|
|
156
|
+
self.code = code
|
|
157
|
+
self.send_body = send_body
|
|
158
|
+
self.message = message
|
|
159
|
+
|
|
160
|
+
def get_code(self):
|
|
161
|
+
return self.code
|
|
162
|
+
|
|
163
|
+
def set_code(self, code):
|
|
164
|
+
self.code = code
|
|
165
|
+
return self
|
|
166
|
+
|
|
167
|
+
def set_send_body(self, send_body):
|
|
168
|
+
self.send_body = send_body
|
|
169
|
+
return self
|
|
170
|
+
|
|
171
|
+
def set_message(self, message):
|
|
172
|
+
self.message = message
|
|
173
|
+
return self
|
|
174
|
+
|
|
175
|
+
def get_message(self):
|
|
176
|
+
return self.message
|
|
177
|
+
|
|
178
|
+
def is_send_body(self):
|
|
179
|
+
return self.send_body
|
|
180
|
+
|
|
181
|
+
def add_data(self, data):
|
|
182
|
+
self.data = data # pylint: disable=attribute-defined-outside-init
|
|
183
|
+
return self
|
|
184
|
+
|
|
185
|
+
def add_header(self, key, val):
|
|
186
|
+
urlrequest.Request.add_header(self, key, val)
|
|
187
|
+
return self
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class HttpResponseJson(HttpResponse):
|
|
191
|
+
def __init__(self, code=200, data="", headers=None, message=None, send_body=True):
|
|
192
|
+
if headers is None:
|
|
193
|
+
headers = {}
|
|
194
|
+
|
|
195
|
+
data = json.dumps(data)
|
|
196
|
+
|
|
197
|
+
if not isinstance(headers, dict):
|
|
198
|
+
headers = {}
|
|
199
|
+
|
|
200
|
+
if headers.get('Content-type', '').split(';', 1)[0].strip() != 'application/json':
|
|
201
|
+
headers['Content-type'] = 'application/json'
|
|
202
|
+
|
|
203
|
+
HttpResponse.__init__(self, code, data, headers, message, send_body)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class HttpReqError(Exception):
|
|
207
|
+
"""
|
|
208
|
+
Catched in HttpReqHandler.common_req() which calls .report().
|
|
209
|
+
|
|
210
|
+
Used to implement the unicity of the response to a single request,
|
|
211
|
+
in a consistent way.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
def __init__(self, code, text=None, exc=None, headers=None, ctype=None):
|
|
215
|
+
if headers is None:
|
|
216
|
+
headers = {}
|
|
217
|
+
|
|
218
|
+
self.code = code
|
|
219
|
+
self.text = text
|
|
220
|
+
self.exc = exc
|
|
221
|
+
self.ctype = ctype or 'msg'
|
|
222
|
+
msg = text or BaseHTTPRequestHandler.responses[code][1]
|
|
223
|
+
|
|
224
|
+
if not isinstance(headers, dict):
|
|
225
|
+
headers = {}
|
|
226
|
+
self.headers = headers
|
|
227
|
+
|
|
228
|
+
Exception.__init__(self, msg)
|
|
229
|
+
|
|
230
|
+
def report(self, req_handler):
|
|
231
|
+
"Send a response corresponding to this error to the client"
|
|
232
|
+
if self.exc:
|
|
233
|
+
req_handler.send_exception(self.code, self.exc, self.headers)
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
text = (self.text
|
|
237
|
+
or BaseHTTPRequestHandler.responses[self.code][1]
|
|
238
|
+
or "Unknown error")
|
|
239
|
+
|
|
240
|
+
getattr(req_handler, "send_error_%s" % self.ctype, req_handler.send_error_msg)(self.code, text, self.headers)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class HttpReqErrJson(HttpReqError):
|
|
244
|
+
def __init__(self, code, text=None, exc=None, headers=None):
|
|
245
|
+
HttpReqError.__init__(self, code, text, exc, headers, ctype = 'json')
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
HTTP_RESPONSE_CLASS = HttpResponse
|
|
249
|
+
HTTP_REQERROR_CLASS = HttpReqError
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class HttpAuthentication(object): # pylint: disable=useless-object-inheritance
|
|
253
|
+
def __init__(self, htpasswd, realm=None):
|
|
254
|
+
self.htpasswd = htpasswd
|
|
255
|
+
self.realm = realm
|
|
256
|
+
self.users = {}
|
|
257
|
+
self._credentials = threading.local()
|
|
258
|
+
|
|
259
|
+
def parse_file(self):
|
|
260
|
+
f = None
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
with open(self.htpasswd, 'r') as f:
|
|
264
|
+
for line in f.readlines():
|
|
265
|
+
tmp = line.strip()
|
|
266
|
+
if not tmp or tmp.startswith('#') or tmp.find(':') < 1:
|
|
267
|
+
continue
|
|
268
|
+
user, passwd = tmp.split(':', 1)
|
|
269
|
+
if not passwd:
|
|
270
|
+
continue
|
|
271
|
+
self.users[user] = passwd
|
|
272
|
+
finally:
|
|
273
|
+
if f:
|
|
274
|
+
f.close()
|
|
275
|
+
|
|
276
|
+
return self
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def user(self):
|
|
280
|
+
return getattr(self._credentials, 'user', None)
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def passwd(self):
|
|
284
|
+
return getattr(self._credentials, 'passwd', None)
|
|
285
|
+
|
|
286
|
+
def valid_authorization(self, authorization):
|
|
287
|
+
self._credentials.user = self._credentials.passwd = None
|
|
288
|
+
try:
|
|
289
|
+
kind, data = authorization.split(None, 1)
|
|
290
|
+
if kind.lower() != 'basic':
|
|
291
|
+
return False
|
|
292
|
+
decoded = b64decode(ensure_binary(data.strip()))
|
|
293
|
+
user, separator, passwd = decoded.partition(b':')
|
|
294
|
+
if not separator:
|
|
295
|
+
return False
|
|
296
|
+
user = user.decode('utf-8')
|
|
297
|
+
password = passwd.decode('utf-8')
|
|
298
|
+
except (ValueError, TypeError, UnicodeError, binascii.Error):
|
|
299
|
+
return False
|
|
300
|
+
secret = self.users.get(user)
|
|
301
|
+
if not secret:
|
|
302
|
+
return False
|
|
303
|
+
if secret.startswith('{SHA}'):
|
|
304
|
+
calculated = '{SHA}' + b64encode(sha1(passwd).digest()).decode('ascii')
|
|
305
|
+
else:
|
|
306
|
+
calculated = crypt(password, secret)
|
|
307
|
+
if not calculated or not compare_digest(ensure_binary(secret), ensure_binary(calculated)):
|
|
308
|
+
return False
|
|
309
|
+
self._credentials.user, self._credentials.passwd = user, password
|
|
310
|
+
return True
|
|
311
|
+
|
|
312
|
+
def unauthorized(self, req_error = None):
|
|
313
|
+
if not req_error:
|
|
314
|
+
req_error = HTTP_REQERROR_CLASS
|
|
315
|
+
|
|
316
|
+
headers = {'WWW-Authenticate': 'Basic realm="%s"' % self.realm or ''}
|
|
317
|
+
|
|
318
|
+
return req_error(code = 401, headers = headers)
|
|
319
|
+
|
|
320
|
+
@staticmethod
|
|
321
|
+
def forbidden(req_error = None):
|
|
322
|
+
if not req_error:
|
|
323
|
+
req_error = HTTP_REQERROR_CLASS
|
|
324
|
+
|
|
325
|
+
return req_error(code = 403)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
class HttpReqHandler(BaseHTTPRequestHandler):
|
|
329
|
+
"""
|
|
330
|
+
Handle one HTTP request
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
_DEFAULT_CONTENT_TYPE = 'text/plain'
|
|
334
|
+
_ALLOWED_CONTENT_TYPES = []
|
|
335
|
+
_ALLOWED_MULTIPART_FORM = True
|
|
336
|
+
_CLASS_HTTP_RESP = HTTP_RESPONSE_CLASS
|
|
337
|
+
_CLASS_REQ_ERROR = HTTP_REQERROR_CLASS
|
|
338
|
+
_FUNC_SEND_ERROR = 'send_error_msgtxt'
|
|
339
|
+
|
|
340
|
+
_SERVER = {}
|
|
341
|
+
|
|
342
|
+
_to_log = True
|
|
343
|
+
_cmd = None
|
|
344
|
+
|
|
345
|
+
_path = None
|
|
346
|
+
_payload = None
|
|
347
|
+
_payload_params = None
|
|
348
|
+
_query_params = {}
|
|
349
|
+
_fragment = None
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def build_response(self, code=200, data="", headers=None, message=None, send_body=True):
|
|
353
|
+
return self._CLASS_HTTP_RESP(code, data, headers, message, send_body)
|
|
354
|
+
|
|
355
|
+
def req_error(self, code, text=None, exc=None, headers=None):
|
|
356
|
+
return self._CLASS_REQ_ERROR(code, text, exc, headers)
|
|
357
|
+
|
|
358
|
+
def send_error_msg(self, code, message, headers=None):
|
|
359
|
+
return getattr(self, self._FUNC_SEND_ERROR)(code, message, headers)
|
|
360
|
+
|
|
361
|
+
def log_enabled(self):
|
|
362
|
+
return self._to_log
|
|
363
|
+
|
|
364
|
+
def get_server_vars(self):
|
|
365
|
+
return self._SERVER
|
|
366
|
+
|
|
367
|
+
def set_log(self, enable = True):
|
|
368
|
+
self._to_log = bool(enable)
|
|
369
|
+
return self
|
|
370
|
+
|
|
371
|
+
def fragment(self):
|
|
372
|
+
return self._fragment
|
|
373
|
+
|
|
374
|
+
def get_cmd(self):
|
|
375
|
+
return self._cmd
|
|
376
|
+
|
|
377
|
+
def get_headers(self):
|
|
378
|
+
if hasattr(self.headers, 'dict'):
|
|
379
|
+
return self.headers.dict
|
|
380
|
+
|
|
381
|
+
return dict(self.headers.items())
|
|
382
|
+
|
|
383
|
+
def get_method(self):
|
|
384
|
+
return self.command
|
|
385
|
+
|
|
386
|
+
def get_path(self):
|
|
387
|
+
return self._path
|
|
388
|
+
|
|
389
|
+
def get_payload(self):
|
|
390
|
+
return self._payload
|
|
391
|
+
|
|
392
|
+
def payload_params(self):
|
|
393
|
+
return self._payload_params
|
|
394
|
+
|
|
395
|
+
def query_params(self):
|
|
396
|
+
return self._query_params
|
|
397
|
+
|
|
398
|
+
def version_string(self):
|
|
399
|
+
return BaseHTTPRequestHandler.version_string(self).strip()
|
|
400
|
+
|
|
401
|
+
def permit_ctype(self, ctype):
|
|
402
|
+
if ctype:
|
|
403
|
+
self._ALLOWED_CONTENT_TYPES += [ctype.lower()]
|
|
404
|
+
return self
|
|
405
|
+
|
|
406
|
+
def forbid_ctype(self, ctype):
|
|
407
|
+
if ctype and ctype in self._ALLOWED_CONTENT_TYPES:
|
|
408
|
+
self._ALLOWED_CONTENT_TYPES.remove(ctype.lower())
|
|
409
|
+
return self
|
|
410
|
+
|
|
411
|
+
def permit_multipart(self):
|
|
412
|
+
self._ALLOWED_MULTIPART_FORM = True
|
|
413
|
+
return self
|
|
414
|
+
|
|
415
|
+
def forbid_multipart(self):
|
|
416
|
+
self._ALLOWED_MULTIPART_FORM = False
|
|
417
|
+
return self
|
|
418
|
+
|
|
419
|
+
@staticmethod
|
|
420
|
+
def parse_date(ims):
|
|
421
|
+
""" Parse rfc1123, rfc850 and asctime timestamps and return UTC epoch. """
|
|
422
|
+
try:
|
|
423
|
+
ts = email.utils.parsedate_tz(ims)
|
|
424
|
+
return time.mktime(ts[:8] + (0,)) - (ts[9] or 0) - time.timezone
|
|
425
|
+
except (TypeError, ValueError, IndexError):
|
|
426
|
+
return None
|
|
427
|
+
|
|
428
|
+
def log_error(self, xformat, *args): # pylint: disable=arguments-differ
|
|
429
|
+
"""
|
|
430
|
+
There is more information in log_request(), which is always called
|
|
431
|
+
=> do nothing
|
|
432
|
+
"""
|
|
433
|
+
pass # pylint: disable=unnecessary-pass
|
|
434
|
+
|
|
435
|
+
def log_request(self, code='-', size='-'):
|
|
436
|
+
"""
|
|
437
|
+
Called by send_response()
|
|
438
|
+
TODO: a configuration option to log or not
|
|
439
|
+
(maybe using logging filters?)
|
|
440
|
+
TODO: discriminate by code and dispatch to various log levels
|
|
441
|
+
"""
|
|
442
|
+
LOG.info("%r %s %s", self.requestline, code, size)
|
|
443
|
+
|
|
444
|
+
def send_response(self, code, message=None, size='-'):
|
|
445
|
+
"""
|
|
446
|
+
Send the response header and log the response code.
|
|
447
|
+
|
|
448
|
+
Also send two standard headers with the server software
|
|
449
|
+
version and the current date.
|
|
450
|
+
"""
|
|
451
|
+
# pylint: disable-msg=W0221
|
|
452
|
+
if self._to_log or LOG.isEnabledFor(logging.DEBUG):
|
|
453
|
+
self.log_request(code, size)
|
|
454
|
+
|
|
455
|
+
if message is None:
|
|
456
|
+
if code in self.responses:
|
|
457
|
+
message = self.responses[code][0]
|
|
458
|
+
else:
|
|
459
|
+
message = ''
|
|
460
|
+
|
|
461
|
+
if self.request_version != 'HTTP/0.9':
|
|
462
|
+
self.wfile.write(ensure_binary("%s %d %s\r\n"
|
|
463
|
+
% (self.protocol_version, code, message)))
|
|
464
|
+
self.send_header('Server', self.version_string())
|
|
465
|
+
self.send_header('Date', self.date_time_string())
|
|
466
|
+
|
|
467
|
+
def end_response(self, response):
|
|
468
|
+
if not isinstance(response, HttpResponse):
|
|
469
|
+
raise TypeError("Response must be HttpResponse instance: %r" % response)
|
|
470
|
+
|
|
471
|
+
code = response.get_code()
|
|
472
|
+
|
|
473
|
+
if response.is_send_body() \
|
|
474
|
+
and self.command != 'HEAD' \
|
|
475
|
+
and code >= 200 \
|
|
476
|
+
and code not in (204, 304):
|
|
477
|
+
data = ensure_binary(response.data or "")
|
|
478
|
+
clen = len(data)
|
|
479
|
+
else:
|
|
480
|
+
data = ""
|
|
481
|
+
clen = 0
|
|
482
|
+
|
|
483
|
+
self.send_response(code = code,
|
|
484
|
+
size = clen,
|
|
485
|
+
message = response.get_message())
|
|
486
|
+
|
|
487
|
+
if response.get_header('Content-type'):
|
|
488
|
+
content_type = response.get_header('Content-type')
|
|
489
|
+
elif self._cmd and self._cmd.content_type:
|
|
490
|
+
content_type = self._cmd.content_type
|
|
491
|
+
else:
|
|
492
|
+
content_type = self._DEFAULT_CONTENT_TYPE
|
|
493
|
+
|
|
494
|
+
self.send_header('Cache-Control', response.get_header('Cache-control') or 'no-cache')
|
|
495
|
+
self.send_header('Pragma', response.get_header('Pragma') or 'no-cache')
|
|
496
|
+
self.send_header('Connection', response.get_header('Connection') or 'close')
|
|
497
|
+
self.send_header('Content-Type', content_type)
|
|
498
|
+
self.send_header('Content-Length', str(clen))
|
|
499
|
+
|
|
500
|
+
for header, value in response.header_items():
|
|
501
|
+
if header.lower() not in tuple(h.lower() for h in _END_EXC_HEADERS):
|
|
502
|
+
self.send_header(header, value)
|
|
503
|
+
|
|
504
|
+
self.end_headers()
|
|
505
|
+
|
|
506
|
+
if clen:
|
|
507
|
+
self.wfile.write(ensure_binary(data))
|
|
508
|
+
|
|
509
|
+
def _mk_error_explain_data(self, code, message, explain, charset):
|
|
510
|
+
return ensure_text(self.error_message_format % {'code': code,
|
|
511
|
+
'message': message,
|
|
512
|
+
'explain': explain},
|
|
513
|
+
charset)
|
|
514
|
+
|
|
515
|
+
def send_error_explain(self, code, message=None, headers=None, content_type=None):
|
|
516
|
+
"do not use directly"
|
|
517
|
+
if headers is None:
|
|
518
|
+
headers = {}
|
|
519
|
+
|
|
520
|
+
if code in self.responses:
|
|
521
|
+
if message is None:
|
|
522
|
+
message = self.responses[code][0]
|
|
523
|
+
|
|
524
|
+
explain = self.responses[code][1]
|
|
525
|
+
else:
|
|
526
|
+
explain = ""
|
|
527
|
+
|
|
528
|
+
if message is None:
|
|
529
|
+
message = ""
|
|
530
|
+
|
|
531
|
+
if not isinstance(headers, dict):
|
|
532
|
+
headers = {}
|
|
533
|
+
|
|
534
|
+
if not content_type:
|
|
535
|
+
if self._cmd and self._cmd.content_type:
|
|
536
|
+
content_type = self._cmd.content_type
|
|
537
|
+
else:
|
|
538
|
+
content_type = self._DEFAULT_CONTENT_TYPE
|
|
539
|
+
|
|
540
|
+
if self._cmd and self._cmd.charset:
|
|
541
|
+
charset = self._cmd.charset
|
|
542
|
+
else:
|
|
543
|
+
charset = DEFAULT_CHARSET
|
|
544
|
+
|
|
545
|
+
headers['Content-type'] = "%s; charset=%s" % (content_type, charset)
|
|
546
|
+
|
|
547
|
+
data = self._mk_error_explain_data(code, message, explain, charset)
|
|
548
|
+
|
|
549
|
+
self.end_response(self.build_response(code, data, headers))
|
|
550
|
+
|
|
551
|
+
def send_error_msgtxt(self, code, message, headers=None):
|
|
552
|
+
"text will be in a <pre> bloc"
|
|
553
|
+
if headers is None:
|
|
554
|
+
headers = {}
|
|
555
|
+
|
|
556
|
+
if isinstance(message, (list, tuple)):
|
|
557
|
+
message = ''.join(message)
|
|
558
|
+
elif isinstance(message, dict):
|
|
559
|
+
message = repr(message)
|
|
560
|
+
|
|
561
|
+
self.send_error_explain(code,
|
|
562
|
+
''.join(("<pre>\n", html_escape(message, True), "</pre>\n")), # pylint: disable=deprecated-method
|
|
563
|
+
headers,
|
|
564
|
+
"text/html")
|
|
565
|
+
|
|
566
|
+
def send_exception(self, code, exc_info=None, headers=None):
|
|
567
|
+
"send an error response including a backtrace to the client"
|
|
568
|
+
if headers is None:
|
|
569
|
+
headers = {}
|
|
570
|
+
|
|
571
|
+
if not exc_info:
|
|
572
|
+
exc_info = sys.exc_info()
|
|
573
|
+
|
|
574
|
+
self.send_error_msg(code,
|
|
575
|
+
traceback.format_exception(*exc_info),
|
|
576
|
+
headers)
|
|
577
|
+
|
|
578
|
+
def send_error_json(self, code, message, headers=None):
|
|
579
|
+
"send an error to the client. text message is formatted in a json stream"
|
|
580
|
+
if headers is None:
|
|
581
|
+
headers = {}
|
|
582
|
+
|
|
583
|
+
self.end_response(HttpResponseJson(code,
|
|
584
|
+
{'code': code,
|
|
585
|
+
'message': message},
|
|
586
|
+
headers))
|
|
587
|
+
|
|
588
|
+
def static_file(self, urlpath, response=None):
|
|
589
|
+
root = os.path.realpath(self._cmd.root).rstrip(os.sep) + os.sep
|
|
590
|
+
res = HttpResponse()
|
|
591
|
+
mimetype = None
|
|
592
|
+
disposition = None
|
|
593
|
+
|
|
594
|
+
if isinstance(self._cmd.name, RePatternType) and self._cmd.replacement: # pylint: disable=protected-access
|
|
595
|
+
filename = self._cmd.name.sub(self._cmd.replacement, urlpath)
|
|
596
|
+
else:
|
|
597
|
+
filename = urlpath
|
|
598
|
+
|
|
599
|
+
filename = os.path.realpath(os.path.join(root, filename.strip('/\\')))
|
|
600
|
+
|
|
601
|
+
if not filename.startswith(root):
|
|
602
|
+
raise self.req_error(403, "Access denied.")
|
|
603
|
+
if not os.path.exists(filename) or not os.path.isfile(filename):
|
|
604
|
+
raise self.req_error(404, "File does not exist.")
|
|
605
|
+
if not os.access(filename, os.R_OK):
|
|
606
|
+
raise self.req_error(403, "You do not have permission to access this file.")
|
|
607
|
+
|
|
608
|
+
if self._cmd.content_type:
|
|
609
|
+
mimetype = self._cmd.content_type
|
|
610
|
+
|
|
611
|
+
if isinstance(response, HttpResponse):
|
|
612
|
+
res = response
|
|
613
|
+
if not mimetype:
|
|
614
|
+
mimetype = res.get_header('Content-type')
|
|
615
|
+
disposition = res.get_header('Content-disposition')
|
|
616
|
+
|
|
617
|
+
if not mimetype or mimetype == '__MAGIC__':
|
|
618
|
+
try:
|
|
619
|
+
mime = magic.open(magic.MAGIC_MIME_TYPE)
|
|
620
|
+
mime.load()
|
|
621
|
+
mimetype = mime.file(filename)
|
|
622
|
+
except AttributeError:
|
|
623
|
+
mimetype = magic.from_file(filename, mime = True)
|
|
624
|
+
|
|
625
|
+
if mimetype == 'image/svg':
|
|
626
|
+
mimetype += '+xml'
|
|
627
|
+
if mimetype:
|
|
628
|
+
res.add_header('Content-type', mimetype)
|
|
629
|
+
else:
|
|
630
|
+
mimetype = mimetype.lower()
|
|
631
|
+
if mimetype.startswith('text/') \
|
|
632
|
+
and self._cmd.charset \
|
|
633
|
+
and mimetype.find('charset') == -1:
|
|
634
|
+
res.add_header('Content-type', "%s; charset=%s" % (mimetype, self._cmd.charset))
|
|
635
|
+
else:
|
|
636
|
+
res.add_header('Content-type', mimetype)
|
|
637
|
+
|
|
638
|
+
if disposition:
|
|
639
|
+
attachment = parse_headers(disposition)
|
|
640
|
+
if attachment.disposition == 'attachment' \
|
|
641
|
+
and not attachment.filename_unsafe:
|
|
642
|
+
res.add_header('Content-disposition',
|
|
643
|
+
build_header(os.path.basename(filename)))
|
|
644
|
+
|
|
645
|
+
stats = os.stat(filename)
|
|
646
|
+
res.add_header('Last-Modified',
|
|
647
|
+
time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(stats.st_mtime)))
|
|
648
|
+
|
|
649
|
+
if_modified = self.headers.get('If-Modified-Since')
|
|
650
|
+
if if_modified:
|
|
651
|
+
if_modified = self.parse_date(if_modified.split(';')[0].strip())
|
|
652
|
+
if if_modified is not None and if_modified >= int(stats.st_mtime):
|
|
653
|
+
return res.set_code(304).set_send_body(False)
|
|
654
|
+
|
|
655
|
+
f = None
|
|
656
|
+
body = binary_type()
|
|
657
|
+
|
|
658
|
+
if self.command != 'HEAD':
|
|
659
|
+
with open(filename, 'rb') as f:
|
|
660
|
+
body = f.read()
|
|
661
|
+
if f:
|
|
662
|
+
f.close()
|
|
663
|
+
|
|
664
|
+
return res.set_code(200).add_data(body)
|
|
665
|
+
|
|
666
|
+
@staticmethod
|
|
667
|
+
def querylist_to_dict(query):
|
|
668
|
+
if not isinstance(query, (list, tuple)):
|
|
669
|
+
return None
|
|
670
|
+
|
|
671
|
+
ret = {}
|
|
672
|
+
|
|
673
|
+
for x in query:
|
|
674
|
+
if not x:
|
|
675
|
+
continue
|
|
676
|
+
|
|
677
|
+
if len(x) > 1:
|
|
678
|
+
value = x[1]
|
|
679
|
+
else:
|
|
680
|
+
value = None
|
|
681
|
+
|
|
682
|
+
if not x[0] or x[0].find(']') == -1:
|
|
683
|
+
ret[x[0]] = value
|
|
684
|
+
continue
|
|
685
|
+
|
|
686
|
+
lbracket = x[0].find('[')
|
|
687
|
+
|
|
688
|
+
if lbracket == -1:
|
|
689
|
+
ret[x[0]] = value
|
|
690
|
+
continue
|
|
691
|
+
|
|
692
|
+
key = x[0][:lbracket]
|
|
693
|
+
|
|
694
|
+
if key not in ret:
|
|
695
|
+
ret[key] = {}
|
|
696
|
+
|
|
697
|
+
matched = re.findall(r'\[([^\]]*)\]', x[0][lbracket:])
|
|
698
|
+
nb = len(matched)
|
|
699
|
+
|
|
700
|
+
if nb == 0:
|
|
701
|
+
ret[key] = value
|
|
702
|
+
continue
|
|
703
|
+
|
|
704
|
+
if not isinstance(ret[key], dict):
|
|
705
|
+
ret[key] = {}
|
|
706
|
+
|
|
707
|
+
ref = ret[key]
|
|
708
|
+
j = 0
|
|
709
|
+
|
|
710
|
+
for i, k in enumerate(matched):
|
|
711
|
+
if k == '':
|
|
712
|
+
while j in ref:
|
|
713
|
+
j += 1
|
|
714
|
+
k = j
|
|
715
|
+
|
|
716
|
+
if i == (nb - 1):
|
|
717
|
+
ref[k] = value
|
|
718
|
+
elif k not in ref \
|
|
719
|
+
or (nb > i and not isinstance(ref[k], dict)):
|
|
720
|
+
ref[k] = {}
|
|
721
|
+
|
|
722
|
+
ref = ref[k]
|
|
723
|
+
|
|
724
|
+
if '____ts' in ret:
|
|
725
|
+
del ret['____ts']
|
|
726
|
+
|
|
727
|
+
return ret
|
|
728
|
+
|
|
729
|
+
def _pathify(self):
|
|
730
|
+
"""
|
|
731
|
+
rfc2616 says in 5.1.2: "all HTTP/1.1 servers MUST accept the
|
|
732
|
+
absoluteURI form in requests" so if we get one, transform it
|
|
733
|
+
to abs_path.
|
|
734
|
+
Raises HttpReqError if the request is malformed, else returns
|
|
735
|
+
(path, query, fragment)
|
|
736
|
+
"""
|
|
737
|
+
try:
|
|
738
|
+
path = self.path
|
|
739
|
+
if helpers.has_len(path):
|
|
740
|
+
path = re.sub(r'^/+', '/', path)
|
|
741
|
+
|
|
742
|
+
(path,
|
|
743
|
+
query,
|
|
744
|
+
self._fragment) = urisup.uri_help_split(path)[2:]
|
|
745
|
+
|
|
746
|
+
if not path:
|
|
747
|
+
path = '/'
|
|
748
|
+
|
|
749
|
+
if path[0] != '/':
|
|
750
|
+
raise urisup.InvalidURIError('path %r does not start with "/"' % path)
|
|
751
|
+
|
|
752
|
+
self._path = re.sub(r'/+', '/', path)
|
|
753
|
+
|
|
754
|
+
if query:
|
|
755
|
+
self._query_params = self.querylist_to_dict(query)
|
|
756
|
+
|
|
757
|
+
return self._path
|
|
758
|
+
except urisup.InvalidURIError as e:
|
|
759
|
+
LOG.error("invalid URI: %s", e)
|
|
760
|
+
raise self.req_error(400, str(e))
|
|
761
|
+
|
|
762
|
+
def authenticate(self, auth_users = None):
|
|
763
|
+
for x in ('HTTP_AUTH_USER', 'HTTP_AUTH_PASSWD'):
|
|
764
|
+
self._SERVER.pop(x, None)
|
|
765
|
+
|
|
766
|
+
if not _AUTH:
|
|
767
|
+
raise self.req_error(401, 'Authentication is not configured')
|
|
768
|
+
|
|
769
|
+
auth = self.headers.get('Authorization')
|
|
770
|
+
if not auth:
|
|
771
|
+
raise _AUTH.unauthorized()
|
|
772
|
+
|
|
773
|
+
allowed = _AUTH.valid_authorization(auth)
|
|
774
|
+
|
|
775
|
+
if not allowed or None in (_AUTH.user, _AUTH.passwd):
|
|
776
|
+
raise _AUTH.unauthorized()
|
|
777
|
+
|
|
778
|
+
self._SERVER['HTTP_AUTH_USER'] = _AUTH.user
|
|
779
|
+
self._SERVER['HTTP_AUTH_PASSWD'] = _AUTH.passwd
|
|
780
|
+
|
|
781
|
+
if auth_users and _AUTH.user not in auth_users:
|
|
782
|
+
raise _AUTH.unauthorized()
|
|
783
|
+
|
|
784
|
+
if not allowed:
|
|
785
|
+
raise _AUTH.unauthorized()
|
|
786
|
+
|
|
787
|
+
def set_cookie(self, name, value = '', expires = 0, path = '/', domain = '', secure = False, http_only = False):
|
|
788
|
+
cook = http_cookies.SimpleCookie()
|
|
789
|
+
cook[name] = value
|
|
790
|
+
cook[name]['expires'] = expires
|
|
791
|
+
cook[name]['path'] = path
|
|
792
|
+
cook[name]['domain'] = domain
|
|
793
|
+
cook[name]['secure'] = secure
|
|
794
|
+
cook[name]['httponly'] = http_only
|
|
795
|
+
|
|
796
|
+
self.send_header('Set-Cookie', cook.output(header = ''))
|
|
797
|
+
|
|
798
|
+
def read_cookies(self):
|
|
799
|
+
if 'cookie' in self.headers:
|
|
800
|
+
return http_cookies.SimpleCookie(self.headers['cookie'])
|
|
801
|
+
|
|
802
|
+
return None
|
|
803
|
+
|
|
804
|
+
@staticmethod
|
|
805
|
+
def parse_payload(data, charset):
|
|
806
|
+
return urlparse.parse_qsl(ensure_text(data, encoding = charset))
|
|
807
|
+
|
|
808
|
+
@staticmethod
|
|
809
|
+
def response_dumps(data, charset): # pylint: disable=unused-argument
|
|
810
|
+
if isinstance(data, bool):
|
|
811
|
+
data = int(data)
|
|
812
|
+
|
|
813
|
+
if data is None:
|
|
814
|
+
return ""
|
|
815
|
+
|
|
816
|
+
if helpers.is_scalar(data):
|
|
817
|
+
return "%s" % data
|
|
818
|
+
|
|
819
|
+
if hasattr(data, '__str__') \
|
|
820
|
+
and type(data).__str__ is not object.__str__:
|
|
821
|
+
return "%s" % data
|
|
822
|
+
|
|
823
|
+
return repr(data)
|
|
824
|
+
|
|
825
|
+
def data_from_query(self, cmd):
|
|
826
|
+
"""
|
|
827
|
+
Callback for .execute_command() for DELETE/GET/HEAD requests
|
|
828
|
+
"""
|
|
829
|
+
res = None
|
|
830
|
+
ckey = "%s /%s" % (self.command, cmd)
|
|
831
|
+
|
|
832
|
+
if not isinstance(self._query_params, dict):
|
|
833
|
+
self._query_params = {}
|
|
834
|
+
|
|
835
|
+
if ckey in _NCMD:
|
|
836
|
+
self._cmd = _NCMD[ckey]
|
|
837
|
+
else:
|
|
838
|
+
for key in sorted(_RCMD, key=len, reverse=True):
|
|
839
|
+
if not key.startswith("%s " % self.command):
|
|
840
|
+
continue
|
|
841
|
+
|
|
842
|
+
m = _RCMD[key].name.match(cmd)
|
|
843
|
+
if m:
|
|
844
|
+
self._cmd = _RCMD[key]
|
|
845
|
+
self._query_params.update(m.groupdict())
|
|
846
|
+
break
|
|
847
|
+
|
|
848
|
+
try:
|
|
849
|
+
if not self._cmd:
|
|
850
|
+
raise self.req_error(404)
|
|
851
|
+
|
|
852
|
+
charset = self._cmd.charset or DEFAULT_CHARSET
|
|
853
|
+
|
|
854
|
+
if not self._cmd.to_log:
|
|
855
|
+
self._to_log = False
|
|
856
|
+
|
|
857
|
+
if self._cmd.to_auth:
|
|
858
|
+
self.authenticate(self._cmd.auth_users)
|
|
859
|
+
|
|
860
|
+
if self._cmd.static:
|
|
861
|
+
if self._cmd.handler:
|
|
862
|
+
res = self._cmd.handler(self)
|
|
863
|
+
|
|
864
|
+
return self.static_file(cmd, res)
|
|
865
|
+
|
|
866
|
+
res = self._cmd.handler(self)
|
|
867
|
+
|
|
868
|
+
if not isinstance(res, HttpResponse):
|
|
869
|
+
return self.response_dumps(res, charset)
|
|
870
|
+
|
|
871
|
+
return res
|
|
872
|
+
finally:
|
|
873
|
+
self._query_params = {}
|
|
874
|
+
|
|
875
|
+
def data_from_payload(self, cmd):
|
|
876
|
+
"""
|
|
877
|
+
Callback for .execute_command() for PATCH/POST/PUT requests
|
|
878
|
+
"""
|
|
879
|
+
multipart = False
|
|
880
|
+
ckey = "%s /%s" % (self.command, cmd)
|
|
881
|
+
|
|
882
|
+
if not isinstance(self._query_params, dict):
|
|
883
|
+
self._query_params = {}
|
|
884
|
+
|
|
885
|
+
if ckey in _NCMD:
|
|
886
|
+
self._cmd = _NCMD[ckey]
|
|
887
|
+
else:
|
|
888
|
+
for key in sorted(_RCMD, key=len, reverse=True):
|
|
889
|
+
if not key.startswith("%s " % self.command):
|
|
890
|
+
continue
|
|
891
|
+
|
|
892
|
+
m = _RCMD[key].name.match(cmd)
|
|
893
|
+
if m:
|
|
894
|
+
self._cmd = _RCMD[key]
|
|
895
|
+
self._query_params.update(m.groupdict())
|
|
896
|
+
break
|
|
897
|
+
|
|
898
|
+
try:
|
|
899
|
+
if not self._cmd:
|
|
900
|
+
raise self.req_error(404)
|
|
901
|
+
|
|
902
|
+
charset = self._cmd.charset or DEFAULT_CHARSET
|
|
903
|
+
|
|
904
|
+
tenc = self.headers.get('Transfer-Encoding')
|
|
905
|
+
if tenc and tenc.lower() != 'identity':
|
|
906
|
+
raise self.req_error(501, "Not supported; Transfer-Encoding: %s" % tenc)
|
|
907
|
+
|
|
908
|
+
ctype = self.headers.get('Content-Type')
|
|
909
|
+
if ctype:
|
|
910
|
+
ctype = ctype.lower().split(';', 1)[0]
|
|
911
|
+
if ctype == 'multipart/form-data':
|
|
912
|
+
if not self._ALLOWED_MULTIPART_FORM:
|
|
913
|
+
raise self.req_error(501, "Not supported; Content-Type: %s" % ctype)
|
|
914
|
+
multipart = True
|
|
915
|
+
elif self._ALLOWED_CONTENT_TYPES:
|
|
916
|
+
ct_found = False
|
|
917
|
+
for x in self._ALLOWED_CONTENT_TYPES:
|
|
918
|
+
if ctype == x:
|
|
919
|
+
ct_found = True
|
|
920
|
+
break
|
|
921
|
+
if not ct_found:
|
|
922
|
+
raise self.req_error(501, "Not supported; Content-Type: %s" % ctype)
|
|
923
|
+
|
|
924
|
+
try:
|
|
925
|
+
clen = int(self.headers.get('Content-Length') or 0)
|
|
926
|
+
except (ValueError, TypeError):
|
|
927
|
+
raise self.req_error(411)
|
|
928
|
+
|
|
929
|
+
if clen < 0:
|
|
930
|
+
raise self.req_error(411)
|
|
931
|
+
|
|
932
|
+
if clen > int(_OPTIONS['max_body_size']):
|
|
933
|
+
raise self.req_error(413)
|
|
934
|
+
|
|
935
|
+
if self._cmd.to_auth:
|
|
936
|
+
self.authenticate(self._cmd.auth_users)
|
|
937
|
+
|
|
938
|
+
if clen > 0:
|
|
939
|
+
payload = self.rfile.read(clen)
|
|
940
|
+
if len(payload) != clen:
|
|
941
|
+
raise self.req_error(400, 'Incomplete request body')
|
|
942
|
+
self._payload = BytesIO(payload)
|
|
943
|
+
|
|
944
|
+
if multipart:
|
|
945
|
+
try:
|
|
946
|
+
self._payload_params = cgi.FieldStorage(environ = {'REQUEST_METHOD': 'POST'},
|
|
947
|
+
fp = self._payload,
|
|
948
|
+
headers = self.headers)
|
|
949
|
+
except Exception as e:
|
|
950
|
+
raise self.req_error(415, text=str(e))
|
|
951
|
+
else:
|
|
952
|
+
try:
|
|
953
|
+
if ctype == 'application/x-www-form-urlencoded':
|
|
954
|
+
self._payload_params = urlparse.parse_qsl(ensure_text(payload,
|
|
955
|
+
encoding = charset))
|
|
956
|
+
else:
|
|
957
|
+
self._payload_params = self.parse_payload(payload, charset)
|
|
958
|
+
except ValueError as e:
|
|
959
|
+
raise self.req_error(415, text=str(e))
|
|
960
|
+
|
|
961
|
+
res = self._cmd.handler(self)
|
|
962
|
+
|
|
963
|
+
if not isinstance(res, HttpResponse):
|
|
964
|
+
return self.response_dumps(res, charset)
|
|
965
|
+
|
|
966
|
+
return res
|
|
967
|
+
finally:
|
|
968
|
+
payload = None
|
|
969
|
+
self._payload = None
|
|
970
|
+
self._payload_params = None
|
|
971
|
+
self._query_params = {}
|
|
972
|
+
|
|
973
|
+
def common_req(self, execute, send_body=True):
|
|
974
|
+
"Common code for GET and POST requests"
|
|
975
|
+
self._SERVER = {'CLIENT_ADDR_HOST': self.client_address[0],
|
|
976
|
+
'CLIENT_ADDR_PORT': self.client_address[1]}
|
|
977
|
+
|
|
978
|
+
self._to_log = True
|
|
979
|
+
self._cmd = None
|
|
980
|
+
|
|
981
|
+
self._payload = None
|
|
982
|
+
self._path = None
|
|
983
|
+
self._payload_params = None
|
|
984
|
+
self._query_params = {}
|
|
985
|
+
self._fragment = None
|
|
986
|
+
|
|
987
|
+
(cmd, res, req) = (None, None, None)
|
|
988
|
+
|
|
989
|
+
try:
|
|
990
|
+
try:
|
|
991
|
+
path = self._pathify() # pylint: disable-msg=W0612
|
|
992
|
+
cmd = path[1:]
|
|
993
|
+
res = execute(cmd)
|
|
994
|
+
except HttpReqError as e:
|
|
995
|
+
e.report(self)
|
|
996
|
+
except Exception:
|
|
997
|
+
try:
|
|
998
|
+
self.send_error_msg(500, 'Internal server error')
|
|
999
|
+
except Exception: # pylint: disable-msg=W0703
|
|
1000
|
+
pass
|
|
1001
|
+
raise
|
|
1002
|
+
else:
|
|
1003
|
+
if not isinstance(res, HttpResponse):
|
|
1004
|
+
req = self.build_response()
|
|
1005
|
+
if send_body:
|
|
1006
|
+
req.add_data(res)
|
|
1007
|
+
req.set_send_body(send_body)
|
|
1008
|
+
else:
|
|
1009
|
+
req = res
|
|
1010
|
+
|
|
1011
|
+
self.end_response(req)
|
|
1012
|
+
except socket.error as e:
|
|
1013
|
+
if e.errno in (errno.ECONNRESET, errno.EPIPE):
|
|
1014
|
+
return
|
|
1015
|
+
LOG.exception("exception - cmd=%r - method=%r", cmd, self.command)
|
|
1016
|
+
except Exception: # pylint: disable-msg=W0703
|
|
1017
|
+
LOG.exception("exception - cmd=%r - method=%r", cmd, self.command)
|
|
1018
|
+
finally:
|
|
1019
|
+
del req, res
|
|
1020
|
+
|
|
1021
|
+
def do_DELETE(self):
|
|
1022
|
+
"DELETE method"
|
|
1023
|
+
self.common_req(self.data_from_query)
|
|
1024
|
+
|
|
1025
|
+
def do_GET(self):
|
|
1026
|
+
"GET method"
|
|
1027
|
+
self.common_req(self.data_from_query)
|
|
1028
|
+
|
|
1029
|
+
def do_HEAD(self):
|
|
1030
|
+
"HEAD method"
|
|
1031
|
+
self.common_req(self.data_from_query, send_body=False)
|
|
1032
|
+
|
|
1033
|
+
def do_OPTIONS(self):
|
|
1034
|
+
"OPTIONS method"
|
|
1035
|
+
req = self.build_response(code = 204)
|
|
1036
|
+
req.add_header('Access-Control-Allow-Origin', "*")
|
|
1037
|
+
req.add_header('Access-Control-Allow-Methods', "OPTIONS, POST")
|
|
1038
|
+
req.add_header('Access-Control-Allow-Headers', "Origin, X-Requested-With, Content-Type, Accept, Authorization")
|
|
1039
|
+
req.add_header('Access-Control-Max-Age', 1728000)
|
|
1040
|
+
self.end_response(req)
|
|
1041
|
+
|
|
1042
|
+
def do_PATCH(self):
|
|
1043
|
+
"POST method"
|
|
1044
|
+
self.common_req(self.data_from_payload)
|
|
1045
|
+
|
|
1046
|
+
def do_POST(self):
|
|
1047
|
+
"POST method"
|
|
1048
|
+
self.common_req(self.data_from_payload)
|
|
1049
|
+
|
|
1050
|
+
def do_PUT(self):
|
|
1051
|
+
"PUT method"
|
|
1052
|
+
self.common_req(self.data_from_payload)
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
def register(handler,
|
|
1056
|
+
op,
|
|
1057
|
+
safe_init = None,
|
|
1058
|
+
at_start = None,
|
|
1059
|
+
name = None,
|
|
1060
|
+
at_stop = None,
|
|
1061
|
+
static = False,
|
|
1062
|
+
root = None,
|
|
1063
|
+
replacement = None,
|
|
1064
|
+
charset = DEFAULT_CHARSET,
|
|
1065
|
+
content_type = None,
|
|
1066
|
+
to_auth = False,
|
|
1067
|
+
to_log = True):
|
|
1068
|
+
"""
|
|
1069
|
+
Register a command
|
|
1070
|
+
@handler: function to execute when the command is received
|
|
1071
|
+
@op: http method(s)
|
|
1072
|
+
@safe_init: called by the safe_init() function of this module
|
|
1073
|
+
@at_start: called once just before the server starts
|
|
1074
|
+
@at_stop: called once just before the server stops
|
|
1075
|
+
@name: name of the command (if not name, handler.__name__ is used)
|
|
1076
|
+
@static: render static file
|
|
1077
|
+
@root: root path
|
|
1078
|
+
@replacement: rewrite path when name is regexp
|
|
1079
|
+
@charset: charset
|
|
1080
|
+
@content_type: content_type
|
|
1081
|
+
@to_auth: use basic authentification if True
|
|
1082
|
+
@to_log: log request if True
|
|
1083
|
+
|
|
1084
|
+
prototypes:
|
|
1085
|
+
handler(args)
|
|
1086
|
+
safe_init(options)
|
|
1087
|
+
at_start(options)
|
|
1088
|
+
at_stop()
|
|
1089
|
+
"""
|
|
1090
|
+
ref_cmd = _NCMD
|
|
1091
|
+
is_reg = False
|
|
1092
|
+
|
|
1093
|
+
if isinstance(name, RePatternType): # pylint: disable=protected-access
|
|
1094
|
+
key = name.pattern
|
|
1095
|
+
ref_cmd = _RCMD
|
|
1096
|
+
is_reg = True
|
|
1097
|
+
elif name:
|
|
1098
|
+
key = name
|
|
1099
|
+
replacement = None
|
|
1100
|
+
else:
|
|
1101
|
+
key = handler.__name__
|
|
1102
|
+
name = handler.__name__
|
|
1103
|
+
replacement = None
|
|
1104
|
+
|
|
1105
|
+
methods = []
|
|
1106
|
+
|
|
1107
|
+
if not isinstance(op, (list, tuple)):
|
|
1108
|
+
op = [op.upper()]
|
|
1109
|
+
|
|
1110
|
+
for x in op:
|
|
1111
|
+
x = x.upper()
|
|
1112
|
+
if x not in _METHODS:
|
|
1113
|
+
raise ValueError("unknown HTTP method: %r" % x)
|
|
1114
|
+
|
|
1115
|
+
if static and x not in ('GET', 'HEAD'):
|
|
1116
|
+
raise ValueError("Static must be GET, HEAD command")
|
|
1117
|
+
|
|
1118
|
+
methods.append(x)
|
|
1119
|
+
|
|
1120
|
+
if not methods:
|
|
1121
|
+
raise ValueError("Missing HTTP method")
|
|
1122
|
+
|
|
1123
|
+
if static and not root:
|
|
1124
|
+
raise ValueError("Missing root argument for static")
|
|
1125
|
+
|
|
1126
|
+
cmd = Command(name,
|
|
1127
|
+
handler,
|
|
1128
|
+
methods,
|
|
1129
|
+
safe_init,
|
|
1130
|
+
at_start,
|
|
1131
|
+
at_stop,
|
|
1132
|
+
static,
|
|
1133
|
+
root,
|
|
1134
|
+
replacement,
|
|
1135
|
+
charset,
|
|
1136
|
+
content_type,
|
|
1137
|
+
to_auth,
|
|
1138
|
+
to_log)
|
|
1139
|
+
|
|
1140
|
+
for method in methods:
|
|
1141
|
+
if not is_reg:
|
|
1142
|
+
mkey = "%s /%s" % (method, key)
|
|
1143
|
+
else:
|
|
1144
|
+
mkey = "%s %s" % (method, key)
|
|
1145
|
+
|
|
1146
|
+
if mkey in _COMMANDS:
|
|
1147
|
+
raise ValueError("%s is already registred" % name)
|
|
1148
|
+
_COMMANDS[mkey] = cmd
|
|
1149
|
+
ref_cmd[mkey] = _COMMANDS[mkey]
|
|
1150
|
+
|
|
1151
|
+
def sigterm_handler(signum, stack_frame):
|
|
1152
|
+
"""
|
|
1153
|
+
Just tell the server to exit.
|
|
1154
|
+
|
|
1155
|
+
WARNING: There are race conditions, for example with TimeoutSocket.accept.
|
|
1156
|
+
We don't care: the user can just rekill the process after like 1 sec. if
|
|
1157
|
+
the first kill did not work.
|
|
1158
|
+
"""
|
|
1159
|
+
# pylint: disable-msg=W0613
|
|
1160
|
+
global _KILLED
|
|
1161
|
+
|
|
1162
|
+
for name, cmd in iteritems(_COMMANDS):
|
|
1163
|
+
if cmd.at_stop:
|
|
1164
|
+
LOG.info("at_stop: %r", name)
|
|
1165
|
+
cmd.at_stop()
|
|
1166
|
+
|
|
1167
|
+
_KILLED = True
|
|
1168
|
+
|
|
1169
|
+
if _HTTP_SERVER:
|
|
1170
|
+
_HTTP_SERVER.kill()
|
|
1171
|
+
_HTTP_SERVER.server_close()
|
|
1172
|
+
|
|
1173
|
+
def stop():
|
|
1174
|
+
sigterm_handler(None, None)
|
|
1175
|
+
|
|
1176
|
+
def run(options, http_req_handler = HttpReqHandler, http_server_class = KillableThreadingHTTPServer):
|
|
1177
|
+
"""
|
|
1178
|
+
Start and execute the server
|
|
1179
|
+
"""
|
|
1180
|
+
# pylint: disable-msg=W0613
|
|
1181
|
+
global _HTTP_SERVER
|
|
1182
|
+
|
|
1183
|
+
for x in ('server_version', 'sys_version'):
|
|
1184
|
+
if _OPTIONS.get(x) is not None:
|
|
1185
|
+
setattr(http_req_handler, x, _OPTIONS[x])
|
|
1186
|
+
|
|
1187
|
+
_HTTP_SERVER = http_server_class(
|
|
1188
|
+
_OPTIONS,
|
|
1189
|
+
(_OPTIONS['listen_addr'], _OPTIONS['listen_port']),
|
|
1190
|
+
http_req_handler,
|
|
1191
|
+
name = "httpdis")
|
|
1192
|
+
|
|
1193
|
+
for name, cmd in iteritems(_COMMANDS):
|
|
1194
|
+
if cmd.at_start:
|
|
1195
|
+
LOG.info("at_start: %r", name)
|
|
1196
|
+
cmd.at_start(options)
|
|
1197
|
+
|
|
1198
|
+
LOG.info("will now serve")
|
|
1199
|
+
while not _KILLED:
|
|
1200
|
+
try:
|
|
1201
|
+
_HTTP_SERVER.serve_until_killed()
|
|
1202
|
+
except (socket.error, select.error) as why:
|
|
1203
|
+
if errno.EINTR == why.errno:
|
|
1204
|
+
LOG.debug("interrupted system call")
|
|
1205
|
+
elif errno.EBADF == why.errno and _KILLED:
|
|
1206
|
+
LOG.debug("server close")
|
|
1207
|
+
else:
|
|
1208
|
+
raise
|
|
1209
|
+
|
|
1210
|
+
LOG.info("exiting")
|
|
1211
|
+
|
|
1212
|
+
def init(options, use_sigterm_handler=True):
|
|
1213
|
+
"""
|
|
1214
|
+
Must be called just after registration, before anything else
|
|
1215
|
+
"""
|
|
1216
|
+
# pylint: disable-msg=W0613
|
|
1217
|
+
global _AUTH, _OPTIONS, _KILLED
|
|
1218
|
+
|
|
1219
|
+
_AUTH = None
|
|
1220
|
+
_KILLED = False
|
|
1221
|
+
|
|
1222
|
+
if isinstance(options, dict):
|
|
1223
|
+
_OPTIONS = DEFAULT_OPTIONS.copy()
|
|
1224
|
+
_OPTIONS.update(options)
|
|
1225
|
+
else:
|
|
1226
|
+
for optname, optvalue in iteritems(DEFAULT_OPTIONS):
|
|
1227
|
+
if hasattr(options, optname):
|
|
1228
|
+
_OPTIONS[optname] = getattr(options, optname)
|
|
1229
|
+
else:
|
|
1230
|
+
_OPTIONS[optname] = optvalue
|
|
1231
|
+
|
|
1232
|
+
if _OPTIONS['testmethods']:
|
|
1233
|
+
def fortytwo(request):
|
|
1234
|
+
"test GET method"
|
|
1235
|
+
return 42
|
|
1236
|
+
def ping(request):
|
|
1237
|
+
"test POST method"
|
|
1238
|
+
return request.payload_params()
|
|
1239
|
+
register(fortytwo, 'GET')
|
|
1240
|
+
register(ping, 'POST')
|
|
1241
|
+
|
|
1242
|
+
if _OPTIONS['auth_basic_file']:
|
|
1243
|
+
_AUTH = HttpAuthentication(_OPTIONS['auth_basic_file'],
|
|
1244
|
+
realm = _OPTIONS['auth_basic']).parse_file()
|
|
1245
|
+
|
|
1246
|
+
for name, cmd in iteritems(_COMMANDS):
|
|
1247
|
+
if cmd.safe_init:
|
|
1248
|
+
LOG.info("safe_init: %r", name)
|
|
1249
|
+
cmd.safe_init(_OPTIONS)
|
|
1250
|
+
|
|
1251
|
+
if use_sigterm_handler:
|
|
1252
|
+
# signal.signal(signal.SIGHUP, lambda *x: None) # XXX
|
|
1253
|
+
signal.signal(signal.SIGTERM, sigterm_handler)
|
|
1254
|
+
signal.signal(signal.SIGINT, sigterm_handler)
|