Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 20 additions & 16 deletions pcapkit/protocols/application/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,26 @@ def layer(self) -> 'Literal["Application"]':
"""Protocol layer."""
return self.__layer__

##########################################################################
# Methods.
##########################################################################

@classmethod
def from_data(cls, data: '_PT | dict[str, Any]', **kwargs: 'Any') -> 'Self':
"""Create protocol instance from data.

Args:
data: Protocol data.
**kwargs: Construction keywords, as for :meth:`Protocol.from_data
<pcapkit.protocols.protocol.Protocol.from_data>`.

Returns:
Protocol instance, cut back to what was captured when the header
it was parsed from was cut short (:issue:`1458`).

"""
return replay_short_read(super().from_data(data, **kwargs), data)

##########################################################################
# Data models.
##########################################################################
Expand Down Expand Up @@ -115,22 +135,6 @@ def __index__(cls) -> 'NoReturn': # pylint: disable=invalid-index-returned
"""
raise IntError(f'{cls.__name__!r} object cannot be interpreted as an integer')

@classmethod
def from_data(cls, data: '_PT | dict[str, Any]', **kwargs: 'Any') -> 'Self':
"""Create protocol instance from data.

Args:
data: Protocol data.
**kwargs: Construction keywords, as for :meth:`Protocol.from_data
<pcapkit.protocols.protocol.Protocol.from_data>`.

Returns:
Protocol instance, cut back to what was captured when the header
it was parsed from was cut short (:issue:`1458`).

"""
return replay_short_read(super().from_data(data, **kwargs), data)

##########################################################################
# Utilities.
##########################################################################
Expand Down
278 changes: 139 additions & 139 deletions pcapkit/protocols/application/httpv1.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,102 +277,6 @@ def make(self, # type: ignore[override]
data=header_line + b''.join(header_fields) + b'\r\n' + body,
)

@staticmethod
def _check_raw(raw: 'bytes', *values: 'str', fold: 'bool') -> 'None':
"""Refuse a raw line that would write a line break its values lack.

Reading a line back strips the whitespace around its tokens, and that
includes a lone CR or LF, so a raw line can read back as values that
carry no line break while writing one to the wire.

Args:
raw: Raw start line or field line, CRLF excluded.
*values: The values ``raw`` reads back as.
fold: Whether the CRLF of an ``obs-fold``, followed by SP or HTAB,
is allowed in ``raw``.

Raises:
ProtocolError: If ``raw`` carries a CR or LF that ``values`` do not.

"""
if fold:
raw = re.sub(rb'\r\n(?=[ \t])', b'', raw)
for char in ('\r', '\n'):
if raw.count(char.encode()) != sum(value.count(char) for value in values):
raise ProtocolError('HTTP: raw line carries a CR or LF its values do not')

def _make_start_line(self, http_version: 'str | bytes', *,
method: 'Optional[Enum_Method | str | bytes]' = None,
uri: 'Optional[str | bytes]' = None,
status: 'Optional[Enum_StatusCode | str | bytes | int]' = None,
status_default: 'Optional[int]' = None,
status_namespace: 'Optional[dict[str, int] | dict[int, str] | _Type[StdlibEnum] | _Type[AenumEnum]]' = None, # pylint: disable=line-too-long
status_reversed: 'bool' = False,
message: 'Optional[str | bytes]' = None,
charset: 'Optional[str]' = None) -> 'bytes':
"""Make an HTTP/1.* start line from its values.

Args:
http_version: HTTP version.
method: HTTP method.
uri: HTTP request URI.
status: HTTP status code.
status_default: Default HTTP status code.
status_namespace: Namespace of HTTP status code.
status_reversed: Whether to reverse the namespace.
message: HTTP status message.
charset: Encoding for a :obj:`str` request URI or status message,
**UTF-8** if not given.

Returns:
The start line, CRLF included.

Raises:
ProtocolError: If the values are neither a request's nor a
response's, or if they are a request's and ``uri`` is
:obj:`None`.

"""
version = http_version.encode() if isinstance(http_version, str) else http_version
if method is not None and status is None:
if uri is None:
raise ProtocolError('HTTP: invalid start line: a request needs a URI')

if isinstance(method, Enum_Method):
meth = method.value.encode()
elif isinstance(method, bytes):
meth = method
elif isinstance(method, str):
meth = method.encode()
else:
meth = method.value.encode()
uri_val = uri.encode(charset or 'utf-8') if isinstance(uri, str) else uri

header_line = b'%s %s HTTP/%s\r\n' % (meth, uri_val, version)
elif method is None and status is not None:
# NOTE: ``status`` defaults to :data:`None`, so ``_make_index`` has no
# enum default to infer the namespace of a :obj:`str` member name from;
# name it here rather than leave the name unresolvable (#2356).
status_code = self._make_index(
status, status_default,
namespace=status_namespace if status_namespace is not None else Enum_StatusCode,
reversed=status_reversed, pack=False)
status_code_val = int(status_code)

if message is None:
msg = getattr(status, 'message', None) or getattr(status_code, 'message', b'') or b''
else:
msg = message.encode(charset or 'utf-8') if isinstance(message, str) else message
if isinstance(msg, str):
msg = msg.encode()

# status-code is 3DIGIT (RFC 9112, section 4), so keep a leading zero
header_line = b'HTTP/%s %03d %s\r\n' % (version, status_code_val, msg)
else:
raise ProtocolError('HTTP: invalid start line: must have either a method or a status, '
f'not method={method!r} and status={status!r}')
return header_line

@classmethod
def id(cls) -> 'tuple[Literal["HTTP"], Literal["HTTPv1"]]': # type: ignore[override]
"""Index ID of the protocol.
Expand Down Expand Up @@ -481,6 +385,20 @@ def _read_http_header(self, header: 'bytes') -> 'tuple[Data_Header, OrderedMulti

return header_line, header_fields

def _read_http_body(self, body: 'bytes', *,
headers: 'OrderedMultiDict[str, str]') -> 'Any':
"""Read HTTP/1.* body.

Args:
body: HTTP body data.
headers: HTTP header fields.

Returns:
Raw HTTP body.

"""
return body

@staticmethod
def _split_field_lines(headerfield: 'bytes') -> 'tuple[bytes, ...]':
"""Split the field lines of an HTTP/1.* header.
Expand Down Expand Up @@ -517,42 +435,6 @@ def _split_field_lines(headerfield: 'bytes') -> 'tuple[bytes, ...]':
raw_fields.append(line)
return tuple(raw_fields)

def _read_field_line(self, line: 'bytes') -> 'tuple[str, str]':
"""Read one HTTP/1.* field line.

Args:
line: Field line as received, CRLF excluded, with any ``obs-fold``
continuations still in it.

Returns:
The field name and the field value, the latter unfolded and without
the optional whitespace around it.

Raises:
ProtocolError: If the line is not a field line.

"""
# NOTE: The accumulator is right-stripped as well as the continuation,
# because the production is ``obs-fold = OWS CRLF RWS`` and it is the
# *whole* obs-fold that is replaced by a single space -- the RFC's own
# remedy (:rfc:`9112#section-5.2`). The OWS before the CRLF belongs to
# the fold, not to the value, so ``X: a \t\r\n\tb`` unfolds to ``'a b'``.
field, *continuations = line.split(b'\r\n')
for continuation in continuations:
if not continuation.startswith((b' ', b'\t')):
raise ProtocolError('HTTP: invalid format')
field = field.rstrip() + b' ' + continuation.strip()

# NOTE: Checked rather than left to ``item[1]``, and refused rather than
# skipped: a field line with no colon is not a header field, and dropping
# it would hand back a message whose fields are quietly not the ones on
# the wire. ``ProtocolError`` for the reason ``read`` gives, and to the
# same message.
item = re.split(rb'\s*:\s*', field, maxsplit=1)
if len(item) != 2:
raise ProtocolError('HTTP: invalid format')
return self.decode(item[0].strip()), self.decode(item[1].strip())

def _read_start_line(self, startline: 'bytes') -> 'Data_Header':
"""Read an HTTP/1.* start line.

Expand Down Expand Up @@ -607,16 +489,134 @@ def _read_start_line(self, startline: 'bytes') -> 'Data_Header':
raise ProtocolError('HTTP: invalid format')
return header_line

def _read_http_body(self, body: 'bytes', *,
headers: 'OrderedMultiDict[str, str]') -> 'Any':
"""Read HTTP/1.* body.
def _make_start_line(self, http_version: 'str | bytes', *,
method: 'Optional[Enum_Method | str | bytes]' = None,
uri: 'Optional[str | bytes]' = None,
status: 'Optional[Enum_StatusCode | str | bytes | int]' = None,
status_default: 'Optional[int]' = None,
status_namespace: 'Optional[dict[str, int] | dict[int, str] | _Type[StdlibEnum] | _Type[AenumEnum]]' = None, # pylint: disable=line-too-long
status_reversed: 'bool' = False,
message: 'Optional[str | bytes]' = None,
charset: 'Optional[str]' = None) -> 'bytes':
"""Make an HTTP/1.* start line from its values.

Args:
body: HTTP body data.
headers: HTTP header fields.
http_version: HTTP version.
method: HTTP method.
uri: HTTP request URI.
status: HTTP status code.
status_default: Default HTTP status code.
status_namespace: Namespace of HTTP status code.
status_reversed: Whether to reverse the namespace.
message: HTTP status message.
charset: Encoding for a :obj:`str` request URI or status message,
**UTF-8** if not given.

Returns:
Raw HTTP body.
The start line, CRLF included.

Raises:
ProtocolError: If the values are neither a request's nor a
response's, or if they are a request's and ``uri`` is
:obj:`None`.

"""
return body
version = http_version.encode() if isinstance(http_version, str) else http_version
if method is not None and status is None:
if uri is None:
raise ProtocolError('HTTP: invalid start line: a request needs a URI')

if isinstance(method, Enum_Method):
meth = method.value.encode()
elif isinstance(method, bytes):
meth = method
elif isinstance(method, str):
meth = method.encode()
else:
meth = method.value.encode()
uri_val = uri.encode(charset or 'utf-8') if isinstance(uri, str) else uri

header_line = b'%s %s HTTP/%s\r\n' % (meth, uri_val, version)
elif method is None and status is not None:
# NOTE: ``status`` defaults to :data:`None`, so ``_make_index`` has no
# enum default to infer the namespace of a :obj:`str` member name from;
# name it here rather than leave the name unresolvable (#2356).
status_code = self._make_index(
status, status_default,
namespace=status_namespace if status_namespace is not None else Enum_StatusCode,
reversed=status_reversed, pack=False)
status_code_val = int(status_code)

if message is None:
msg = getattr(status, 'message', None) or getattr(status_code, 'message', b'') or b''
else:
msg = message.encode(charset or 'utf-8') if isinstance(message, str) else message
if isinstance(msg, str):
msg = msg.encode()

# status-code is 3DIGIT (RFC 9112, section 4), so keep a leading zero
header_line = b'HTTP/%s %03d %s\r\n' % (version, status_code_val, msg)
else:
raise ProtocolError('HTTP: invalid start line: must have either a method or a status, '
f'not method={method!r} and status={status!r}')
return header_line

@staticmethod
def _check_raw(raw: 'bytes', *values: 'str', fold: 'bool') -> 'None':
"""Refuse a raw line that would write a line break its values lack.

Reading a line back strips the whitespace around its tokens, and that
includes a lone CR or LF, so a raw line can read back as values that
carry no line break while writing one to the wire.

Args:
raw: Raw start line or field line, CRLF excluded.
*values: The values ``raw`` reads back as.
fold: Whether the CRLF of an ``obs-fold``, followed by SP or HTAB,
is allowed in ``raw``.

Raises:
ProtocolError: If ``raw`` carries a CR or LF that ``values`` do not.

"""
if fold:
raw = re.sub(rb'\r\n(?=[ \t])', b'', raw)
for char in ('\r', '\n'):
if raw.count(char.encode()) != sum(value.count(char) for value in values):
raise ProtocolError('HTTP: raw line carries a CR or LF its values do not')

def _read_field_line(self, line: 'bytes') -> 'tuple[str, str]':
"""Read one HTTP/1.* field line.

Args:
line: Field line as received, CRLF excluded, with any ``obs-fold``
continuations still in it.

Returns:
The field name and the field value, the latter unfolded and without
the optional whitespace around it.

Raises:
ProtocolError: If the line is not a field line.

"""
# NOTE: The accumulator is right-stripped as well as the continuation,
# because the production is ``obs-fold = OWS CRLF RWS`` and it is the
# *whole* obs-fold that is replaced by a single space -- the RFC's own
# remedy (:rfc:`9112#section-5.2`). The OWS before the CRLF belongs to
# the fold, not to the value, so ``X: a \t\r\n\tb`` unfolds to ``'a b'``.
field, *continuations = line.split(b'\r\n')
for continuation in continuations:
if not continuation.startswith((b' ', b'\t')):
raise ProtocolError('HTTP: invalid format')
field = field.rstrip() + b' ' + continuation.strip()

# NOTE: Checked rather than left to ``item[1]``, and refused rather than
# skipped: a field line with no colon is not a header field, and dropping
# it would hand back a message whose fields are quietly not the ones on
# the wire. ``ProtocolError`` for the reason ``read`` gives, and to the
# same message.
item = re.split(rb'\s*:\s*', field, maxsplit=1)
if len(item) != 2:
raise ProtocolError('HTTP: invalid format')
return self.decode(item[0].strip()), self.decode(item[1].strip())
Loading
Loading