|
| 1 | +"""This module contains the binary specification for an INTERSECT message IF the protocol does not provide any built-in header support. If it does, use the built-in functionality of the protocol. |
| 2 | +
|
| 3 | +Some protocols do not have any built-in header support, leaving it up to us to define the binary structure of the message. We want to avoid "chunking" messages into multiple parts, so a message should be guaranteed to include all metadata. |
| 4 | +""" |
| 5 | + |
| 6 | +from intersect_sdk_common import IntersectApplicationError |
| 7 | + |
| 8 | +_HEADER_KV_SEPARATOR = b'\x02' |
| 9 | +"""Indicates end of a header key and start of a header value. |
| 10 | +
|
| 11 | +Used because applications should generally assign no special meaning to this byte, and this byte has no reason to appear in header keys or values. |
| 12 | +""" |
| 13 | +_HEADER_VALUE_SEPARATOR = b'\x03' |
| 14 | +"""Indicates end of a header value and start of the next header key. |
| 15 | +
|
| 16 | +Used because applications should generally assign no special meaning to this byte, and this byte has no reason to appear in header keys or values. |
| 17 | +""" |
| 18 | +_PAYLOAD_SEPARATOR = b'\x01' |
| 19 | +"""Indicates end of headers and start of the payload. |
| 20 | +
|
| 21 | +Used because applications should generally assign no special meaning to this byte, and this byte has no reason to appear in header keys or values. |
| 22 | +""" |
| 23 | + |
| 24 | +_TOTAL_REASONABLE_HEADER_BYTES = 131072 |
| 25 | +"""The total number of bytes that should be reasonably expected to be used for header keys, values, and header separators combined. This provides applications with some level of DOS protection. |
| 26 | +
|
| 27 | +It is rare in practice for ANY application to use this number of bytes for the total amount of headers, for example http2_max_header_size in NGINX is rarely set above 128KB |
| 28 | +""" |
| 29 | + |
| 30 | +CONTENT_TYPE_HEADER_KEY = 'content_type' |
| 31 | +CONTENT_TYPE_HEADER_KEY_BYTES = CONTENT_TYPE_HEADER_KEY.encode() |
| 32 | + |
| 33 | + |
| 34 | +def create_binary_message(body: bytes, content_type: str, headers: dict[str, str]) -> bytes: |
| 35 | + """Create a binary message from the body, headers, and content type.""" |
| 36 | + return b''.join( |
| 37 | + [ |
| 38 | + # content-type 'header' first (this is generally handled separately from other headers in many protocols) |
| 39 | + CONTENT_TYPE_HEADER_KEY_BYTES, |
| 40 | + _HEADER_KV_SEPARATOR, |
| 41 | + content_type.encode(), |
| 42 | + _HEADER_VALUE_SEPARATOR if len(headers) else b'', |
| 43 | + # headers |
| 44 | + _HEADER_VALUE_SEPARATOR.join( |
| 45 | + _HEADER_KV_SEPARATOR.join([key.encode(), value.encode()]) |
| 46 | + for key, value in headers.items() |
| 47 | + ), |
| 48 | + # end of headers |
| 49 | + _PAYLOAD_SEPARATOR, |
| 50 | + # body |
| 51 | + body, |
| 52 | + ] |
| 53 | + ) |
| 54 | + |
| 55 | + |
| 56 | +def parse_binary_message(message: bytes) -> tuple[bytes, str, dict[str, str]]: |
| 57 | + """Parse a binary message into its body, content type, and headers.""" |
| 58 | + # IMPORTANT!!! ----- Total length of header keys and values combined should be limited to first several bytes, terminate header search early if headers aren't a reasonable length. |
| 59 | + payload_sep_location = message.find(_PAYLOAD_SEPARATOR, 0, _TOTAL_REASONABLE_HEADER_BYTES) |
| 60 | + if payload_sep_location == -1: |
| 61 | + msg = 'Probable malformed message: no payload separator found in first expected bytes, .' |
| 62 | + raise IntersectApplicationError(msg) |
| 63 | + header_string = message[:payload_sep_location] |
| 64 | + headers = { |
| 65 | + key.decode(): value.decode() |
| 66 | + for key, value in ( |
| 67 | + header.split(_HEADER_KV_SEPARATOR) |
| 68 | + for header in header_string.split(_HEADER_VALUE_SEPARATOR) |
| 69 | + ) |
| 70 | + } |
| 71 | + try: |
| 72 | + content_type = headers.pop(CONTENT_TYPE_HEADER_KEY) |
| 73 | + except KeyError as e: |
| 74 | + msg = 'Probable malformed message: no content_type header found in message, discarding it.' |
| 75 | + raise IntersectApplicationError(msg) from e |
| 76 | + |
| 77 | + return message[payload_sep_location + 1 :], content_type, headers |
0 commit comments