diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/tools/third_party/hyperframe | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/tools/third_party/hyperframe')
14 files changed, 2355 insertions, 0 deletions
diff --git a/testing/web-platform/tests/tools/third_party/hyperframe/CONTRIBUTORS.rst b/testing/web-platform/tests/tools/third_party/hyperframe/CONTRIBUTORS.rst new file mode 100644 index 0000000000..aa7ab8b637 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/hyperframe/CONTRIBUTORS.rst @@ -0,0 +1,56 @@ +Hyper is written and maintained by Cory Benfield and various contributors: + +Development Lead +```````````````` + +- Cory Benfield <cory@lukasa.co.uk> + +Contributors +```````````` + +In chronological order: + +- Sriram Ganesan (@elricL) + + - Implemented the Huffman encoding/decoding logic. + +- Alek Storm (@alekstorm) + + - Implemented Python 2.7 support. + - Implemented HTTP/2 draft 10 support. + - Implemented server push. + +- Tetsuya Morimoto (@t2y) + + - Fixed a bug where large or incomplete frames were not handled correctly. + - Added hyper command-line tool. + - General code cleanups. + +- Jerome De Cuyper (@jdecuyper) + + - Updated documentation and tests. + +- Maximilian Hils (@mhils) + + - Added repr for frames. + - Improved frame initialization code. + - Added flag validation. + +- Thomas Kriechbaumer (@Kriechi) + + - Improved initialization code. + - Fixed bugs in frame initialization code. + - Improved frame repr for frames with non-printable bodies. + +- Davey Shafik (@dshafik) + + - Fixed Alt Svc frame stream association. + +- Seth Michael Larson (@SethMichaelLarson) + + - Performance improvements to serialization and parsing. + +- Fred Thomsen (@fredthomsen) + + - Support for memoryview in DataFrames. + diff --git a/testing/web-platform/tests/tools/third_party/hyperframe/HISTORY.rst b/testing/web-platform/tests/tools/third_party/hyperframe/HISTORY.rst new file mode 100644 index 0000000000..172b2b91ba --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/hyperframe/HISTORY.rst @@ -0,0 +1,179 @@ +Release History +=============== + +6.0.0dev0 +--------- + +5.2.0 (2019-01-18) +------------------ + +**API Changes (Backward-compatible)** + +- Add a new ENABLE_CONNECT_PROTOCOL settings paramter. + +**Other Changes** + +- Fix collections.abc deprecation. +- Drop support for Python 3.3 and support 3.7. + +5.1.0 (2017-04-24) +------------------ + +**API Changes (Backward-compatible)** + +- Added support for ``DataFrame.data`` being a ``memoryview`` object. + +5.0.0 (2017-03-07) +------------------ + +**Backwards Incompatible API Changes** + +- Added support for unknown extension frames. These will be returned in the new + ``ExtensionFrame`` object. The flag information for these frames is persisted + in ``flag_byte`` if needed. + +4.0.2 (2017-02-20) +------------------ + +**Bugfixes** + +- Fixed AltSvc stream association, which was incorrectly set to ``'both'``: + should have been ``'either'``. +- Fixed a bug where stream IDs on received frames were allowed to be 32-bit, + instead of 31-bit. +- Fixed a bug with frames that had the ``PADDING`` flag set but zero-length + padding, whose flow-controlled length was calculated wrongly. +- Miscellaneous performance improvements to serialization and parsing logic. + +4.0.1 (2016-03-13) +------------------ + +**Bugfixes** + +- Fixed bug with the repr of ``AltSvcFrame``, where building it could throw + exceptions if the frame had been received from the network. + +4.0.0 (2016-03-13) +------------------ + +**Backwards Incompatible API Changes** + +- Updated old ALTSVC frame definition to match the newly specified RFC 7838. +- Remove BLOCKED frame, which was never actually specified. +- Removed previously deprecated ``SettingsFrame.SETTINGS_MAX_FRAME_SIZE`` and + ``SettingsFrame.SETTINGS_MAX_HEADER_LIST_SIZE``. + +3.2.0 (2016-02-02) +------------------ + +**API Changes (Backward-compatible)** + +- Invalid PING frame bodies now raise ``InvalidFrameError``, not + ``ValueError``. Note that ``InvalidFrameError`` is a ``ValueError`` subclass. +- Invalid RST_STREAM frame bodies now raise ``InvalidFramError``, not + ``ValueError``. Note that ``InvalidFrameError`` is a ``ValueError`` subclass. +- Canonicalized the names of ``SettingsFrame.SETTINGS_MAX_FRAME_SIZE`` and + ``SettingsFrame.SETTINGS_MAX_HEADER_LIST_SIZE`` to match their peers, by + adding new properties ``SettingsFrame.MAX_FRAME_SIZE`` and + ``SettingsFrame.SETTINGS_MAX_HEADER_LIST_SIZE``. The old names are still + present, but will be deprecated in 4.0.0. + +**Bugfixes** + +- The change in ``3.1.0`` that ensured that ``InvalidFrameError`` would be + thrown did not affect certain invalid values in ALT_SVC frames. This has been + fixed: ``ValueError`` will no longer be thrown from invalid ALT_SVC bodies. + +3.1.1 (2016-01-18) +------------------ + +**Bugfixes** + +- Correctly error when receiving Ping frames that have insufficient data. + +3.1.0 (2016-01-13) +------------------ + +**API Changes** + +- Added new ``InvalidFrameError`` that is thrown instead of ``struct.error`` + when parsing a frame. + +**Bugfixes** + +- Fixed error when trying to serialize frames that use Priority information + with the defaults for that information. +- Fixed errors when displaying the repr of frames with non-printable bodies. + +3.0.1 (2016-01-08) +------------------ + +**Bugfixes** + +- Fix issue where unpadded DATA, PUSH_PROMISE and HEADERS frames that had empty + bodies would raise ``InvalidPaddingError`` exceptions when parsed. + +3.0.0 (2016-01-08) +------------------ + +**Backwards Incompatible API Changes** + +- Parsing padded frames that have invalid padding sizes now throws an + ``InvalidPaddingError``. + +2.2.0 (2015-10-15) +------------------ + +**API Changes** + +- When an unknown frame is encountered, ``parse_frame_header`` now throws a + ``ValueError`` subclass: ``UnknownFrameError``. This subclass contains the + frame type and the length of the frame body. + +2.1.0 (2015-10-06) +------------------ + +**API Changes** + +- Frames parsed from binary data now carry a ``body_len`` attribute that + matches the frame length (minus the frame header). + +2.0.0 (2015-09-21) +------------------ + +**API Changes** + +- Attempting to parse unrecognised frames now throws ``ValueError`` instead of + ``KeyError``. Thanks to @Kriechi! +- Flags are now validated for correctness, preventing setting flags that + ``hyperframe`` does not recognise and that would not serialize. Thanks to + @mhils! +- Frame properties can now be initialized in the constructors. Thanks to @mhils + and @Kriechi! +- Frames that cannot be sent on a stream now have their stream ID defaulted + to ``0``. Thanks to @Kriechi! + +**Other Changes** + +- Frames have a more useful repr. Thanks to @mhils! + +1.1.1 (2015-07-20) +------------------ + +- Fix a bug where ``FRAME_MAX_LEN`` was one byte too small. + +1.1.0 (2015-06-28) +------------------ + +- Add ``body_len`` property to frames to enable introspection of the actual + frame length. Thanks to @jdecuyper! + +1.0.1 (2015-06-27) +------------------ + +- Fix bug where the frame header would have an incorrect length added to it. + +1.0.0 (2015-04-12) +------------------ + +- Initial extraction from hyper. diff --git a/testing/web-platform/tests/tools/third_party/hyperframe/LICENSE b/testing/web-platform/tests/tools/third_party/hyperframe/LICENSE new file mode 100644 index 0000000000..d24c351e18 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/hyperframe/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Cory Benfield + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/testing/web-platform/tests/tools/third_party/hyperframe/MANIFEST.in b/testing/web-platform/tests/tools/third_party/hyperframe/MANIFEST.in new file mode 100644 index 0000000000..2f464676cb --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/hyperframe/MANIFEST.in @@ -0,0 +1,2 @@ +include README.rst LICENSE CONTRIBUTORS.rst HISTORY.rst + diff --git a/testing/web-platform/tests/tools/third_party/hyperframe/PKG-INFO b/testing/web-platform/tests/tools/third_party/hyperframe/PKG-INFO new file mode 100644 index 0000000000..cfd53f7e93 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/hyperframe/PKG-INFO @@ -0,0 +1,242 @@ +Metadata-Version: 1.1 +Name: hyperframe +Version: 5.2.0 +Summary: HTTP/2 framing layer for Python +Home-page: https://python-hyper.org/hyperframe/en/latest/ +Author: Cory Benfield +Author-email: cory@lukasa.co.uk +License: MIT License +Description: ====================================== + hyperframe: Pure-Python HTTP/2 framing + ====================================== + + .. image:: https://travis-ci.org/python-hyper/hyperframe.png?branch=master + :target: https://travis-ci.org/python-hyper/hyperframe + + This library contains the HTTP/2 framing code used in the `hyper`_ project. It + provides a pure-Python codebase that is capable of decoding a binary stream + into HTTP/2 frames. + + This library is used directly by `hyper`_ and a number of other projects to + provide HTTP/2 frame decoding logic. + + Contributing + ============ + + hyperframe welcomes contributions from anyone! Unlike many other projects we + are happy to accept cosmetic contributions and small contributions, in addition + to large feature requests and changes. + + Before you contribute (either by opening an issue or filing a pull request), + please `read the contribution guidelines`_. + + .. _read the contribution guidelines: http://hyper.readthedocs.org/en/development/contributing.html + + License + ======= + + hyperframe is made available under the MIT License. For more details, see the + ``LICENSE`` file in the repository. + + Authors + ======= + + hyperframe is maintained by Cory Benfield, with contributions from others. For + more details about the contributors, please see ``CONTRIBUTORS.rst``. + + .. _hyper: http://python-hyper.org/ + + + Release History + =============== + + 6.0.0dev0 + --------- + + 5.2.0 (2019-01-18) + ------------------ + + **API Changes (Backward-compatible)** + + - Add a new ENABLE_CONNECT_PROTOCOL settings paramter. + + **Other Changes** + + - Fix collections.abc deprecation. + - Drop support for Python 3.3 and support 3.7. + + 5.1.0 (2017-04-24) + ------------------ + + **API Changes (Backward-compatible)** + + - Added support for ``DataFrame.data`` being a ``memoryview`` object. + + 5.0.0 (2017-03-07) + ------------------ + + **Backwards Incompatible API Changes** + + - Added support for unknown extension frames. These will be returned in the new + ``ExtensionFrame`` object. The flag information for these frames is persisted + in ``flag_byte`` if needed. + + 4.0.2 (2017-02-20) + ------------------ + + **Bugfixes** + + - Fixed AltSvc stream association, which was incorrectly set to ``'both'``: + should have been ``'either'``. + - Fixed a bug where stream IDs on received frames were allowed to be 32-bit, + instead of 31-bit. + - Fixed a bug with frames that had the ``PADDING`` flag set but zero-length + padding, whose flow-controlled length was calculated wrongly. + - Miscellaneous performance improvements to serialization and parsing logic. + + 4.0.1 (2016-03-13) + ------------------ + + **Bugfixes** + + - Fixed bug with the repr of ``AltSvcFrame``, where building it could throw + exceptions if the frame had been received from the network. + + 4.0.0 (2016-03-13) + ------------------ + + **Backwards Incompatible API Changes** + + - Updated old ALTSVC frame definition to match the newly specified RFC 7838. + - Remove BLOCKED frame, which was never actually specified. + - Removed previously deprecated ``SettingsFrame.SETTINGS_MAX_FRAME_SIZE`` and + ``SettingsFrame.SETTINGS_MAX_HEADER_LIST_SIZE``. + + 3.2.0 (2016-02-02) + ------------------ + + **API Changes (Backward-compatible)** + + - Invalid PING frame bodies now raise ``InvalidFrameError``, not + ``ValueError``. Note that ``InvalidFrameError`` is a ``ValueError`` subclass. + - Invalid RST_STREAM frame bodies now raise ``InvalidFramError``, not + ``ValueError``. Note that ``InvalidFrameError`` is a ``ValueError`` subclass. + - Canonicalized the names of ``SettingsFrame.SETTINGS_MAX_FRAME_SIZE`` and + ``SettingsFrame.SETTINGS_MAX_HEADER_LIST_SIZE`` to match their peers, by + adding new properties ``SettingsFrame.MAX_FRAME_SIZE`` and + ``SettingsFrame.SETTINGS_MAX_HEADER_LIST_SIZE``. The old names are still + present, but will be deprecated in 4.0.0. + + **Bugfixes** + + - The change in ``3.1.0`` that ensured that ``InvalidFrameError`` would be + thrown did not affect certain invalid values in ALT_SVC frames. This has been + fixed: ``ValueError`` will no longer be thrown from invalid ALT_SVC bodies. + + 3.1.1 (2016-01-18) + ------------------ + + **Bugfixes** + + - Correctly error when receiving Ping frames that have insufficient data. + + 3.1.0 (2016-01-13) + ------------------ + + **API Changes** + + - Added new ``InvalidFrameError`` that is thrown instead of ``struct.error`` + when parsing a frame. + + **Bugfixes** + + - Fixed error when trying to serialize frames that use Priority information + with the defaults for that information. + - Fixed errors when displaying the repr of frames with non-printable bodies. + + 3.0.1 (2016-01-08) + ------------------ + + **Bugfixes** + + - Fix issue where unpadded DATA, PUSH_PROMISE and HEADERS frames that had empty + bodies would raise ``InvalidPaddingError`` exceptions when parsed. + + 3.0.0 (2016-01-08) + ------------------ + + **Backwards Incompatible API Changes** + + - Parsing padded frames that have invalid padding sizes now throws an + ``InvalidPaddingError``. + + 2.2.0 (2015-10-15) + ------------------ + + **API Changes** + + - When an unknown frame is encountered, ``parse_frame_header`` now throws a + ``ValueError`` subclass: ``UnknownFrameError``. This subclass contains the + frame type and the length of the frame body. + + 2.1.0 (2015-10-06) + ------------------ + + **API Changes** + + - Frames parsed from binary data now carry a ``body_len`` attribute that + matches the frame length (minus the frame header). + + 2.0.0 (2015-09-21) + ------------------ + + **API Changes** + + - Attempting to parse unrecognised frames now throws ``ValueError`` instead of + ``KeyError``. Thanks to @Kriechi! + - Flags are now validated for correctness, preventing setting flags that + ``hyperframe`` does not recognise and that would not serialize. Thanks to + @mhils! + - Frame properties can now be initialized in the constructors. Thanks to @mhils + and @Kriechi! + - Frames that cannot be sent on a stream now have their stream ID defaulted + to ``0``. Thanks to @Kriechi! + + **Other Changes** + + - Frames have a more useful repr. Thanks to @mhils! + + 1.1.1 (2015-07-20) + ------------------ + + - Fix a bug where ``FRAME_MAX_LEN`` was one byte too small. + + 1.1.0 (2015-06-28) + ------------------ + + - Add ``body_len`` property to frames to enable introspection of the actual + frame length. Thanks to @jdecuyper! + + 1.0.1 (2015-06-27) + ------------------ + + - Fix bug where the frame header would have an incorrect length added to it. + + 1.0.0 (2015-04-12) + ------------------ + + - Initial extraction from hyper. + +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: Implementation :: CPython diff --git a/testing/web-platform/tests/tools/third_party/hyperframe/README.rst b/testing/web-platform/tests/tools/third_party/hyperframe/README.rst new file mode 100644 index 0000000000..385b39af9b --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/hyperframe/README.rst @@ -0,0 +1,39 @@ +====================================== +hyperframe: Pure-Python HTTP/2 framing +====================================== + +.. image:: https://travis-ci.org/python-hyper/hyperframe.png?branch=master + :target: https://travis-ci.org/python-hyper/hyperframe + +This library contains the HTTP/2 framing code used in the `hyper`_ project. It +provides a pure-Python codebase that is capable of decoding a binary stream +into HTTP/2 frames. + +This library is used directly by `hyper`_ and a number of other projects to +provide HTTP/2 frame decoding logic. + +Contributing +============ + +hyperframe welcomes contributions from anyone! Unlike many other projects we +are happy to accept cosmetic contributions and small contributions, in addition +to large feature requests and changes. + +Before you contribute (either by opening an issue or filing a pull request), +please `read the contribution guidelines`_. + +.. _read the contribution guidelines: http://hyper.readthedocs.org/en/development/contributing.html + +License +======= + +hyperframe is made available under the MIT License. For more details, see the +``LICENSE`` file in the repository. + +Authors +======= + +hyperframe is maintained by Cory Benfield, with contributions from others. For +more details about the contributors, please see ``CONTRIBUTORS.rst``. + +.. _hyper: http://python-hyper.org/ diff --git a/testing/web-platform/tests/tools/third_party/hyperframe/hyperframe/__init__.py b/testing/web-platform/tests/tools/third_party/hyperframe/hyperframe/__init__.py new file mode 100644 index 0000000000..7620b4bdf7 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/hyperframe/hyperframe/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +""" +hyperframe +~~~~~~~~~~ + +A module for providing a pure-Python HTTP/2 framing layer. +""" +__version__ = '5.2.0' diff --git a/testing/web-platform/tests/tools/third_party/hyperframe/hyperframe/exceptions.py b/testing/web-platform/tests/tools/third_party/hyperframe/hyperframe/exceptions.py new file mode 100644 index 0000000000..dd30369c70 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/hyperframe/hyperframe/exceptions.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +""" +hyperframe/exceptions +~~~~~~~~~~~~~~~~~~~~~ + +Defines the exceptions that can be thrown by hyperframe. +""" + + +class UnknownFrameError(ValueError): + """ + An frame of unknown type was received. + """ + def __init__(self, frame_type, length): + #: The type byte of the unknown frame that was received. + self.frame_type = frame_type + + #: The length of the data portion of the unknown frame. + self.length = length + + def __str__(self): + return ( + "UnknownFrameError: Unknown frame type 0x%X received, " + "length %d bytes" % (self.frame_type, self.length) + ) + + +class InvalidPaddingError(ValueError): + """ + A frame with invalid padding was received. + """ + pass + + +class InvalidFrameError(ValueError): + """ + Parsing a frame failed because the data was not laid out appropriately. + + .. versionadded:: 3.0.2 + """ + pass diff --git a/testing/web-platform/tests/tools/third_party/hyperframe/hyperframe/flags.py b/testing/web-platform/tests/tools/third_party/hyperframe/hyperframe/flags.py new file mode 100644 index 0000000000..1660bd1800 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/hyperframe/hyperframe/flags.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +""" +hyperframe/flags +~~~~~~~~~~~~~~~~ + +Defines basic Flag and Flags data structures. +""" +import collections + +try: + from collections.abc import MutableSet +except ImportError: # pragma: no cover + # Python 2.7 compatibility + from collections import MutableSet + +Flag = collections.namedtuple("Flag", ["name", "bit"]) + + +class Flags(MutableSet): + """ + A simple MutableSet implementation that will only accept known flags as + elements. + + Will behave like a regular set(), except that a ValueError will be thrown + when .add()ing unexpected flags. + """ + def __init__(self, defined_flags): + self._valid_flags = set(flag.name for flag in defined_flags) + self._flags = set() + + def __contains__(self, x): + return self._flags.__contains__(x) + + def __iter__(self): + return self._flags.__iter__() + + def __len__(self): + return self._flags.__len__() + + def discard(self, value): + return self._flags.discard(value) + + def add(self, value): + if value not in self._valid_flags: + raise ValueError( + "Unexpected flag: {}. Valid flags are: {}".format( + value, self._valid_flags + ) + ) + return self._flags.add(value) diff --git a/testing/web-platform/tests/tools/third_party/hyperframe/hyperframe/frame.py b/testing/web-platform/tests/tools/third_party/hyperframe/hyperframe/frame.py new file mode 100644 index 0000000000..795057279b --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/hyperframe/hyperframe/frame.py @@ -0,0 +1,822 @@ +# -*- coding: utf-8 -*- +""" +hyperframe/frame +~~~~~~~~~~~~~~~~ + +Defines framing logic for HTTP/2. Provides both classes to represent framed +data and logic for aiding the connection when it comes to reading from the +socket. +""" +import struct +import binascii + +from .exceptions import ( + UnknownFrameError, InvalidPaddingError, InvalidFrameError +) +from .flags import Flag, Flags + + +# The maximum initial length of a frame. Some frames have shorter maximum +# lengths. +FRAME_MAX_LEN = (2 ** 14) + +# The maximum allowed length of a frame. +FRAME_MAX_ALLOWED_LEN = (2 ** 24) - 1 + +# Stream association enumerations. +_STREAM_ASSOC_HAS_STREAM = "has-stream" +_STREAM_ASSOC_NO_STREAM = "no-stream" +_STREAM_ASSOC_EITHER = "either" + +# Structs for packing and unpacking +_STRUCT_HBBBL = struct.Struct(">HBBBL") +_STRUCT_LL = struct.Struct(">LL") +_STRUCT_HL = struct.Struct(">HL") +_STRUCT_LB = struct.Struct(">LB") +_STRUCT_L = struct.Struct(">L") +_STRUCT_H = struct.Struct(">H") +_STRUCT_B = struct.Struct(">B") + + +class Frame(object): + """ + The base class for all HTTP/2 frames. + """ + #: The flags defined on this type of frame. + defined_flags = [] + + #: The byte used to define the type of the frame. + type = None + + # If 'has-stream', the frame's stream_id must be non-zero. If 'no-stream', + # it must be zero. If 'either', it's not checked. + stream_association = None + + def __init__(self, stream_id, flags=()): + #: The stream identifier for the stream this frame was received on. + #: Set to 0 for frames sent on the connection (stream-id 0). + self.stream_id = stream_id + + #: The flags set for this frame. + self.flags = Flags(self.defined_flags) + + #: The frame length, excluding the nine-byte header. + self.body_len = 0 + + for flag in flags: + self.flags.add(flag) + + if (not self.stream_id and + self.stream_association == _STREAM_ASSOC_HAS_STREAM): + raise ValueError('Stream ID must be non-zero') + if (self.stream_id and + self.stream_association == _STREAM_ASSOC_NO_STREAM): + raise ValueError('Stream ID must be zero') + + def __repr__(self): + flags = ", ".join(self.flags) or "None" + body = binascii.hexlify(self.serialize_body()).decode('ascii') + if len(body) > 20: + body = body[:20] + "..." + return ( + "{type}(Stream: {stream}; Flags: {flags}): {body}" + ).format( + type=type(self).__name__, + stream=self.stream_id, + flags=flags, + body=body + ) + + @staticmethod + def parse_frame_header(header, strict=False): + """ + Takes a 9-byte frame header and returns a tuple of the appropriate + Frame object and the length that needs to be read from the socket. + + This populates the flags field, and determines how long the body is. + + :param strict: Whether to raise an exception when encountering a frame + not defined by spec and implemented by hyperframe. + + :raises hyperframe.exceptions.UnknownFrameError: If a frame of unknown + type is received. + + .. versionchanged:: 5.0.0 + Added :param:`strict` to accommodate :class:`ExtensionFrame` + """ + try: + fields = _STRUCT_HBBBL.unpack(header) + except struct.error: + raise InvalidFrameError("Invalid frame header") + + # First 24 bits are frame length. + length = (fields[0] << 8) + fields[1] + type = fields[2] + flags = fields[3] + stream_id = fields[4] & 0x7FFFFFFF + + try: + frame = FRAMES[type](stream_id) + except KeyError: + if strict: + raise UnknownFrameError(type, length) + frame = ExtensionFrame(type=type, stream_id=stream_id) + + frame.parse_flags(flags) + return (frame, length) + + def parse_flags(self, flag_byte): + for flag, flag_bit in self.defined_flags: + if flag_byte & flag_bit: + self.flags.add(flag) + + return self.flags + + def serialize(self): + """ + Convert a frame into a bytestring, representing the serialized form of + the frame. + """ + body = self.serialize_body() + self.body_len = len(body) + + # Build the common frame header. + # First, get the flags. + flags = 0 + + for flag, flag_bit in self.defined_flags: + if flag in self.flags: + flags |= flag_bit + + header = _STRUCT_HBBBL.pack( + (self.body_len >> 8) & 0xFFFF, # Length spread over top 24 bits + self.body_len & 0xFF, + self.type, + flags, + self.stream_id & 0x7FFFFFFF # Stream ID is 32 bits. + ) + + return header + body + + def serialize_body(self): + raise NotImplementedError() + + def parse_body(self, data): + """ + Given the body of a frame, parses it into frame data. This populates + the non-header parts of the frame: that is, it does not populate the + stream ID or flags. + + :param data: A memoryview object containing the body data of the frame. + Must not contain *more* data than the length returned by + :meth:`parse_frame_header + <hyperframe.frame.Frame.parse_frame_header>`. + """ + raise NotImplementedError() + + +class Padding(object): + """ + Mixin for frames that contain padding. Defines extra fields that can be + used and set by frames that can be padded. + """ + def __init__(self, stream_id, pad_length=0, **kwargs): + super(Padding, self).__init__(stream_id, **kwargs) + + #: The length of the padding to use. + self.pad_length = pad_length + + def serialize_padding_data(self): + if 'PADDED' in self.flags: + return _STRUCT_B.pack(self.pad_length) + return b'' + + def parse_padding_data(self, data): + if 'PADDED' in self.flags: + try: + self.pad_length = struct.unpack('!B', data[:1])[0] + except struct.error: + raise InvalidFrameError("Invalid Padding data") + return 1 + return 0 + + @property + def total_padding(self): + return self.pad_length + + +class Priority(object): + """ + Mixin for frames that contain priority data. Defines extra fields that can + be used and set by frames that contain priority data. + """ + def __init__(self, + stream_id, + depends_on=0x0, + stream_weight=0x0, + exclusive=False, + **kwargs): + super(Priority, self).__init__(stream_id, **kwargs) + + #: The stream ID of the stream on which this stream depends. + self.depends_on = depends_on + + #: The weight of the stream. This is an integer between 0 and 256. + self.stream_weight = stream_weight + + #: Whether the exclusive bit was set. + self.exclusive = exclusive + + def serialize_priority_data(self): + return _STRUCT_LB.pack( + self.depends_on + (0x80000000 if self.exclusive else 0), + self.stream_weight + ) + + def parse_priority_data(self, data): + try: + self.depends_on, self.stream_weight = _STRUCT_LB.unpack(data[:5]) + except struct.error: + raise InvalidFrameError("Invalid Priority data") + + self.exclusive = True if self.depends_on >> 31 else False + self.depends_on &= 0x7FFFFFFF + return 5 + + +class DataFrame(Padding, Frame): + """ + DATA frames convey arbitrary, variable-length sequences of octets + associated with a stream. One or more DATA frames are used, for instance, + to carry HTTP request or response payloads. + """ + #: The flags defined for DATA frames. + defined_flags = [ + Flag('END_STREAM', 0x01), + Flag('PADDED', 0x08), + ] + + #: The type byte for data frames. + type = 0x0 + + stream_association = _STREAM_ASSOC_HAS_STREAM + + def __init__(self, stream_id, data=b'', **kwargs): + super(DataFrame, self).__init__(stream_id, **kwargs) + + #: The data contained on this frame. + self.data = data + + def serialize_body(self): + padding_data = self.serialize_padding_data() + padding = b'\0' * self.total_padding + if isinstance(self.data, memoryview): + self.data = self.data.tobytes() + return b''.join([padding_data, self.data, padding]) + + def parse_body(self, data): + padding_data_length = self.parse_padding_data(data) + self.data = ( + data[padding_data_length:len(data)-self.total_padding].tobytes() + ) + self.body_len = len(data) + + if self.total_padding and self.total_padding >= self.body_len: + raise InvalidPaddingError("Padding is too long.") + + @property + def flow_controlled_length(self): + """ + The length of the frame that needs to be accounted for when considering + flow control. + """ + padding_len = 0 + if 'PADDED' in self.flags: + # Account for extra 1-byte padding length field, which is still + # present if possibly zero-valued. + padding_len = self.total_padding + 1 + return len(self.data) + padding_len + + +class PriorityFrame(Priority, Frame): + """ + The PRIORITY frame specifies the sender-advised priority of a stream. It + can be sent at any time for an existing stream. This enables + reprioritisation of existing streams. + """ + #: The flags defined for PRIORITY frames. + defined_flags = [] + + #: The type byte defined for PRIORITY frames. + type = 0x02 + + stream_association = _STREAM_ASSOC_HAS_STREAM + + def serialize_body(self): + return self.serialize_priority_data() + + def parse_body(self, data): + self.parse_priority_data(data) + self.body_len = len(data) + + +class RstStreamFrame(Frame): + """ + The RST_STREAM frame allows for abnormal termination of a stream. When sent + by the initiator of a stream, it indicates that they wish to cancel the + stream or that an error condition has occurred. When sent by the receiver + of a stream, it indicates that either the receiver is rejecting the stream, + requesting that the stream be cancelled or that an error condition has + occurred. + """ + #: The flags defined for RST_STREAM frames. + defined_flags = [] + + #: The type byte defined for RST_STREAM frames. + type = 0x03 + + stream_association = _STREAM_ASSOC_HAS_STREAM + + def __init__(self, stream_id, error_code=0, **kwargs): + super(RstStreamFrame, self).__init__(stream_id, **kwargs) + + #: The error code used when resetting the stream. + self.error_code = error_code + + def serialize_body(self): + return _STRUCT_L.pack(self.error_code) + + def parse_body(self, data): + if len(data) != 4: + raise InvalidFrameError( + "RST_STREAM must have 4 byte body: actual length %s." % + len(data) + ) + + try: + self.error_code = _STRUCT_L.unpack(data)[0] + except struct.error: # pragma: no cover + raise InvalidFrameError("Invalid RST_STREAM body") + + self.body_len = 4 + + +class SettingsFrame(Frame): + """ + The SETTINGS frame conveys configuration parameters that affect how + endpoints communicate. The parameters are either constraints on peer + behavior or preferences. + + Settings are not negotiated. Settings describe characteristics of the + sending peer, which are used by the receiving peer. Different values for + the same setting can be advertised by each peer. For example, a client + might set a high initial flow control window, whereas a server might set a + lower value to conserve resources. + """ + #: The flags defined for SETTINGS frames. + defined_flags = [Flag('ACK', 0x01)] + + #: The type byte defined for SETTINGS frames. + type = 0x04 + + stream_association = _STREAM_ASSOC_NO_STREAM + + # We need to define the known settings, they may as well be class + # attributes. + #: The byte that signals the SETTINGS_HEADER_TABLE_SIZE setting. + HEADER_TABLE_SIZE = 0x01 + #: The byte that signals the SETTINGS_ENABLE_PUSH setting. + ENABLE_PUSH = 0x02 + #: The byte that signals the SETTINGS_MAX_CONCURRENT_STREAMS setting. + MAX_CONCURRENT_STREAMS = 0x03 + #: The byte that signals the SETTINGS_INITIAL_WINDOW_SIZE setting. + INITIAL_WINDOW_SIZE = 0x04 + #: The byte that signals the SETTINGS_MAX_FRAME_SIZE setting. + MAX_FRAME_SIZE = 0x05 + #: The byte that signals the SETTINGS_MAX_HEADER_LIST_SIZE setting. + MAX_HEADER_LIST_SIZE = 0x06 + #: The byte that signals SETTINGS_ENABLE_CONNECT_PROTOCOL setting. + ENABLE_CONNECT_PROTOCOL = 0x08 + + def __init__(self, stream_id=0, settings=None, **kwargs): + super(SettingsFrame, self).__init__(stream_id, **kwargs) + + if settings and "ACK" in kwargs.get("flags", ()): + raise ValueError("Settings must be empty if ACK flag is set.") + + #: A dictionary of the setting type byte to the value of the setting. + self.settings = settings or {} + + def serialize_body(self): + return b''.join([_STRUCT_HL.pack(setting & 0xFF, value) + for setting, value in self.settings.items()]) + + def parse_body(self, data): + body_len = 0 + for i in range(0, len(data), 6): + try: + name, value = _STRUCT_HL.unpack(data[i:i+6]) + except struct.error: + raise InvalidFrameError("Invalid SETTINGS body") + + self.settings[name] = value + body_len += 6 + + self.body_len = body_len + + +class PushPromiseFrame(Padding, Frame): + """ + The PUSH_PROMISE frame is used to notify the peer endpoint in advance of + streams the sender intends to initiate. + """ + #: The flags defined for PUSH_PROMISE frames. + defined_flags = [ + Flag('END_HEADERS', 0x04), + Flag('PADDED', 0x08) + ] + + #: The type byte defined for PUSH_PROMISE frames. + type = 0x05 + + stream_association = _STREAM_ASSOC_HAS_STREAM + + def __init__(self, stream_id, promised_stream_id=0, data=b'', **kwargs): + super(PushPromiseFrame, self).__init__(stream_id, **kwargs) + + #: The stream ID that is promised by this frame. + self.promised_stream_id = promised_stream_id + + #: The HPACK-encoded header block for the simulated request on the new + #: stream. + self.data = data + + def serialize_body(self): + padding_data = self.serialize_padding_data() + padding = b'\0' * self.total_padding + data = _STRUCT_L.pack(self.promised_stream_id) + return b''.join([padding_data, data, self.data, padding]) + + def parse_body(self, data): + padding_data_length = self.parse_padding_data(data) + + try: + self.promised_stream_id = _STRUCT_L.unpack( + data[padding_data_length:padding_data_length + 4] + )[0] + except struct.error: + raise InvalidFrameError("Invalid PUSH_PROMISE body") + + self.data = data[padding_data_length + 4:].tobytes() + self.body_len = len(data) + + if self.total_padding and self.total_padding >= self.body_len: + raise InvalidPaddingError("Padding is too long.") + + +class PingFrame(Frame): + """ + The PING frame is a mechanism for measuring a minimal round-trip time from + the sender, as well as determining whether an idle connection is still + functional. PING frames can be sent from any endpoint. + """ + #: The flags defined for PING frames. + defined_flags = [Flag('ACK', 0x01)] + + #: The type byte defined for PING frames. + type = 0x06 + + stream_association = _STREAM_ASSOC_NO_STREAM + + def __init__(self, stream_id=0, opaque_data=b'', **kwargs): + super(PingFrame, self).__init__(stream_id, **kwargs) + + #: The opaque data sent in this PING frame, as a bytestring. + self.opaque_data = opaque_data + + def serialize_body(self): + if len(self.opaque_data) > 8: + raise InvalidFrameError( + "PING frame may not have more than 8 bytes of data, got %s" % + self.opaque_data + ) + + data = self.opaque_data + data += b'\x00' * (8 - len(self.opaque_data)) + return data + + def parse_body(self, data): + if len(data) != 8: + raise InvalidFrameError( + "PING frame must have 8 byte length: got %s" % len(data) + ) + + self.opaque_data = data.tobytes() + self.body_len = 8 + + +class GoAwayFrame(Frame): + """ + The GOAWAY frame informs the remote peer to stop creating streams on this + connection. It can be sent from the client or the server. Once sent, the + sender will ignore frames sent on new streams for the remainder of the + connection. + """ + #: The flags defined for GOAWAY frames. + defined_flags = [] + + #: The type byte defined for GOAWAY frames. + type = 0x07 + + stream_association = _STREAM_ASSOC_NO_STREAM + + def __init__(self, + stream_id=0, + last_stream_id=0, + error_code=0, + additional_data=b'', + **kwargs): + super(GoAwayFrame, self).__init__(stream_id, **kwargs) + + #: The last stream ID definitely seen by the remote peer. + self.last_stream_id = last_stream_id + + #: The error code for connection teardown. + self.error_code = error_code + + #: Any additional data sent in the GOAWAY. + self.additional_data = additional_data + + def serialize_body(self): + data = _STRUCT_LL.pack( + self.last_stream_id & 0x7FFFFFFF, + self.error_code + ) + data += self.additional_data + + return data + + def parse_body(self, data): + try: + self.last_stream_id, self.error_code = _STRUCT_LL.unpack( + data[:8] + ) + except struct.error: + raise InvalidFrameError("Invalid GOAWAY body.") + + self.body_len = len(data) + + if len(data) > 8: + self.additional_data = data[8:].tobytes() + + +class WindowUpdateFrame(Frame): + """ + The WINDOW_UPDATE frame is used to implement flow control. + + Flow control operates at two levels: on each individual stream and on the + entire connection. + + Both types of flow control are hop by hop; that is, only between the two + endpoints. Intermediaries do not forward WINDOW_UPDATE frames between + dependent connections. However, throttling of data transfer by any receiver + can indirectly cause the propagation of flow control information toward the + original sender. + """ + #: The flags defined for WINDOW_UPDATE frames. + defined_flags = [] + + #: The type byte defined for WINDOW_UPDATE frames. + type = 0x08 + + stream_association = _STREAM_ASSOC_EITHER + + def __init__(self, stream_id, window_increment=0, **kwargs): + super(WindowUpdateFrame, self).__init__(stream_id, **kwargs) + + #: The amount the flow control window is to be incremented. + self.window_increment = window_increment + + def serialize_body(self): + return _STRUCT_L.pack(self.window_increment & 0x7FFFFFFF) + + def parse_body(self, data): + try: + self.window_increment = _STRUCT_L.unpack(data)[0] + except struct.error: + raise InvalidFrameError("Invalid WINDOW_UPDATE body") + + self.body_len = 4 + + +class HeadersFrame(Padding, Priority, Frame): + """ + The HEADERS frame carries name-value pairs. It is used to open a stream. + HEADERS frames can be sent on a stream in the "open" or "half closed + (remote)" states. + + The HeadersFrame class is actually basically a data frame in this + implementation, because of the requirement to control the sizes of frames. + A header block fragment that doesn't fit in an entire HEADERS frame needs + to be followed with CONTINUATION frames. From the perspective of the frame + building code the header block is an opaque data segment. + """ + #: The flags defined for HEADERS frames. + defined_flags = [ + Flag('END_STREAM', 0x01), + Flag('END_HEADERS', 0x04), + Flag('PADDED', 0x08), + Flag('PRIORITY', 0x20), + ] + + #: The type byte defined for HEADERS frames. + type = 0x01 + + stream_association = _STREAM_ASSOC_HAS_STREAM + + def __init__(self, stream_id, data=b'', **kwargs): + super(HeadersFrame, self).__init__(stream_id, **kwargs) + + #: The HPACK-encoded header block. + self.data = data + + def serialize_body(self): + padding_data = self.serialize_padding_data() + padding = b'\0' * self.total_padding + + if 'PRIORITY' in self.flags: + priority_data = self.serialize_priority_data() + else: + priority_data = b'' + + return b''.join([padding_data, priority_data, self.data, padding]) + + def parse_body(self, data): + padding_data_length = self.parse_padding_data(data) + data = data[padding_data_length:] + + if 'PRIORITY' in self.flags: + priority_data_length = self.parse_priority_data(data) + else: + priority_data_length = 0 + + self.body_len = len(data) + self.data = ( + data[priority_data_length:len(data)-self.total_padding].tobytes() + ) + + if self.total_padding and self.total_padding >= self.body_len: + raise InvalidPaddingError("Padding is too long.") + + +class ContinuationFrame(Frame): + """ + The CONTINUATION frame is used to continue a sequence of header block + fragments. Any number of CONTINUATION frames can be sent on an existing + stream, as long as the preceding frame on the same stream is one of + HEADERS, PUSH_PROMISE or CONTINUATION without the END_HEADERS flag set. + + Much like the HEADERS frame, hyper treats this as an opaque data frame with + different flags and a different type. + """ + #: The flags defined for CONTINUATION frames. + defined_flags = [Flag('END_HEADERS', 0x04)] + + #: The type byte defined for CONTINUATION frames. + type = 0x09 + + stream_association = _STREAM_ASSOC_HAS_STREAM + + def __init__(self, stream_id, data=b'', **kwargs): + super(ContinuationFrame, self).__init__(stream_id, **kwargs) + + #: The HPACK-encoded header block. + self.data = data + + def serialize_body(self): + return self.data + + def parse_body(self, data): + self.data = data.tobytes() + self.body_len = len(data) + + +class AltSvcFrame(Frame): + """ + The ALTSVC frame is used to advertise alternate services that the current + host, or a different one, can understand. This frame is standardised as + part of RFC 7838. + + This frame does no work to validate that the ALTSVC field parameter is + acceptable per the rules of RFC 7838. + + .. note:: If the ``stream_id`` of this frame is nonzero, the origin field + must have zero length. Conversely, if the ``stream_id`` of this + frame is zero, the origin field must have nonzero length. Put + another way, a valid ALTSVC frame has ``stream_id != 0`` XOR + ``len(origin) != 0``. + """ + type = 0xA + + stream_association = _STREAM_ASSOC_EITHER + + def __init__(self, stream_id, origin=b'', field=b'', **kwargs): + super(AltSvcFrame, self).__init__(stream_id, **kwargs) + + if not isinstance(origin, bytes): + raise ValueError("AltSvc origin must be bytestring.") + if not isinstance(field, bytes): + raise ValueError("AltSvc field must be a bytestring.") + self.origin = origin + self.field = field + + def serialize_body(self): + origin_len = _STRUCT_H.pack(len(self.origin)) + return b''.join([origin_len, self.origin, self.field]) + + def parse_body(self, data): + try: + origin_len = _STRUCT_H.unpack(data[0:2])[0] + self.origin = data[2:2+origin_len].tobytes() + + if len(self.origin) != origin_len: + raise InvalidFrameError("Invalid ALTSVC frame body.") + + self.field = data[2+origin_len:].tobytes() + except (struct.error, ValueError): + raise InvalidFrameError("Invalid ALTSVC frame body.") + + self.body_len = len(data) + + +class ExtensionFrame(Frame): + """ + ExtensionFrame is used to wrap frames which are not natively interpretable + by hyperframe. + + Although certain byte prefixes are ordained by specification to have + certain contextual meanings, frames with other prefixes are not prohibited, + and may be used to communicate arbitrary meaning between HTTP/2 peers. + + Thus, hyperframe, rather than raising an exception when such a frame is + encountered, wraps it in a generic frame to be properly acted upon by + upstream consumers which might have additional context on how to use it. + + .. versionadded:: 5.0.0 + """ + + stream_association = _STREAM_ASSOC_EITHER + + def __init__(self, type, stream_id, **kwargs): + super(ExtensionFrame, self).__init__(stream_id, **kwargs) + self.type = type + self.flag_byte = None + + def parse_flags(self, flag_byte): + """ + For extension frames, we parse the flags by just storing a flag byte. + """ + self.flag_byte = flag_byte + + def parse_body(self, data): + self.body = data.tobytes() + self.body_len = len(data) + + def serialize(self): + """ + A broad override of the serialize method that ensures that the data + comes back out exactly as it came in. This should not be used in most + user code: it exists only as a helper method if frames need to be + reconstituted. + """ + # Build the frame header. + # First, get the flags. + flags = self.flag_byte + + header = _STRUCT_HBBBL.pack( + (self.body_len >> 8) & 0xFFFF, # Length spread over top 24 bits + self.body_len & 0xFF, + self.type, + flags, + self.stream_id & 0x7FFFFFFF # Stream ID is 32 bits. + ) + + return header + self.body + + +_FRAME_CLASSES = [ + DataFrame, + HeadersFrame, + PriorityFrame, + RstStreamFrame, + SettingsFrame, + PushPromiseFrame, + PingFrame, + GoAwayFrame, + WindowUpdateFrame, + ContinuationFrame, + AltSvcFrame, +] +#: FRAMES maps the type byte for each frame to the class used to represent that +#: frame. +FRAMES = {cls.type: cls for cls in _FRAME_CLASSES} diff --git a/testing/web-platform/tests/tools/third_party/hyperframe/setup.cfg b/testing/web-platform/tests/tools/third_party/hyperframe/setup.cfg new file mode 100644 index 0000000000..50220e135b --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/hyperframe/setup.cfg @@ -0,0 +1,10 @@ +[wheel] +universal = 1 + +[tool:pytest] +testpaths = test + +[egg_info] +tag_build = +tag_date = 0 + diff --git a/testing/web-platform/tests/tools/third_party/hyperframe/setup.py b/testing/web-platform/tests/tools/third_party/hyperframe/setup.py new file mode 100644 index 0000000000..1ab7212141 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/hyperframe/setup.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import itertools +import os +import re +import sys + +try: + from setuptools import setup +except ImportError: + from distutils.core import setup + +# Get the version +version_regex = r'__version__ = ["\']([^"\']*)["\']' +with open('hyperframe/__init__.py', 'r') as f: + text = f.read() + match = re.search(version_regex, text) + + if match: + version = match.group(1) + else: + raise RuntimeError("No version number found!") + +# Stealing this from Kenneth Reitz +if sys.argv[-1] == 'publish': + os.system('python setup.py sdist upload') + sys.exit() + + +packages = ['hyperframe'] + +setup( + name='hyperframe', + version=version, + description='HTTP/2 framing layer for Python', + long_description=open('README.rst').read() + '\n\n' + open('HISTORY.rst').read(), + author='Cory Benfield', + author_email='cory@lukasa.co.uk', + url='https://python-hyper.org/hyperframe/en/latest/', + packages=packages, + package_data={'': ['LICENSE', 'README.rst', 'CONTRIBUTORS.rst', 'HISTORY.rst']}, + package_dir={'hyperframe': 'hyperframe'}, + include_package_data=True, + license='MIT License', + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: Implementation :: CPython', + ], +) diff --git a/testing/web-platform/tests/tools/third_party/hyperframe/test/test_flags.py b/testing/web-platform/tests/tools/third_party/hyperframe/test/test_flags.py new file mode 100644 index 0000000000..62a6a30f67 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/hyperframe/test/test_flags.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +from hyperframe.frame import ( + Flags, Flag, +) +import pytest + + +class TestFlags(object): + def test_add(self): + flags = Flags([Flag("VALID_FLAG", 0x00)]) + assert not flags + + flags.add("VALID_FLAG") + flags.add("VALID_FLAG") + assert "VALID_FLAG" in flags + assert list(flags) == ["VALID_FLAG"] + assert len(flags) == 1 + + def test_remove(self): + flags = Flags([Flag("VALID_FLAG", 0x00)]) + flags.add("VALID_FLAG") + + flags.discard("VALID_FLAG") + assert "VALID_FLAG" not in flags + assert list(flags) == [] + assert len(flags) == 0 + + # discarding elements not in the set should not throw an exception + flags.discard("END_STREAM") + + def test_validation(self): + flags = Flags([Flag("VALID_FLAG", 0x00)]) + flags.add("VALID_FLAG") + with pytest.raises(ValueError): + flags.add("INVALID_FLAG") diff --git a/testing/web-platform/tests/tools/third_party/hyperframe/test/test_frames.py b/testing/web-platform/tests/tools/third_party/hyperframe/test/test_frames.py new file mode 100644 index 0000000000..abfecb692d --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/hyperframe/test/test_frames.py @@ -0,0 +1,791 @@ +# -*- coding: utf-8 -*- +from hyperframe.frame import ( + Frame, Flags, DataFrame, PriorityFrame, RstStreamFrame, SettingsFrame, + PushPromiseFrame, PingFrame, GoAwayFrame, WindowUpdateFrame, HeadersFrame, + ContinuationFrame, AltSvcFrame, ExtensionFrame +) +from hyperframe.exceptions import ( + UnknownFrameError, InvalidPaddingError, InvalidFrameError +) +import pytest + + +def decode_frame(frame_data): + f, length = Frame.parse_frame_header(frame_data[:9]) + f.parse_body(memoryview(frame_data[9:9 + length])) + assert 9 + length == len(frame_data) + return f + + +class TestGeneralFrameBehaviour(object): + def test_base_frame_ignores_flags(self): + f = Frame(stream_id=0) + flags = f.parse_flags(0xFF) + assert not flags + assert isinstance(flags, Flags) + + def test_base_frame_cant_serialize(self): + f = Frame(stream_id=0) + with pytest.raises(NotImplementedError): + f.serialize() + + def test_base_frame_cant_parse_body(self): + data = b'' + f = Frame(stream_id=0) + with pytest.raises(NotImplementedError): + f.parse_body(data) + + def test_parse_frame_header_unknown_type_strict(self): + with pytest.raises(UnknownFrameError) as excinfo: + Frame.parse_frame_header( + b'\x00\x00\x59\xFF\x00\x00\x00\x00\x01', + strict=True + ) + exception = excinfo.value + assert exception.frame_type == 0xFF + assert exception.length == 0x59 + assert str(exception) == ( + "UnknownFrameError: Unknown frame type 0xFF received, " + "length 89 bytes" + ) + + def test_parse_frame_header_ignore_first_bit_of_stream_id(self): + s = b'\x00\x00\x00\x06\x01\x80\x00\x00\x00' + f, _ = Frame.parse_frame_header(s) + + assert f.stream_id == 0 + + def test_parse_frame_header_unknown_type(self): + frame, length = Frame.parse_frame_header( + b'\x00\x00\x59\xFF\x00\x00\x00\x00\x01' + ) + assert frame.type == 0xFF + assert length == 0x59 + assert isinstance(frame, ExtensionFrame) + assert frame.stream_id == 1 + + def test_flags_are_persisted(self): + frame, length = Frame.parse_frame_header( + b'\x00\x00\x59\xFF\x09\x00\x00\x00\x01' + ) + assert frame.type == 0xFF + assert length == 0x59 + assert frame.flag_byte == 0x09 + + def test_parse_body_unknown_type(self): + frame = decode_frame( + b'\x00\x00\x0C\xFF\x00\x00\x00\x00\x01hello world!' + ) + assert frame.body == b'hello world!' + assert frame.body_len == 12 + assert frame.stream_id == 1 + + def test_can_round_trip_unknown_frames(self): + frame_data = b'\x00\x00\x0C\xFF\x00\x00\x00\x00\x01hello world!' + f = decode_frame(frame_data) + assert f.serialize() == frame_data + + def test_repr(self, monkeypatch): + f = Frame(stream_id=0) + monkeypatch.setattr(Frame, "serialize_body", lambda _: b"body") + assert repr(f) == "Frame(Stream: 0; Flags: None): 626f6479" + + monkeypatch.setattr(Frame, "serialize_body", lambda _: b"A"*25) + assert repr(f) == ( + "Frame(Stream: 0; Flags: None): {}...".format("41"*10) + ) + + def test_cannot_parse_invalid_frame_header(self): + with pytest.raises(InvalidFrameError): + Frame.parse_frame_header(b'\x00\x00\x08\x00\x01\x00\x00\x00') + + +class TestDataFrame(object): + payload = b'\x00\x00\x08\x00\x01\x00\x00\x00\x01testdata' + payload_with_padding = ( + b'\x00\x00\x13\x00\x09\x00\x00\x00\x01\x0Atestdata' + b'\0' * 10 + ) + + def test_data_frame_has_correct_flags(self): + f = DataFrame(1) + flags = f.parse_flags(0xFF) + assert flags == set([ + 'END_STREAM', 'PADDED' + ]) + + @pytest.mark.parametrize('data', [ + b'testdata', + memoryview(b'testdata') + ]) + def test_data_frame_serializes_properly(self, data): + f = DataFrame(1) + f.flags = set(['END_STREAM']) + f.data = data + + s = f.serialize() + assert s == self.payload + + def test_data_frame_with_padding_serializes_properly(self): + f = DataFrame(1) + f.flags = set(['END_STREAM', 'PADDED']) + f.data = b'testdata' + f.pad_length = 10 + + s = f.serialize() + assert s == self.payload_with_padding + + def test_data_frame_parses_properly(self): + f = decode_frame(self.payload) + + assert isinstance(f, DataFrame) + assert f.flags == set(['END_STREAM']) + assert f.pad_length == 0 + assert f.data == b'testdata' + assert f.body_len == 8 + + def test_data_frame_with_padding_parses_properly(self): + f = decode_frame(self.payload_with_padding) + + assert isinstance(f, DataFrame) + assert f.flags == set(['END_STREAM', 'PADDED']) + assert f.pad_length == 10 + assert f.data == b'testdata' + assert f.body_len == 19 + + def test_data_frame_with_invalid_padding_errors(self): + with pytest.raises(InvalidFrameError): + decode_frame(self.payload_with_padding[:9]) + + def test_data_frame_with_padding_calculates_flow_control_len(self): + f = DataFrame(1) + f.flags = set(['PADDED']) + f.data = b'testdata' + f.pad_length = 10 + + assert f.flow_controlled_length == 19 + + def test_data_frame_zero_length_padding_calculates_flow_control_len(self): + f = DataFrame(1) + f.flags = set(['PADDED']) + f.data = b'testdata' + f.pad_length = 0 + + assert f.flow_controlled_length == len(b'testdata') + 1 + + def test_data_frame_without_padding_calculates_flow_control_len(self): + f = DataFrame(1) + f.data = b'testdata' + + assert f.flow_controlled_length == 8 + + def test_data_frame_comes_on_a_stream(self): + with pytest.raises(ValueError): + DataFrame(0) + + def test_long_data_frame(self): + f = DataFrame(1) + + # Use more than 256 bytes of data to force setting higher bits. + f.data = b'\x01' * 300 + data = f.serialize() + + # The top three bytes should be numerically equal to 300. That means + # they should read 00 01 2C. + # The weird double index trick is to ensure this test behaves equally + # on Python 2 and Python 3. + assert data[0] == b'\x00'[0] + assert data[1] == b'\x01'[0] + assert data[2] == b'\x2C'[0] + + def test_body_length_behaves_correctly(self): + f = DataFrame(1) + + f.data = b'\x01' * 300 + + # Initially the body length is zero. For now this is incidental, but + # I'm going to test it to ensure that the behaviour is codified. We + # should change this test if we change that. + assert f.body_len == 0 + + f.serialize() + assert f.body_len == 300 + + def test_data_frame_with_invalid_padding_fails_to_parse(self): + # This frame has a padding length of 6 bytes, but a total length of + # only 5. + data = b'\x00\x00\x05\x00\x0b\x00\x00\x00\x01\x06\x54\x65\x73\x74' + + with pytest.raises(InvalidPaddingError): + decode_frame(data) + + def test_data_frame_with_no_length_parses(self): + # Fixes issue with empty data frames raising InvalidPaddingError. + f = DataFrame(1) + f.data = b'' + data = f.serialize() + + new_frame = decode_frame(data) + assert new_frame.data == b'' + + +class TestPriorityFrame(object): + payload = b'\x00\x00\x05\x02\x00\x00\x00\x00\x01\x80\x00\x00\x04\x40' + + def test_priority_frame_has_no_flags(self): + f = PriorityFrame(1) + flags = f.parse_flags(0xFF) + assert flags == set() + assert isinstance(flags, Flags) + + def test_priority_frame_default_serializes_properly(self): + f = PriorityFrame(1) + + assert f.serialize() == ( + b'\x00\x00\x05\x02\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00' + ) + + def test_priority_frame_with_all_data_serializes_properly(self): + f = PriorityFrame(1) + f.depends_on = 0x04 + f.stream_weight = 64 + f.exclusive = True + + assert f.serialize() == self.payload + + def test_priority_frame_with_all_data_parses_properly(self): + f = decode_frame(self.payload) + + assert isinstance(f, PriorityFrame) + assert f.flags == set() + assert f.depends_on == 4 + assert f.stream_weight == 64 + assert f.exclusive is True + assert f.body_len == 5 + + def test_priority_frame_comes_on_a_stream(self): + with pytest.raises(ValueError): + PriorityFrame(0) + + def test_short_priority_frame_errors(self): + with pytest.raises(InvalidFrameError): + decode_frame(self.payload[:-2]) + + +class TestRstStreamFrame(object): + def test_rst_stream_frame_has_no_flags(self): + f = RstStreamFrame(1) + flags = f.parse_flags(0xFF) + assert not flags + assert isinstance(flags, Flags) + + def test_rst_stream_frame_serializes_properly(self): + f = RstStreamFrame(1) + f.error_code = 420 + + s = f.serialize() + assert s == b'\x00\x00\x04\x03\x00\x00\x00\x00\x01\x00\x00\x01\xa4' + + def test_rst_stream_frame_parses_properly(self): + s = b'\x00\x00\x04\x03\x00\x00\x00\x00\x01\x00\x00\x01\xa4' + f = decode_frame(s) + + assert isinstance(f, RstStreamFrame) + assert f.flags == set() + assert f.error_code == 420 + assert f.body_len == 4 + + def test_rst_stream_frame_comes_on_a_stream(self): + with pytest.raises(ValueError): + RstStreamFrame(0) + + def test_rst_stream_frame_must_have_body_length_four(self): + f = RstStreamFrame(1) + with pytest.raises(ValueError): + f.parse_body(b'\x01') + + +class TestSettingsFrame(object): + serialized = ( + b'\x00\x00\x2A\x04\x01\x00\x00\x00\x00' + # Frame header + b'\x00\x01\x00\x00\x10\x00' + # HEADER_TABLE_SIZE + b'\x00\x02\x00\x00\x00\x00' + # ENABLE_PUSH + b'\x00\x03\x00\x00\x00\x64' + # MAX_CONCURRENT_STREAMS + b'\x00\x04\x00\x00\xFF\xFF' + # INITIAL_WINDOW_SIZE + b'\x00\x05\x00\x00\x40\x00' + # MAX_FRAME_SIZE + b'\x00\x06\x00\x00\xFF\xFF' + # MAX_HEADER_LIST_SIZE + b'\x00\x08\x00\x00\x00\x01' # ENABLE_CONNECT_PROTOCOL + ) + + settings = { + SettingsFrame.HEADER_TABLE_SIZE: 4096, + SettingsFrame.ENABLE_PUSH: 0, + SettingsFrame.MAX_CONCURRENT_STREAMS: 100, + SettingsFrame.INITIAL_WINDOW_SIZE: 65535, + SettingsFrame.MAX_FRAME_SIZE: 16384, + SettingsFrame.MAX_HEADER_LIST_SIZE: 65535, + SettingsFrame.ENABLE_CONNECT_PROTOCOL: 1, + } + + def test_settings_frame_has_only_one_flag(self): + f = SettingsFrame() + flags = f.parse_flags(0xFF) + assert flags == set(['ACK']) + + def test_settings_frame_serializes_properly(self): + f = SettingsFrame() + f.parse_flags(0xFF) + f.settings = self.settings + + s = f.serialize() + assert s == self.serialized + + def test_settings_frame_with_settings(self): + f = SettingsFrame(settings=self.settings) + assert f.settings == self.settings + + def test_settings_frame_without_settings(self): + f = SettingsFrame() + assert f.settings == {} + + def test_settings_frame_with_ack(self): + f = SettingsFrame(flags=('ACK',)) + assert 'ACK' in f.flags + + def test_settings_frame_ack_and_settings(self): + with pytest.raises(ValueError): + SettingsFrame(settings=self.settings, flags=('ACK',)) + + def test_settings_frame_parses_properly(self): + f = decode_frame(self.serialized) + + assert isinstance(f, SettingsFrame) + assert f.flags == set(['ACK']) + assert f.settings == self.settings + assert f.body_len == 42 + + def test_settings_frames_never_have_streams(self): + with pytest.raises(ValueError): + SettingsFrame(stream_id=1) + + def test_short_settings_frame_errors(self): + with pytest.raises(InvalidFrameError): + decode_frame(self.serialized[:-2]) + + +class TestPushPromiseFrame(object): + def test_push_promise_frame_flags(self): + f = PushPromiseFrame(1) + flags = f.parse_flags(0xFF) + + assert flags == set(['END_HEADERS', 'PADDED']) + + def test_push_promise_frame_serializes_properly(self): + f = PushPromiseFrame(1) + f.flags = set(['END_HEADERS']) + f.promised_stream_id = 4 + f.data = b'hello world' + + s = f.serialize() + assert s == ( + b'\x00\x00\x0F\x05\x04\x00\x00\x00\x01' + + b'\x00\x00\x00\x04' + + b'hello world' + ) + + def test_push_promise_frame_parses_properly(self): + s = ( + b'\x00\x00\x0F\x05\x04\x00\x00\x00\x01' + + b'\x00\x00\x00\x04' + + b'hello world' + ) + f = decode_frame(s) + + assert isinstance(f, PushPromiseFrame) + assert f.flags == set(['END_HEADERS']) + assert f.promised_stream_id == 4 + assert f.data == b'hello world' + assert f.body_len == 15 + + def test_push_promise_frame_with_invalid_padding_fails_to_parse(self): + # This frame has a padding length of 6 bytes, but a total length of + # only 5. + data = b'\x00\x00\x05\x05\x08\x00\x00\x00\x01\x06\x54\x65\x73\x74' + + with pytest.raises(InvalidPaddingError): + decode_frame(data) + + def test_push_promise_frame_with_no_length_parses(self): + # Fixes issue with empty data frames raising InvalidPaddingError. + f = PushPromiseFrame(1) + f.data = b'' + data = f.serialize() + + new_frame = decode_frame(data) + assert new_frame.data == b'' + + def test_short_push_promise_errors(self): + s = ( + b'\x00\x00\x0F\x05\x04\x00\x00\x00\x01' + + b'\x00\x00\x00' # One byte short + ) + + with pytest.raises(InvalidFrameError): + decode_frame(s) + + +class TestPingFrame(object): + def test_ping_frame_has_only_one_flag(self): + f = PingFrame() + flags = f.parse_flags(0xFF) + + assert flags == set(['ACK']) + + def test_ping_frame_serializes_properly(self): + f = PingFrame() + f.parse_flags(0xFF) + f.opaque_data = b'\x01\x02' + + s = f.serialize() + assert s == ( + b'\x00\x00\x08\x06\x01\x00\x00\x00\x00\x01\x02\x00\x00\x00\x00\x00' + b'\x00' + ) + + def test_no_more_than_8_octets(self): + f = PingFrame() + f.opaque_data = b'\x01\x02\x03\x04\x05\x06\x07\x08\x09' + + with pytest.raises(ValueError): + f.serialize() + + def test_ping_frame_parses_properly(self): + s = ( + b'\x00\x00\x08\x06\x01\x00\x00\x00\x00\x01\x02\x00\x00\x00\x00\x00' + b'\x00' + ) + f = decode_frame(s) + + assert isinstance(f, PingFrame) + assert f.flags == set(['ACK']) + assert f.opaque_data == b'\x01\x02\x00\x00\x00\x00\x00\x00' + assert f.body_len == 8 + + def test_ping_frame_never_has_a_stream(self): + with pytest.raises(ValueError): + PingFrame(stream_id=1) + + def test_ping_frame_has_no_more_than_body_length_8(self): + f = PingFrame() + with pytest.raises(ValueError): + f.parse_body(b'\x01\x02\x03\x04\x05\x06\x07\x08\x09') + + def test_ping_frame_has_no_less_than_body_length_8(self): + f = PingFrame() + with pytest.raises(ValueError): + f.parse_body(b'\x01\x02\x03\x04\x05\x06\x07') + + +class TestGoAwayFrame(object): + def test_go_away_has_no_flags(self): + f = GoAwayFrame() + flags = f.parse_flags(0xFF) + + assert not flags + assert isinstance(flags, Flags) + + def test_goaway_serializes_properly(self): + f = GoAwayFrame() + f.last_stream_id = 64 + f.error_code = 32 + f.additional_data = b'hello' + + s = f.serialize() + assert s == ( + b'\x00\x00\x0D\x07\x00\x00\x00\x00\x00' + # Frame header + b'\x00\x00\x00\x40' + # Last Stream ID + b'\x00\x00\x00\x20' + # Error Code + b'hello' # Additional data + ) + + def test_goaway_frame_parses_properly(self): + s = ( + b'\x00\x00\x0D\x07\x00\x00\x00\x00\x00' + # Frame header + b'\x00\x00\x00\x40' + # Last Stream ID + b'\x00\x00\x00\x20' + # Error Code + b'hello' # Additional data + ) + f = decode_frame(s) + + assert isinstance(f, GoAwayFrame) + assert f.flags == set() + assert f.additional_data == b'hello' + assert f.body_len == 13 + + s = ( + b'\x00\x00\x08\x07\x00\x00\x00\x00\x00' + # Frame header + b'\x00\x00\x00\x40' + # Last Stream ID + b'\x00\x00\x00\x20' + # Error Code + b'' # Additional data + ) + f = decode_frame(s) + + assert isinstance(f, GoAwayFrame) + assert f.flags == set() + assert f.additional_data == b'' + assert f.body_len == 8 + + def test_goaway_frame_never_has_a_stream(self): + with pytest.raises(ValueError): + GoAwayFrame(stream_id=1) + + def test_short_goaway_frame_errors(self): + s = ( + b'\x00\x00\x0D\x07\x00\x00\x00\x00\x00' + # Frame header + b'\x00\x00\x00\x40' + # Last Stream ID + b'\x00\x00\x00' # short Error Code + ) + with pytest.raises(InvalidFrameError): + decode_frame(s) + + +class TestWindowUpdateFrame(object): + def test_window_update_has_no_flags(self): + f = WindowUpdateFrame(0) + flags = f.parse_flags(0xFF) + + assert not flags + assert isinstance(flags, Flags) + + def test_window_update_serializes_properly(self): + f = WindowUpdateFrame(0) + f.window_increment = 512 + + s = f.serialize() + assert s == b'\x00\x00\x04\x08\x00\x00\x00\x00\x00\x00\x00\x02\x00' + + def test_windowupdate_frame_parses_properly(self): + s = b'\x00\x00\x04\x08\x00\x00\x00\x00\x00\x00\x00\x02\x00' + f = decode_frame(s) + + assert isinstance(f, WindowUpdateFrame) + assert f.flags == set() + assert f.window_increment == 512 + assert f.body_len == 4 + + def test_short_windowupdate_frame_errors(self): + s = b'\x00\x00\x04\x08\x00\x00\x00\x00\x00\x00\x00\x02' # -1 byte + + with pytest.raises(InvalidFrameError): + decode_frame(s) + + +class TestHeadersFrame(object): + def test_headers_frame_flags(self): + f = HeadersFrame(1) + flags = f.parse_flags(0xFF) + + assert flags == set(['END_STREAM', 'END_HEADERS', + 'PADDED', 'PRIORITY']) + + def test_headers_frame_serializes_properly(self): + f = HeadersFrame(1) + f.flags = set(['END_STREAM', 'END_HEADERS']) + f.data = b'hello world' + + s = f.serialize() + assert s == ( + b'\x00\x00\x0B\x01\x05\x00\x00\x00\x01' + + b'hello world' + ) + + def test_headers_frame_parses_properly(self): + s = ( + b'\x00\x00\x0B\x01\x05\x00\x00\x00\x01' + + b'hello world' + ) + f = decode_frame(s) + + assert isinstance(f, HeadersFrame) + assert f.flags == set(['END_STREAM', 'END_HEADERS']) + assert f.data == b'hello world' + assert f.body_len == 11 + + def test_headers_frame_with_priority_parses_properly(self): + # This test also tests that we can receive a HEADERS frame with no + # actual headers on it. This is technically possible. + s = ( + b'\x00\x00\x05\x01\x20\x00\x00\x00\x01' + + b'\x80\x00\x00\x04\x40' + ) + f = decode_frame(s) + + assert isinstance(f, HeadersFrame) + assert f.flags == set(['PRIORITY']) + assert f.data == b'' + assert f.depends_on == 4 + assert f.stream_weight == 64 + assert f.exclusive is True + assert f.body_len == 5 + + def test_headers_frame_with_priority_serializes_properly(self): + # This test also tests that we can receive a HEADERS frame with no + # actual headers on it. This is technically possible. + s = ( + b'\x00\x00\x05\x01\x20\x00\x00\x00\x01' + + b'\x80\x00\x00\x04\x40' + ) + f = HeadersFrame(1) + f.flags = set(['PRIORITY']) + f.data = b'' + f.depends_on = 4 + f.stream_weight = 64 + f.exclusive = True + + assert f.serialize() == s + + def test_headers_frame_with_invalid_padding_fails_to_parse(self): + # This frame has a padding length of 6 bytes, but a total length of + # only 5. + data = b'\x00\x00\x05\x01\x08\x00\x00\x00\x01\x06\x54\x65\x73\x74' + + with pytest.raises(InvalidPaddingError): + decode_frame(data) + + def test_headers_frame_with_no_length_parses(self): + # Fixes issue with empty data frames raising InvalidPaddingError. + f = HeadersFrame(1) + f.data = b'' + data = f.serialize() + + new_frame = decode_frame(data) + assert new_frame.data == b'' + + +class TestContinuationFrame(object): + def test_continuation_frame_flags(self): + f = ContinuationFrame(1) + flags = f.parse_flags(0xFF) + + assert flags == set(['END_HEADERS']) + + def test_continuation_frame_serializes(self): + f = ContinuationFrame(1) + f.parse_flags(0x04) + f.data = b'hello world' + + s = f.serialize() + assert s == ( + b'\x00\x00\x0B\x09\x04\x00\x00\x00\x01' + + b'hello world' + ) + + def test_continuation_frame_parses_properly(self): + s = b'\x00\x00\x0B\x09\x04\x00\x00\x00\x01hello world' + f = decode_frame(s) + + assert isinstance(f, ContinuationFrame) + assert f.flags == set(['END_HEADERS']) + assert f.data == b'hello world' + assert f.body_len == 11 + + +class TestAltSvcFrame(object): + payload_with_origin = ( + b'\x00\x00\x31' # Length + b'\x0A' # Type + b'\x00' # Flags + b'\x00\x00\x00\x00' # Stream ID + b'\x00\x0B' # Origin len + b'example.com' # Origin + b'h2="alt.example.com:8000", h2=":443"' # Field Value + ) + payload_without_origin = ( + b'\x00\x00\x13' # Length + b'\x0A' # Type + b'\x00' # Flags + b'\x00\x00\x00\x01' # Stream ID + b'\x00\x00' # Origin len + b'' # Origin + b'h2=":8000"; ma=60' # Field Value + ) + payload_with_origin_and_stream = ( + b'\x00\x00\x36' # Length + b'\x0A' # Type + b'\x00' # Flags + b'\x00\x00\x00\x01' # Stream ID + b'\x00\x0B' # Origin len + b'example.com' # Origin + b'Alt-Svc: h2=":443"; ma=2592000; persist=1' # Field Value + ) + + def test_altsvc_frame_flags(self): + f = AltSvcFrame(stream_id=0) + flags = f.parse_flags(0xFF) + + assert flags == set() + + def test_altsvc_frame_with_origin_serializes_properly(self): + f = AltSvcFrame(stream_id=0) + f.origin = b'example.com' + f.field = b'h2="alt.example.com:8000", h2=":443"' + + s = f.serialize() + assert s == self.payload_with_origin + + def test_altsvc_frame_with_origin_parses_properly(self): + f = decode_frame(self.payload_with_origin) + + assert isinstance(f, AltSvcFrame) + assert f.origin == b'example.com' + assert f.field == b'h2="alt.example.com:8000", h2=":443"' + assert f.body_len == 49 + assert f.stream_id == 0 + + def test_altsvc_frame_without_origin_serializes_properly(self): + f = AltSvcFrame(stream_id=1, origin=b'', field=b'h2=":8000"; ma=60') + s = f.serialize() + assert s == self.payload_without_origin + + def test_altsvc_frame_without_origin_parses_properly(self): + f = decode_frame(self.payload_without_origin) + + assert isinstance(f, AltSvcFrame) + assert f.origin == b'' + assert f.field == b'h2=":8000"; ma=60' + assert f.body_len == 19 + assert f.stream_id == 1 + + def test_altsvc_frame_without_origin_parses_with_good_repr(self): + f = decode_frame(self.payload_without_origin) + + assert repr(f) == ( + "AltSvcFrame(Stream: 1; Flags: None): 000068323d223a383030..." + ) + + def test_altsvc_frame_with_origin_and_stream_serializes_properly(self): + # This frame is not valid, but we allow it to be serialized anyway. + f = AltSvcFrame(stream_id=1) + f.origin = b'example.com' + f.field = b'Alt-Svc: h2=":443"; ma=2592000; persist=1' + + assert f.serialize() == self.payload_with_origin_and_stream + + def test_short_altsvc_frame_errors(self): + with pytest.raises(InvalidFrameError): + decode_frame(self.payload_with_origin[:12]) + + with pytest.raises(InvalidFrameError): + decode_frame(self.payload_with_origin[:10]) + + def test_altsvc_with_unicode_origin_fails(self): + with pytest.raises(ValueError): + AltSvcFrame( + stream_id=0, origin=u'hello', field=b'h2=":8000"; ma=60' + + ) + + def test_altsvc_with_unicode_field_fails(self): + with pytest.raises(ValueError): + AltSvcFrame( + stream_id=0, origin=b'hello', field=u'h2=":8000"; ma=60' + ) |