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/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)