Skip to content

Support never-indexed header fields. #203

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 21, 2016
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
7 changes: 6 additions & 1 deletion HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@ API Changes (Backward-Compatible)
to unicode. This defaults to UTF-8 for backward compatibility. To disable the
decode and use bytes exclusively, set the field to False, None, or the empty
string. This affects all headers, including those pushed by servers.
- Bumped the minimum version of HPACK allowed from 2.0 to 2.1.
- Bumped the minimum version of HPACK allowed from 2.0 to 2.2.
- Added support for advertising RFC 7838 Alternative services.
- Allowed users to provide ``hpack.HeaderTuple`` and
``hpack.NeverIndexedHeaderTuple`` objects to all methods that send headers.
- Changed all events that carry headers to emit ``hpack.HeaderTuple`` and
``hpack.NeverIndexedHeaderTuple`` instead of plain tuples. This allows users
to maintain header indexing state.

Bugfixes
~~~~~~~~
Expand Down
3 changes: 2 additions & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,5 +264,6 @@

# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {
'python': ('https://docs.python.org/3.5/', None)
'python': ('https://docs.python.org/3.5/', None),
'hpack': ('https://python-hyper.org/hpack/en/stable/', None),
}
24 changes: 22 additions & 2 deletions h2/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,13 @@ def send_headers(self, stream_id, headers, end_stream=False):
Clients may send one or two header blocks: one request block, and
optionally one trailer block.

If it is important to send HPACK "never indexed" header fields (as
defined in `RFC 7451 Section 7.1.3
<https://tools.ietf.org/html/rfc7541#section-7.1.3>`_), the user may
instead provide headers using the HPACK library's :class:`HeaderTuple
<hpack:hpack.HeaderTuple>` and :class:`NeverIndexedHeaderTuple
<hpack:hpack.NeverIndexedHeaderTuple>` objects.

.. warning:: In HTTP/2, it is mandatory that all the HTTP/2 special
headers (that is, ones whose header keys begin with ``:``) appear
at the start of the header block, before any normal headers.
Expand All @@ -571,11 +578,16 @@ def send_headers(self, stream_id, headers, end_stream=False):
may fail. For this reason, passing a ``dict`` to ``headers`` is
*deprecated*, and will be removed in 3.0.

.. versionchanged:: 2.3.0
Added support for using :class:`HeaderTuple
<hpack:hpack.HeaderTuple>` objects to store headers.

:param stream_id: The stream ID to send the headers on. If this stream
does not currently exist, it will be created.
:type stream_id: ``int``
:param headers: The request/response headers to send.
:type headers: An iterable of two tuples of bytestrings.
:type headers: An iterable of two tuples of bytestrings or
:class:`HeaderTuple <hpack:hpack.HeaderTuple>` objects.
:returns: Nothing
"""
# Check we can open the stream.
Expand Down Expand Up @@ -705,14 +717,22 @@ def push_stream(self, stream_id, promised_stream_id, request_headers):
"""
Push a response to the client by sending a PUSH_PROMISE frame.

If it is important to send HPACK "never indexed" header fields (as
defined in `RFC 7451 Section 7.1.3
<https://tools.ietf.org/html/rfc7541#section-7.1.3>`_), the user may
instead provide headers using the HPACK library's :class:`HeaderTuple
<hpack:hpack.HeaderTuple>` and :class:`NeverIndexedHeaderTuple
<hpack:hpack.NeverIndexedHeaderTuple>` objects.

:param stream_id: The ID of the stream that this push is a response to.
:type stream_id: ``int``
:param promised_stream_id: The ID of the stream that the pushed
response will be sent on.
:type promised_stream_id: ``int``
:param request_headers: The headers of the request that the pushed
response will be responding to.
:type request_headers: An iterable of two tuples of bytestrings.
:type request_headers: An iterable of two tuples of bytestrings or
:class:`HeaderTuple <hpack:hpack.HeaderTuple>` objects.
:returns: Nothing
"""
if not self.remote_settings.enable_push:
Expand Down
16 changes: 16 additions & 0 deletions h2/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ class RequestReceived(object):
The RequestReceived event is fired whenever request headers are received.
This event carries the HTTP headers for the given request and the stream ID
of the new stream.

.. versionchanged:: 2.3.0
Changed the type of ``headers`` to :class:`HeaderTuple
<hpack:hpack.HeaderTuple>`. This has no effect on current users.
"""
def __init__(self):
#: The Stream ID for the stream this request was made on.
Expand All @@ -38,6 +42,10 @@ class ResponseReceived(object):
The ResponseReceived event is fired whenever request headers are received.
This event carries the HTTP headers for the given response and the stream
ID of the new stream.

.. versionchanged:: 2.3.0
Changed the type of ``headers`` to :class:`HeaderTuple
<hpack:hpack.HeaderTuple>`. This has no effect on current users.
"""
def __init__(self):
#: The Stream ID for the stream this response was made on.
Expand All @@ -60,6 +68,10 @@ class TrailersReceived(object):
ahead of time (e.g. content-length). This event carries the HTTP header
fields that form the trailers and the stream ID of the stream on which they
were received.

.. versionchanged:: 2.3.0
Changed the type of ``headers`` to :class:`HeaderTuple
<hpack:hpack.HeaderTuple>`. This has no effect on current users.
"""
def __init__(self):
#: The Stream ID for the stream on which these trailers were received.
Expand Down Expand Up @@ -88,6 +100,10 @@ class InformationalResponseReceived(object):
1XX status code.

.. versionadded:: 2.2.0

.. versionchanged:: 2.3.0
Changed the type of ``headers`` to :class:`HeaderTuple
<hpack:hpack.HeaderTuple>`. This has no effect on current users.
"""
def __init__(self):
#: The Stream ID for the stream this informational response was made
Expand Down
44 changes: 35 additions & 9 deletions h2/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import warnings

from enum import Enum, IntEnum
from hpack import HeaderTuple
from hyperframe.frame import (
HeadersFrame, ContinuationFrame, DataFrame, WindowUpdateFrame,
RstStreamFrame, PushPromiseFrame, AltSvcFrame
Expand Down Expand Up @@ -844,10 +845,7 @@ def receive_push_promise_in_band(self,
events[0].pushed_stream_id = promised_stream_id

if header_encoding:
headers = [
(n.decode(header_encoding), v.decode(header_encoding))
for n, v in headers
]
headers = list(_decode_headers(headers, header_encoding))
events[0].headers = headers
return [], events

Expand Down Expand Up @@ -890,10 +888,7 @@ def receive_headers(self, headers, end_stream, header_encoding):
raise ProtocolError("Trailers must have END_STREAM set")

if header_encoding:
headers = [
(n.decode(header_encoding), v.decode(header_encoding))
for n, v in headers
]
headers = list(_decode_headers(headers, header_encoding))

events[0].headers = headers
return [], events
Expand Down Expand Up @@ -993,7 +988,8 @@ def _build_headers_frames(self,
"""
Helper method to build headers or push promise frames.
"""
headers = ((name.lower(), value) for name, value in headers)
# We need to lowercase the header names.
headers = _lowercase_header_names(headers)
encoded_headers = encoder.encode(headers)

# Slice into blocks of max_outbound_frame_size. Be careful with this:
Expand Down Expand Up @@ -1055,3 +1051,33 @@ def _track_content_length(self, length, end_stream):

if end_stream and expected != actual:
raise InvalidBodyLengthError(expected, actual)


def _lowercase_header_names(headers):
"""
Given an iterable of header two-tuples, rebuilds that iterable with the
header names lowercased. This generator produces tuples that preserve the
original type of the header tuple for tuple and any ``HeaderTuple``.
"""
for header in headers:
if isinstance(header, HeaderTuple):
yield header.__class__(header[0].lower(), header[1])
else:
yield (header[0].lower(), header[1])


def _decode_headers(headers, encoding):
"""
Given an iterable of header two-tuples and an encoding, decodes those
headers using that encoding while preserving the type of the header tuple.
This ensures that the use of ``HeaderTuple`` is preserved.
"""
for header in headers:
# This function expects to work on decoded headers, which are always
# HeaderTuple objects.
assert isinstance(header, HeaderTuple)

name, value = header
name = name.decode(encoding)
value = value.decode(encoding)
yield header.__class__(name, value)
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
],
install_requires=[
'hyperframe>=3.1, <5, !=4.0.0',
'hpack>=2.1, <3',
'hpack>=2.2, <3',
],
extras_require={
':python_version == "2.7" or python_version == "3.3"': ['enum34>=1.0.4, <2'],
Expand Down
Loading