You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			251 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			251 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			Python
		
	
# Code to read HTTP data
 | 
						|
#
 | 
						|
# Strategy: each reader is a callable which takes a ReceiveBuffer object, and
 | 
						|
# either:
 | 
						|
# 1) consumes some of it and returns an Event
 | 
						|
# 2) raises a LocalProtocolError (for consistency -- e.g. we call validate()
 | 
						|
#    and it might raise a LocalProtocolError, so simpler just to always use
 | 
						|
#    this)
 | 
						|
# 3) returns None, meaning "I need more data"
 | 
						|
#
 | 
						|
# If they have a .read_eof attribute, then this will be called if an EOF is
 | 
						|
# received -- but this is optional. Either way, the actual ConnectionClosed
 | 
						|
# event will be generated afterwards.
 | 
						|
#
 | 
						|
# READERS is a dict describing how to pick a reader. It maps states to either:
 | 
						|
# - a reader
 | 
						|
# - or, for body readers, a dict of per-framing reader factories
 | 
						|
 | 
						|
import re
 | 
						|
from typing import Any, Callable, Dict, Iterable, NoReturn, Optional, Tuple, Type, Union
 | 
						|
 | 
						|
from ._abnf import chunk_header, header_field, request_line, status_line
 | 
						|
from ._events import Data, EndOfMessage, InformationalResponse, Request, Response
 | 
						|
from ._receivebuffer import ReceiveBuffer
 | 
						|
from ._state import (
 | 
						|
    CLIENT,
 | 
						|
    CLOSED,
 | 
						|
    DONE,
 | 
						|
    IDLE,
 | 
						|
    MUST_CLOSE,
 | 
						|
    SEND_BODY,
 | 
						|
    SEND_RESPONSE,
 | 
						|
    SERVER,
 | 
						|
)
 | 
						|
from ._util import LocalProtocolError, RemoteProtocolError, Sentinel, validate
 | 
						|
 | 
						|
__all__ = ["READERS"]
 | 
						|
 | 
						|
header_field_re = re.compile(header_field.encode("ascii"))
 | 
						|
obs_fold_re = re.compile(rb"[ \t]+")
 | 
						|
 | 
						|
 | 
						|
def _obsolete_line_fold(lines: Iterable[bytes]) -> Iterable[bytes]:
 | 
						|
    it = iter(lines)
 | 
						|
    last: Optional[bytes] = None
 | 
						|
    for line in it:
 | 
						|
        match = obs_fold_re.match(line)
 | 
						|
        if match:
 | 
						|
            if last is None:
 | 
						|
                raise LocalProtocolError("continuation line at start of headers")
 | 
						|
            if not isinstance(last, bytearray):
 | 
						|
                # Cast to a mutable type, avoiding copy on append to ensure O(n) time
 | 
						|
                last = bytearray(last)
 | 
						|
            last += b" "
 | 
						|
            last += line[match.end() :]
 | 
						|
        else:
 | 
						|
            if last is not None:
 | 
						|
                yield last
 | 
						|
            last = line
 | 
						|
    if last is not None:
 | 
						|
        yield last
 | 
						|
 | 
						|
 | 
						|
def _decode_header_lines(
 | 
						|
    lines: Iterable[bytes],
 | 
						|
) -> Iterable[Tuple[bytes, bytes]]:
 | 
						|
    for line in _obsolete_line_fold(lines):
 | 
						|
        matches = validate(header_field_re, line, "illegal header line: {!r}", line)
 | 
						|
        yield (matches["field_name"], matches["field_value"])
 | 
						|
 | 
						|
 | 
						|
request_line_re = re.compile(request_line.encode("ascii"))
 | 
						|
 | 
						|
 | 
						|
def maybe_read_from_IDLE_client(buf: ReceiveBuffer) -> Optional[Request]:
 | 
						|
    lines = buf.maybe_extract_lines()
 | 
						|
    if lines is None:
 | 
						|
        if buf.is_next_line_obviously_invalid_request_line():
 | 
						|
            raise LocalProtocolError("illegal request line")
 | 
						|
        return None
 | 
						|
    if not lines:
 | 
						|
        raise LocalProtocolError("no request line received")
 | 
						|
    matches = validate(
 | 
						|
        request_line_re, lines[0], "illegal request line: {!r}", lines[0]
 | 
						|
    )
 | 
						|
    return Request(
 | 
						|
        headers=list(_decode_header_lines(lines[1:])), _parsed=True, **matches
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
status_line_re = re.compile(status_line.encode("ascii"))
 | 
						|
 | 
						|
 | 
						|
def maybe_read_from_SEND_RESPONSE_server(
 | 
						|
    buf: ReceiveBuffer,
 | 
						|
) -> Union[InformationalResponse, Response, None]:
 | 
						|
    lines = buf.maybe_extract_lines()
 | 
						|
    if lines is None:
 | 
						|
        if buf.is_next_line_obviously_invalid_request_line():
 | 
						|
            raise LocalProtocolError("illegal request line")
 | 
						|
        return None
 | 
						|
    if not lines:
 | 
						|
        raise LocalProtocolError("no response line received")
 | 
						|
    matches = validate(status_line_re, lines[0], "illegal status line: {!r}", lines[0])
 | 
						|
    http_version = (
 | 
						|
        b"1.1" if matches["http_version"] is None else matches["http_version"]
 | 
						|
    )
 | 
						|
    reason = b"" if matches["reason"] is None else matches["reason"]
 | 
						|
    status_code = int(matches["status_code"])
 | 
						|
    class_: Union[Type[InformationalResponse], Type[Response]] = (
 | 
						|
        InformationalResponse if status_code < 200 else Response
 | 
						|
    )
 | 
						|
    return class_(
 | 
						|
        headers=list(_decode_header_lines(lines[1:])),
 | 
						|
        _parsed=True,
 | 
						|
        status_code=status_code,
 | 
						|
        reason=reason,
 | 
						|
        http_version=http_version,
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
class ContentLengthReader:
 | 
						|
    def __init__(self, length: int) -> None:
 | 
						|
        self._length = length
 | 
						|
        self._remaining = length
 | 
						|
 | 
						|
    def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]:
 | 
						|
        if self._remaining == 0:
 | 
						|
            return EndOfMessage()
 | 
						|
        data = buf.maybe_extract_at_most(self._remaining)
 | 
						|
        if data is None:
 | 
						|
            return None
 | 
						|
        self._remaining -= len(data)
 | 
						|
        return Data(data=data)
 | 
						|
 | 
						|
    def read_eof(self) -> NoReturn:
 | 
						|
        raise RemoteProtocolError(
 | 
						|
            "peer closed connection without sending complete message body "
 | 
						|
            "(received {} bytes, expected {})".format(
 | 
						|
                self._length - self._remaining, self._length
 | 
						|
            )
 | 
						|
        )
 | 
						|
 | 
						|
 | 
						|
chunk_header_re = re.compile(chunk_header.encode("ascii"))
 | 
						|
 | 
						|
 | 
						|
class ChunkedReader:
 | 
						|
    def __init__(self) -> None:
 | 
						|
        self._bytes_in_chunk = 0
 | 
						|
        # After reading a chunk, we have to throw away the trailing \r\n.
 | 
						|
        # This tracks the bytes that we need to match and throw away.
 | 
						|
        self._bytes_to_discard = b""
 | 
						|
        self._reading_trailer = False
 | 
						|
 | 
						|
    def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]:
 | 
						|
        if self._reading_trailer:
 | 
						|
            lines = buf.maybe_extract_lines()
 | 
						|
            if lines is None:
 | 
						|
                return None
 | 
						|
            return EndOfMessage(headers=list(_decode_header_lines(lines)))
 | 
						|
        if self._bytes_to_discard:
 | 
						|
            data = buf.maybe_extract_at_most(len(self._bytes_to_discard))
 | 
						|
            if data is None:
 | 
						|
                return None
 | 
						|
            if data != self._bytes_to_discard[: len(data)]:
 | 
						|
                raise LocalProtocolError(
 | 
						|
                    f"malformed chunk footer: {data!r} (expected {self._bytes_to_discard!r})"
 | 
						|
                )
 | 
						|
            self._bytes_to_discard = self._bytes_to_discard[len(data) :]
 | 
						|
            if self._bytes_to_discard:
 | 
						|
                return None
 | 
						|
            # else, fall through and read some more
 | 
						|
        assert self._bytes_to_discard == b""
 | 
						|
        if self._bytes_in_chunk == 0:
 | 
						|
            # We need to refill our chunk count
 | 
						|
            chunk_header = buf.maybe_extract_next_line()
 | 
						|
            if chunk_header is None:
 | 
						|
                return None
 | 
						|
            matches = validate(
 | 
						|
                chunk_header_re,
 | 
						|
                chunk_header,
 | 
						|
                "illegal chunk header: {!r}",
 | 
						|
                chunk_header,
 | 
						|
            )
 | 
						|
            # XX FIXME: we discard chunk extensions. Does anyone care?
 | 
						|
            self._bytes_in_chunk = int(matches["chunk_size"], base=16)
 | 
						|
            if self._bytes_in_chunk == 0:
 | 
						|
                self._reading_trailer = True
 | 
						|
                return self(buf)
 | 
						|
            chunk_start = True
 | 
						|
        else:
 | 
						|
            chunk_start = False
 | 
						|
        assert self._bytes_in_chunk > 0
 | 
						|
        data = buf.maybe_extract_at_most(self._bytes_in_chunk)
 | 
						|
        if data is None:
 | 
						|
            return None
 | 
						|
        self._bytes_in_chunk -= len(data)
 | 
						|
        if self._bytes_in_chunk == 0:
 | 
						|
            self._bytes_to_discard = b"\r\n"
 | 
						|
            chunk_end = True
 | 
						|
        else:
 | 
						|
            chunk_end = False
 | 
						|
        return Data(data=data, chunk_start=chunk_start, chunk_end=chunk_end)
 | 
						|
 | 
						|
    def read_eof(self) -> NoReturn:
 | 
						|
        raise RemoteProtocolError(
 | 
						|
            "peer closed connection without sending complete message body "
 | 
						|
            "(incomplete chunked read)"
 | 
						|
        )
 | 
						|
 | 
						|
 | 
						|
class Http10Reader:
 | 
						|
    def __call__(self, buf: ReceiveBuffer) -> Optional[Data]:
 | 
						|
        data = buf.maybe_extract_at_most(999999999)
 | 
						|
        if data is None:
 | 
						|
            return None
 | 
						|
        return Data(data=data)
 | 
						|
 | 
						|
    def read_eof(self) -> EndOfMessage:
 | 
						|
        return EndOfMessage()
 | 
						|
 | 
						|
 | 
						|
def expect_nothing(buf: ReceiveBuffer) -> None:
 | 
						|
    if buf:
 | 
						|
        raise LocalProtocolError("Got data when expecting EOF")
 | 
						|
    return None
 | 
						|
 | 
						|
 | 
						|
ReadersType = Dict[
 | 
						|
    Union[Type[Sentinel], Tuple[Type[Sentinel], Type[Sentinel]]],
 | 
						|
    Union[Callable[..., Any], Dict[str, Callable[..., Any]]],
 | 
						|
]
 | 
						|
 | 
						|
READERS: ReadersType = {
 | 
						|
    (CLIENT, IDLE): maybe_read_from_IDLE_client,
 | 
						|
    (SERVER, IDLE): maybe_read_from_SEND_RESPONSE_server,
 | 
						|
    (SERVER, SEND_RESPONSE): maybe_read_from_SEND_RESPONSE_server,
 | 
						|
    (CLIENT, DONE): expect_nothing,
 | 
						|
    (CLIENT, MUST_CLOSE): expect_nothing,
 | 
						|
    (CLIENT, CLOSED): expect_nothing,
 | 
						|
    (SERVER, DONE): expect_nothing,
 | 
						|
    (SERVER, MUST_CLOSE): expect_nothing,
 | 
						|
    (SERVER, CLOSED): expect_nothing,
 | 
						|
    SEND_BODY: {
 | 
						|
        "chunked": ChunkedReader,
 | 
						|
        "content-length": ContentLengthReader,
 | 
						|
        "http/1.0": Http10Reader,
 | 
						|
    },
 | 
						|
}
 |