1""" 2httping.py http async io (nonblocking) support 3 4 5""" 6from __future__ import absolute_import, division, print_function 7 8 9import sys 10import os 11from collections import deque 12import codecs 13import json 14 15from urllib.parse import urlsplit, quote, quote_plus, unquote, unquote_plus 16 17 18try: 19 import simplejson as json 20except ImportError: 21 import json 22 23# Import ioflo libs 24from ...aid.sixing import * 25from ...aid.odicting import odict, lodict, modict 26from ...aid import aiding 27from ...aid.consoling import getConsole 28 29console = getConsole() 30 31CRLF = b"\r\n" 32LF = b"\n" 33CR = b"\r" 34MAX_LINE_SIZE = 65536 35MAX_HEADERS = 100 36 37HTTP_PORT = 80 38HTTPS_PORT = 443 39HTTP_11_VERSION_STRING = u'HTTP/1.1' # http v1.1 version string 40 41# status codes 42# informational 43CONTINUE = 100 44 45# These constants are here for potential backwards compatibility 46# currently most are unused 47# status codes 48# informational 49 50SWITCHING_PROTOCOLS = 101 51PROCESSING = 102 52 53# successful 54OK = 200 55CREATED = 201 56ACCEPTED = 202 57NON_AUTHORITATIVE_INFORMATION = 203 58NO_CONTENT = 204 59RESET_CONTENT = 205 60PARTIAL_CONTENT = 206 61MULTI_STATUS = 207 62IM_USED = 226 63 64# redirection 65MULTIPLE_CHOICES = 300 66MOVED_PERMANENTLY = 301 67FOUND = 302 68SEE_OTHER = 303 69NOT_MODIFIED = 304 70USE_PROXY = 305 71TEMPORARY_REDIRECT = 307 72 73# client error 74BAD_REQUEST = 400 75UNAUTHORIZED = 401 76PAYMENT_REQUIRED = 402 77FORBIDDEN = 403 78NOT_FOUND = 404 79METHOD_NOT_ALLOWED = 405 80NOT_ACCEPTABLE = 406 81PROXY_AUTHENTICATION_REQUIRED = 407 82REQUEST_TIMEOUT = 408 83CONFLICT = 409 84GONE = 410 85LENGTH_REQUIRED = 411 86PRECONDITION_FAILED = 412 87REQUEST_ENTITY_TOO_LARGE = 413 88REQUEST_URI_TOO_LONG = 414 89UNSUPPORTED_MEDIA_TYPE = 415 90REQUESTED_RANGE_NOT_SATISFIABLE = 416 91EXPECTATION_FAILED = 417 92UNPROCESSABLE_ENTITY = 422 93LOCKED = 423 94FAILED_DEPENDENCY = 424 95UPGRADE_REQUIRED = 426 96PRECONDITION_REQUIRED = 428 97TOO_MANY_REQUESTS = 429 98REQUEST_HEADER_FIELDS_TOO_LARGE = 431 99 100# server error 101INTERNAL_SERVER_ERROR = 500 102NOT_IMPLEMENTED = 501 103BAD_GATEWAY = 502 104SERVICE_UNAVAILABLE = 503 105GATEWAY_TIMEOUT = 504 106HTTP_VERSION_NOT_SUPPORTED = 505 107INSUFFICIENT_STORAGE = 507 108NOT_EXTENDED = 510 109NETWORK_AUTHENTICATION_REQUIRED = 511 110 111# Mapping status codes to official W3C names 112STATUS_DESCRIPTIONS = { 113 100: 'Continue', 114 101: 'Switching Protocols', 115 116 200: 'OK', 117 201: 'Created', 118 202: 'Accepted', 119 203: 'Non-Authoritative Information', 120 204: 'No Content', 121 205: 'Reset Content', 122 206: 'Partial Content', 123 124 300: 'Multiple Choices', 125 301: 'Moved Permanently', 126 302: 'Found', 127 303: 'See Other', 128 304: 'Not Modified', 129 305: 'Use Proxy', 130 306: '(Unused)', 131 307: 'Temporary Redirect', 132 133 400: 'Bad Request', 134 401: 'Unauthorized', 135 402: 'Payment Required', 136 403: 'Forbidden', 137 404: 'Not Found', 138 405: 'Method Not Allowed', 139 406: 'Not Acceptable', 140 407: 'Proxy Authentication Required', 141 408: 'Request Timeout', 142 409: 'Conflict', 143 410: 'Gone', 144 411: 'Length Required', 145 412: 'Precondition Failed', 146 413: 'Request Entity Too Large', 147 414: 'Request-URI Too Long', 148 415: 'Unsupported Media Type', 149 416: 'Requested Range Not Satisfiable', 150 417: 'Expectation Failed', 151 428: 'Precondition Required', 152 429: 'Too Many Requests', 153 431: 'Request Header Fields Too Large', 154 155 500: 'Internal Server Error', 156 501: 'Not Implemented', 157 502: 'Bad Gateway', 158 503: 'Service Unavailable', 159 504: 'Gateway Timeout', 160 505: 'HTTP Version Not Supported', 161 511: 'Network Authentication Required', 162} 163 164METHODS = (u'GET', u'HEAD', u'PUT', u'PATCH', u'POST', u'DELETE', 165 u'OPTIONS', u'TRACE', u'CONNECT' ) 166 167# maximal amount of data to read at one time in _safe_read 168MAXAMOUNT = 1048576 169 170# maximal line length when calling readline(). 171_MAXLINE = 65536 172_MAXHEADERS = 100 173 174class HTTPException(Exception): 175 # Subclasses that define an __init__ must call Exception.__init__ 176 # or define self.args. Otherwise, str() will fail. 177 pass 178 179class InvalidURL(HTTPException): 180 pass 181 182 183class UnknownProtocol(HTTPException): 184 def __init__(self, version): 185 self.args = version, 186 self.version = version 187 188class BadStatusLine(HTTPException): 189 def __init__(self, line): 190 if not line: 191 line = repr(line) 192 self.args = line, 193 self.line = line 194 195class BadRequestLine(BadStatusLine): 196 pass 197 198class BadMethod(HTTPException): 199 def __init__(self, method): 200 self.args = method, 201 self.method = method 202 203class LineTooLong(HTTPException): 204 def __init__(self, kind): 205 HTTPException.__init__(self, "got more than %d bytes while parsing %s" 206 % (MAX_LINE_SIZE, kind)) 207 208class PrematureClosure(HTTPException): 209 def __init__(self, msg): 210 self.args = msg, 211 self.msg = msg 212 213 214class HTTPError(Exception): 215 """ 216 HTTP error for use with Valet or Other WSGI servers to raise exceptions 217 caught by the WSGI server. 218 219 220 Attributes: 221 status is int HTTP status code, e.g. 400 222 reason is str HTTP status text, "Unknown Error" 223 title is str title of error 224 225 headers is dict of extra headers to add to the response 226 error (int): An internal application error code 227 """ 228 229 __slots__ = ('status', 'reason', 'title', 'detail', 'headers', 'fault') 230 231 def __init__(self, 232 status, 233 reason="", 234 title="", 235 detail="", 236 fault=None, 237 headers=None): 238 """ 239 Parameters: 240 status is int HTTP status response code 241 reason is str HTTP reason phase for status code 242 title is str title of error 243 detail is str detailed description of error 244 fault is int internal application fault code for tracking 245 headers is dict of extra headers to add to the response 246 """ 247 self.status = int(status) 248 self.reason = (str(reason) if reason else 249 STATUS_DESCRIPTIONS.get(self.status, "Unknown")) 250 251 self.title = title 252 self.detail = detail 253 self.fault = fault if fault is None else int(fault) 254 self.headers = odict(headers) if headers else odict() 255 256 def __repr__(self): 257 return '<%s: %s>' % (self.__class__.__name__, self.status) 258 259 def render(self, jsonify=False): 260 """ 261 Render and return the attributes as a bytes 262 If jsonify then render as serialized json 263 """ 264 if jsonify: 265 data = odict() 266 data["status"] = self.status 267 data["reason"] = self.reason 268 data["title"] = self.title 269 data["detail"] = self.detail 270 data["fault"] = self.fault 271 body = json.dumps(data, indent=2) 272 273 else: 274 body = "{} {}\n{}\n{}\n{}".format(self.status, 275 self.reason, 276 self.title, 277 self.detail, 278 self.fault if self.fault is not None else "") 279 return body.encode('iso-8859-1') 280 281 282# Utility functions 283 284def httpDate1123(dt): 285 """Return a string representation of a date according to RFC 1123 286 (HTTP/1.1). 287 288 The supplied date must be in UTC. 289 import datetime 290 httpDate1123(datetime.datetime.utcnow()) 291 'Wed, 30 Sep 2015 14:29:18 GMT' 292 """ 293 weekday = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][dt.weekday()] 294 month = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", 295 "Oct", "Nov", "Dec"][dt.month - 1] 296 return "%s, %02d %s %04d %02d:%02d:%02d GMT" % (weekday, dt.day, month, 297 dt.year, dt.hour, dt.minute, dt.second) 298 299def normalizeHostPort(host, port=None, defaultPort=80): 300 """ 301 Given hostname host which could also be netloc which includes port 302 and or port 303 generate and return tuple (hostname, port) 304 priority is if port is provided in hostname as host:port then use 305 otherwise use port otherwise use defaultPort 306 """ 307 if port is None: 308 port = defaultPort 309 310 # rfind returns -1 if not found 311 # ipv6 312 i = host.rfind(u':') # is port included in hostname 313 j = host.rfind(u']') # ipv6 addresses have [...] 314 if i > j: # means ':' is found 315 if host[i+1:]: # non empty after ':' since 'hostname:' == 'hostname' 316 port = host[i+1:] 317 host = host[:i] 318 319 if host and host[0] == u'[' and host[-1] == u']': # strip of ipv6 brackets 320 host = host[1:-1] 321 322 try: 323 port = int(port) 324 except ValueError: 325 raise InvalidURL("Nonnumeric port: '{0}'".format(port)) 326 327 return (host, port) 328 329def parseQuery(query): 330 """ 331 Return odict of parsed query string. 332 Utility function 333 """ 334 qargs = odict() 335 if u';' in query: # as of 2014 W3C semicolon obsolete 336 querySplits = query.split(u';') 337 elif u'&' in query: 338 querySplits = query.split(u'&') 339 else: 340 querySplits = [query] 341 for queryPart in querySplits: # this prevents duplicates even if desired 342 if queryPart: 343 if '=' in queryPart: 344 key, val = queryPart.split('=', 1) 345 val = unquote(val) 346 else: 347 key = queryPart 348 val = u'true' 349 qargs[key] = val 350 return qargs 351 352def updateQargsQuery(qargs=None, query=u'',): 353 """ 354 Returns duple of updated (qargs, query) 355 Where qargs parameter is odict of query arguments and query parameter is query string 356 The returned qargs is updated with query string arguments 357 and the returned query string is generated from the updated qargs 358 If provided, qargs may have additional fields not in query string 359 This allows combining query args from two sources, a dict and a string 360 361 https://www.w3.org/TR/2014/REC-html5-20141028/forms.html#url-encoded-form-data 362 """ 363 if qargs == None: 364 qargs = odict() 365 366 if query: 367 if u';' in query: # as of 2014 W3C semicolon obsolete 368 querySplits = query.split(u';') 369 elif u'&' in query: 370 querySplits = query.split(u'&') 371 else: 372 querySplits = [query] 373 for queryPart in querySplits: # this prevents duplicates even if desired 374 if queryPart: 375 if '=' in queryPart: 376 key, val = queryPart.split('=', 1) 377 val = unquote_plus(val) 378 else: 379 key = queryPart 380 val = u'true' 381 qargs[key] = val 382 383 qargParts = [u"{0}={1}".format(key, quote_plus(str(val))) 384 for key, val in qargs.items()] 385 query = '&'.join(qargParts) # only use ampersand since semicolon obsolete 386 return (qargs, query) 387 388def unquoteQuery(query): 389 """ 390 Returns query string with unquoted values 391 """ 392 sep = u'&' 393 parts = [] 394 if u';' in query: 395 splits = query.split(u';') 396 sep = u';' 397 elif u'&' in query: 398 splits = query.split(u'&') 399 else: 400 splits = [query] 401 for part in splits: # this prevents duplicates even if desired 402 if part: 403 if '=' in part: 404 key, val = part.split('=', 1) 405 val = unquote_plus(val) 406 parts.append(u"{0}={1}".format(key, str(val))) 407 else: 408 key = part 409 parts.append[part] 410 query = '&'.join(parts) 411 return query 412 413 414def packHeader(name, *values): 415 """ 416 Format and return a header line. 417 418 For example: h.packHeader('Accept', 'text/html') 419 """ 420 if isinstance(name, str): # not bytes 421 name = name.encode('ascii') 422 name = name.title() # make title case 423 values = list(values) # make copy 424 for i, value in enumerate(values): 425 if isinstance(value, str): 426 values[i] = value.encode('iso-8859-1') 427 elif isinstance(value, int): 428 values[i] = str(value).encode('ascii') 429 value = b', '.join(values) 430 return (name + b': ' + value) 431 432def packChunk(msg): 433 """ 434 Return msg bytes in a chunk 435 """ 436 lines = [] 437 size = len(msg) 438 lines.append(u"{0:x}\r\n".format(size).encode('ascii')) # convert to bytes 439 lines.append(msg) 440 lines.append(b'\r\n') 441 return (b''.join(lines)) 442 443def parseLine(raw, eols=(CRLF, LF, CR ), kind="event line"): 444 """ 445 Generator to parse line from raw bytearray 446 Each line demarcated by one of eols 447 kind is line type string for error message 448 449 Yields None If waiting for more to parse 450 Yields line Otherwise 451 452 Consumes parsed portions of raw bytearray 453 454 Raise error if eol not found before MAX_LINE_SIZE 455 """ 456 while True: 457 for eol in eols: # loop over eols unless found 458 index = raw.find(eol) # not found index == -1 459 if index >= 0: 460 break 461 462 if index < 0: # not found 463 if len(raw) > MAX_LINE_SIZE: 464 raise LineTooLong(kind) 465 else: 466 (yield None) # more data needed not done parsing header 467 continue 468 469 if index > MAX_LINE_SIZE: # found but line too long 470 raise LineTooLong(kind) 471 472 line = raw[:index] 473 index += len(eol) # strip eol 474 del raw[:index] # remove used bytes 475 (yield line) 476 return 477 478def parseLeader(raw, eols=(CRLF, LF), kind="leader header line", headers=None): 479 """ 480 Generator to parse entire leader of header lines from raw bytearray 481 Each line demarcated by one of eols 482 Yields None If more to parse 483 Yields lodict of headers Otherwise as indicated by empty headers 484 485 Raise error if eol not found before MAX_LINE_SIZE 486 """ 487 headers = headers if headers is not None else lodict() 488 while True: # loop until entire heading indicated by empty line 489 for eol in eols: # loop over eols unless found 490 index = raw.find(eol) # not found index == -1 491 if index >= 0: 492 break 493 494 if index < 0: # not found 495 if len(raw) > MAX_LINE_SIZE: 496 raise LineTooLong(kind) 497 else: 498 (yield None) # more data needed not done parsing header 499 continue 500 501 if index > MAX_LINE_SIZE: # found but line too long 502 raise LineTooLong(kind) 503 504 line = raw[:index] 505 index += len(eol) # strip eol 506 del raw[:index] # remove used bytes 507 if line: 508 line = line.decode('iso-8859-1') # convert to unicode string 509 key, value = line.split(': ', 1) 510 headers[key] = value 511 512 if len(headers) > MAX_HEADERS: 513 raise HTTPException("Too many headers, more than {0}".format(MAX_HEADERS)) 514 515 if not line: # empty line so entire leader done 516 (yield headers) # leader done 517 return 518 519def parseChunk(raw): # reading transfer encoded raw 520 """ 521 Generator to parse next chunk from raw bytearray 522 Consumes used portions of raw 523 Yields None If waiting for more bytes 524 Yields tuple (size, parms, trails, chunk) Otherwise 525 Where: 526 size is int size of the chunk 527 parms is dict of chunk extension parameters 528 trails is dict of chunk trailer headers (only on last chunk if any) 529 chunk is chunk if any or empty if not 530 531 Chunked-Body = *chunk 532 last-chunk 533 trailer 534 CRLF 535 chunk = chunk-size [ chunk-extension ] CRLF 536 chunk-data CRLF 537 chunk-size = 1*HEX 538 last-chunk = 1*("0") [ chunk-extension ] CRLF 539 chunk-extension= *( ";" chunk-ext-name [ "=" chunk-ext-val ] ) 540 chunk-ext-name = token 541 chunk-ext-val = token | quoted-string 542 chunk-data = chunk-size(OCTET) 543 trailer = *(entity-header CRLF) 544 """ 545 size = 0 546 parms = odict() 547 trails = lodict() 548 chunk = bytearray() 549 550 lineParser = parseLine(raw=raw, eols=(CRLF, ), kind="chunk size line") 551 while True: 552 line = next(lineParser) 553 if line is not None: 554 lineParser.close() # close generator 555 break 556 (yield None) 557 558 size, sep, exts = line.partition(b';') 559 try: 560 size = int(size.strip().decode('ascii'), 16) 561 except ValueError: # bad size 562 raise 563 564 if exts: # parse extensions parameters 565 exts = exts.split(b';') 566 for ext in exts: 567 ext = ext.strip() 568 name, sep, value = ext.partition(b'=') 569 parms[name.strip()] = value.strip() or None 570 571 if size == 0: # last chunk so parse trailing headers if any 572 leaderParser = parseLeader(raw=raw, 573 eols=(CRLF, LF), 574 kind="trailer header line") 575 while True: 576 headers = next(leaderParser) 577 if headers is not None: 578 leaderParser.close() 579 break 580 (yield None) 581 trails.update(headers) 582 583 else: 584 while len(raw) < size: # need more for chunk 585 (yield None) 586 587 chunk = raw[:size] 588 del raw[:size] # remove used bytes 589 590 lineParser = parseLine(raw=raw, eols=(CRLF, ), kind="chunk end line") 591 while True: 592 line = next(lineParser) 593 if line is not None: 594 lineParser.close() # close generator 595 break 596 (yield None) 597 598 if line: # not empty so raise error 599 raise ValueError("Chunk end error. Expected empty got " 600 "'{0}' instead".format(line.decode('iso-8859-1'))) 601 602 (yield (size, parms, trails, chunk)) 603 return 604 605def parseBom(raw, bom=codecs.BOM_UTF8): 606 """ 607 Generator to parse bom from raw bytearray 608 Yields None If waiting for more to parse 609 Yields bom If found 610 Yields empty bytearray Otherwise 611 Consumes parsed portions of raw bytearray 612 """ 613 size = len(bom) 614 while True: 615 if len(raw) >= size: # enough bytes for bom 616 if raw[:size] == bom: # bom present 617 del raw[:size] 618 (yield bom) 619 break 620 (yield raw[0:0]) # bom not present so yield empty bytearray 621 break 622 (yield None) # not enough bytes yet 623 return 624 625def parseStatusLine(line): 626 """ 627 Parse the response status line 628 """ 629 line = line.decode("iso-8859-1") 630 if not line: 631 raise BadStatusLine(line) # connection closed before sending valid msg 632 633 version, status, reason = aiding.repack(3, line.split(), default = u'') 634 reason = u" ".join(reason) 635 636 if not version.startswith("HTTP/"): 637 raise BadStatusLine(line) 638 639 # The status code is a three-digit number 640 try: 641 status = int(status) 642 if status < 100 or status > 999: 643 raise BadStatusLine(line) 644 except ValueError: 645 raise BadStatusLine(line) 646 return (version, status, reason) 647 648def parseRequestLine(line): 649 """ 650 Parse the request start line 651 """ 652 line = line.decode("iso-8859-1") 653 if not line: 654 raise BadRequestLine(line) # connection closed before sending valid msg 655 656 method, path, version, extra = aiding.repack(4, line.split(), default = u'') 657 658 if not version.startswith("HTTP/"): 659 raise UnknownProtocol(version) 660 661 if method not in METHODS: 662 raise BadMethod(method) 663 664 return (method, path, version) 665 666 667 668class EventSource(object): 669 """ 670 Server Sent Event Stream Client parser 671 """ 672 Bom = codecs.BOM_UTF8 # utf-8 encoded bom b'\xef\xbb\xbf' 673 674 def __init__(self, raw=None, events=None, dictable=False): 675 """ 676 Initialize Instance 677 raw must be bytearray 678 IF events is not None then used passed in deque 679 .events will be deque of event odicts 680 IF dictable then deserialize event data as json 681 682 """ 683 self.raw = raw if raw is not None else bytearray() 684 self.events = events if events is not None else deque() 685 self.dictable = True if dictable else False 686 687 self.parser = None 688 self.leid = None # last event id 689 self.bom = None # bom if any 690 self.retry = None # reconnection time in milliseconds 691 self.ended = None 692 self.closed = None 693 694 self.makeParser() 695 696 def close(self): 697 """ 698 Assign True to .closed 699 """ 700 self.closed = True 701 702 def parseEvents(self): 703 """ 704 Generator to parse events from .raw bytearray and append to .events 705 Each event is odict with the following items: 706 id: event id utf-8 decoded or empty 707 name: event name utf-8 decoded or empty 708 data: event data utf-8 decoded 709 json: event data deserialized to odict when applicable pr None 710 711 assigns .retry if any 712 713 Yields None If waiting for more bytes 714 Yields True When done 715 716 717 event = *( comment / field ) end-of-line 718 comment = colon *any-char end-of-line 719 field = 1*name-char [ colon [ space ] *any-char ] end-of-line 720 end-of-line = ( cr lf / cr / lf / eof ) 721 eof = < matches repeatedly at the end of the stream > 722 lf = \n 0xA 723 cr = \r 0xD 724 space = 0x20 725 colon = 0x3A 726 bom = \uFEFF when encoded as utf-8 b'\xef\xbb\xbf' 727 name-char = a Unicode character other than LF, CR, or : 728 any-char = a Unicode character other than LF or CR 729 Event streams in this format must always be encoded as UTF-8. [RFC3629] 730 """ 731 eid = self.leid 732 ename = u'' 733 edata = u'' 734 parts = [] 735 ejson = None 736 lineParser = parseLine(raw=self.raw, eols=(CRLF, LF, CR ), kind="event line") 737 while True: 738 line = next(lineParser) 739 if line is None: 740 (yield None) 741 continue 742 743 if not line or self.closed: # empty line or closed so attempt dispatch 744 if parts: 745 edata = u'\n'.join(parts) 746 if edata: # data so dispatch event by appending to .events 747 if self.dictable: 748 try: 749 ejson = json.loads(edata, encoding='utf-8', object_pairs_hook=odict) 750 except ValueError as ex: 751 ejson = None 752 else: # valid json set edata to ejson 753 edata = ejson 754 755 self.events.append(odict([('id', eid), 756 ('name', ename), 757 ('data', edata), 758 ])) 759 if self.closed: # all done 760 lineParser.close() # close generator 761 break 762 ename = u'' 763 edata = u'' 764 parts = [] 765 ejson = None 766 continue # parse another event if any 767 768 field, sep, value = line.partition(b':') 769 if sep: # has colon 770 if not field: # comment so ignore 771 # may need to update retry timer here 772 continue 773 774 field = field.decode('UTF-8') 775 if value and value[0:1] == b' ': 776 del value[0] 777 value = value.decode('UTF-8') 778 779 if field == u'event': 780 ename = value 781 elif field == u'data': 782 parts.append(value) 783 elif field == u'id': 784 self.leid = eid = value 785 elif field == u'retry': # 786 try: 787 value = int(value) 788 except ValueError as ex: 789 pass # ignore 790 else: 791 self.retry = value 792 793 (yield (eid, ename, edata)) 794 return 795 796 def parseEventStream(self): 797 """ 798 Generator to parse event stream from .raw bytearray stream 799 appends each event to .events deque. 800 assigns .bom if any 801 assigns .retry if any 802 Parses until connection closed 803 804 Each event is odict with the following items: 805 id: event id utf-8 decoded or empty 806 name: event name utf-8 decoded or empty 807 data: event data utf-8 decoded 808 json: event data deserialized to odict when applicable pr None 809 810 Yields None If waiting for more bytes 811 Yields True When completed and sets .ended to True 812 If BOM present at beginning of event stream then assigns to .bom and 813 deletes. 814 Consumes bytearray as it parses 815 816 stream = [ bom ] *event 817 event = *( comment / field ) end-of-line 818 comment = colon *any-char end-of-line 819 field = 1*name-char [ colon [ space ] *any-char ] end-of-line 820 end-of-line = ( cr lf / cr / lf / eof ) 821 eof = < matches repeatedly at the end of the stream > 822 lf = \n 0xA 823 cr = \r 0xD 824 space = 0x20 825 colon = 0x3A 826 bom = \uFEFF when encoded as utf-8 b'\xef\xbb\xbf' 827 name-char = a Unicode character other than LF, CR, or : 828 any-char = a Unicode character other than LF or CR 829 Event streams in this format must always be encoded as UTF-8. [RFC3629] 830 """ 831 self.bom = None 832 self.retry = None 833 self.leid = None 834 self.ended = None 835 self.closed = None 836 837 bomParser = parseBom(raw=self.raw, bom=self.Bom) 838 while True: # parse bom if any 839 bom = next(bomParser) 840 if bom is not None: 841 bomParser.close() # close generator 842 self.bom = bom.decode('UTF-8') 843 break 844 845 if self.closed: # no more data so finish 846 bomParser.close() 847 break 848 (yield None) 849 850 eventsParser = self.parseEvents() 851 while True: # parse event(s) so far if any 852 result = next(eventParser) 853 if result is not None: 854 eventsParser.close() 855 break 856 (yield None) 857 858 (yield True) 859 return 860 861 def makeParser(self, raw=None): 862 """ 863 Make event stream parser generator and assign to .parser 864 Assign msg to .msg If provided 865 """ 866 if raw: 867 self.raw = raw 868 self.parser = self.parseEvents() # make generator 869 870 def parse(self): 871 """ 872 Service the event stream parsing 873 must call .makeParser to setup parser 874 When done parsing, 875 .parser is None 876 .ended is True 877 """ 878 if self.parser: 879 result = next(self.parser) 880 if result is not None: 881 self.parser.close() 882 self.parser = None 883 884 885class Parsent(object): 886 """ 887 Base class for objects that parse HTTP messages 888 """ 889 def __init__(self, 890 msg=None, 891 dictable=None, 892 method=u'GET'): 893 """ 894 Initialize Instance 895 msg = bytearray of request msg to parse 896 dictable = True If should attempt to convert body to json 897 method = method of associated request 898 """ 899 self.msg = msg if msg is not None else bytearray() 900 self.dictable = True if dictable else False # convert body json 901 self.parser = None # response parser generator 902 self.version = None # HTTP-Version from status line 903 self.length = None # content length of body in request 904 self.chunked = None # is transfer encoding "chunked" being used? 905 self.jsoned = None # is content application/json 906 self.encoding = 'ISO-8859-1' # encoding charset if provided else default 907 self.persisted = None # persist connection until client closes 908 self.started = None # True first time parse called and .msg is not empty 909 self.headed = None # head completely parsed 910 self.bodied = None # body completely parsed 911 self.ended = None # response from server has ended no more remaining 912 self.closed = None # True when connection closed 913 self.errored = False # True when error occurs in response processing 914 self.error = None # Error Description String 915 916 self.headers = None 917 self.parms = None # chunked encoding extension parameters 918 self.trails = None # chunked encoding trailing headers 919 self.body = bytearray() # body data bytearray 920 self.text = u'' # body decoded as unicode string 921 self.data = None # content dict deserialized from body json 922 self.method = method.upper() if method else u'GET' 923 924 self.makeParser() # set up for new msg 925 926 def reinit(self, 927 msg=None, 928 dictable=None, 929 method=u'GET'): 930 """ 931 Reinitialize Instance 932 msg = bytearray of request msg to parse 933 dictable = Boolean flag If True attempt to convert json body 934 method = method verb of associated request 935 """ 936 if msg is not None: 937 self.msg = msg 938 if dictable is not None: 939 self.dictable = True if dictable else False 940 if method is not None: 941 self.method = method.upper() 942 self.data = None 943 944 def close(self): 945 """ 946 Assign True to .closed and close parser 947 """ 948 self.closed = True 949 950 def checkPersisted(self): 951 """ 952 Checks headers to determine if connection should be kept open until 953 client closes it 954 Sets the .persisted flag 955 """ 956 self.persisted = False 957 958 def parseHead(self): 959 """ 960 Generator to parse headers in heading of .msg 961 Yields None if more to parse 962 Yields True if done parsing 963 """ 964 if self.headed: 965 return # already parsed the head 966 self.headers = lodict() 967 self.checkPersisted() # sets .persisted 968 self.headed = True 969 yield True 970 return 971 972 def parseBody(self): 973 """ 974 Parse body 975 """ 976 if self.bodied: 977 return # already parsed the body 978 self.length = 0 979 self.bodied = True 980 (yield True) 981 return 982 983 def parseMessage(self): 984 """ 985 Generator to parse message bytearray. 986 Parses msg if not None 987 Otherwise parse .msg 988 """ 989 self.headed = False 990 self.bodied = False 991 self.ended = False 992 self.closed = False 993 self.errored = False 994 self.error = None 995 996 while not self.started: 997 if self.msg: 998 self.started = True 999 break 1000 (yield None) 1001 1002 try: 1003 headParser = self.parseHead() 1004 while True: 1005 result = next(headParser) 1006 if result is not None: 1007 headParser.close() 1008 break 1009 (yield None) 1010 1011 bodyParser = self.parseBody() 1012 while True: 1013 result = next(bodyParser) 1014 if result is not None: 1015 bodyParser.close() 1016 break 1017 (yield None) 1018 except HTTPException as ex: 1019 self.errored = True 1020 self.error = str(ex) 1021 1022 self.ended = True 1023 self.started = False 1024 (yield True) 1025 return 1026 1027 def makeParser(self, msg=None): 1028 """ 1029 Make message parser generator and assign to .parser 1030 Assign msg to .msg If provided 1031 """ 1032 if msg is not None: 1033 self.msg = msg 1034 if self.parser: 1035 self.parser.close() 1036 self.parser = self.parseMessage() # make generator 1037 1038 def parse(self): 1039 """ 1040 Service the message parsing 1041 must call .makeParser to setup parser 1042 When done parsing, 1043 .parser is None 1044 .ended is True 1045 """ 1046 if self.parser: 1047 result = next(self.parser) 1048 if result is not None: 1049 self.parser.close() 1050 self.parser = None 1051 1052 def dictify(self): 1053 """ 1054 Attempt to convert body to dict data if .dictable or json content-type 1055 """ 1056 # convert body to data based on content-type, and .dictable flag 1057 1058 if self.jsoned or self.dictable: # attempt to deserialize json 1059 try: 1060 self.data = json.loads(self.body.decode('utf-8'), 1061 encoding='utf-8', 1062 object_pairs_hook=odict) 1063 except ValueError as ex: 1064 self.data = None 1065 1066