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