Skip to content

Commit 0d336ab

Browse files
authored
Merge pull request #468 from apolcyn/add_send_with_padding_option
Allow sending data frames with padding
2 parents c8c2ed2 + 03d121f commit 0d336ab

File tree

5 files changed

+186
-11
lines changed

5 files changed

+186
-11
lines changed

HISTORY.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ API Changes (Backward-Compatible)
2222
a subclass of ``int``, this is non-breaking.
2323
- Deprecated the other fields in ``h2.settings``. These will be removed in
2424
3.0.0.
25+
- Added an optional ``pad_length`` parameter to ``H2Connection.send_data``
26+
to allow the user to include padding on a data frame.
27+
2528

2629
Bugfixes
2730
~~~~~~~~

h2/connection.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -833,7 +833,7 @@ def send_headers(self, stream_id, headers, end_stream=False,
833833

834834
self._prepare_for_sending(frames)
835835

836-
def send_data(self, stream_id, data, end_stream=False):
836+
def send_data(self, stream_id, data, end_stream=False, pad_length=None):
837837
"""
838838
Send data on a given stream.
839839
@@ -857,24 +857,44 @@ def send_data(self, stream_id, data, end_stream=False):
857857
:param end_stream: (optional) Whether this is the last data to be sent
858858
on the stream. Defaults to ``False``.
859859
:type end_stream: ``bool``
860+
:param pad_length: (optional) Length of the padding to apply to the
861+
data frame. Defaults to ``None`` for no use of padding. Note that
862+
a value of ``0`` results in padding of length ``0``
863+
(with the "padding" flag set on the frame).
864+
865+
.. versionadded:: 2.6.0
866+
867+
:type pad_length: ``int``
860868
:returns: Nothing
861869
"""
862-
if len(data) > self.local_flow_control_window(stream_id):
870+
frame_size = len(data)
871+
if pad_length is not None:
872+
if not isinstance(pad_length, int):
873+
raise TypeError("pad_length must be an int")
874+
if pad_length < 0 or pad_length > 255:
875+
raise ValueError("pad_length must be within range: [0, 255]")
876+
# Account for padding bytes plus the 1-byte padding length field.
877+
frame_size += pad_length + 1
878+
879+
if frame_size > self.local_flow_control_window(stream_id):
863880
raise FlowControlError(
864881
"Cannot send %d bytes, flow control window is %d." %
865-
(len(data), self.local_flow_control_window(stream_id))
882+
(frame_size, self.local_flow_control_window(stream_id))
866883
)
867-
elif len(data) > self.max_outbound_frame_size:
884+
elif frame_size > self.max_outbound_frame_size:
868885
raise FrameTooLargeError(
869886
"Cannot send frame size %d, max frame size is %d" %
870-
(len(data), self.max_outbound_frame_size)
887+
(frame_size, self.max_outbound_frame_size)
871888
)
872889

873890
self.state_machine.process_input(ConnectionInputs.SEND_DATA)
874-
frames = self.streams[stream_id].send_data(data, end_stream)
891+
frames = self.streams[stream_id].send_data(
892+
data, end_stream, pad_length=pad_length
893+
)
894+
875895
self._prepare_for_sending(frames)
876896

877-
self.outbound_flow_control_window -= len(data)
897+
self.outbound_flow_control_window -= frame_size
878898
assert self.outbound_flow_control_window >= 0
879899

880900
def end_stream(self, stream_id):

h2/stream.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -841,7 +841,7 @@ def locally_pushed(self):
841841
assert not events
842842
return []
843843

844-
def send_data(self, data, end_stream=False):
844+
def send_data(self, data, end_stream=False, pad_length=None):
845845
"""
846846
Prepare some data frames. Optionally end the stream.
847847
@@ -854,8 +854,12 @@ def send_data(self, data, end_stream=False):
854854
if end_stream:
855855
self.state_machine.process_input(StreamInputs.SEND_END_STREAM)
856856
df.flags.add('END_STREAM')
857+
if pad_length is not None:
858+
df.flags.add('PADDED')
859+
df.pad_length = pad_length
857860

858-
self.outbound_flow_control_window -= len(data)
861+
# Subtract flow_controlled_length to account for possible padding
862+
self.outbound_flow_control_window -= df.flow_controlled_length
859863
assert self.outbound_flow_control_window >= 0
860864

861865
return [df]

test/test_basic_logic.py

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,117 @@ def test_sending_data(self):
109109
c.clear_outbound_data_buffer()
110110
events = c.send_data(1, b'some data')
111111
assert not events
112+
data_to_send = c.data_to_send()
112113
assert (
113-
c.data_to_send() == b'\x00\x00\t\x00\x00\x00\x00\x00\x01some data'
114+
data_to_send == b'\x00\x00\t\x00\x00\x00\x00\x00\x01some data'
114115
)
115116

117+
buffer = h2.frame_buffer.FrameBuffer(server=False)
118+
buffer.max_frame_size = 65535
119+
buffer.add_data(data_to_send)
120+
data_frame = list(buffer)[0]
121+
sanity_check_data_frame(
122+
data_frame=data_frame,
123+
expected_flow_controlled_length=len(b'some data'),
124+
expect_padded_flag=False,
125+
expected_data_frame_pad_length=0
126+
)
127+
128+
def test_sending_data_with_padding(self):
129+
"""
130+
Single data frames with padding are encoded correctly.
131+
"""
132+
c = h2.connection.H2Connection()
133+
c.initiate_connection()
134+
c.send_headers(1, self.example_request_headers)
135+
136+
# Clear the data, then send some data.
137+
c.clear_outbound_data_buffer()
138+
events = c.send_data(1, b'some data', pad_length=5)
139+
assert not events
140+
data_to_send = c.data_to_send()
141+
assert data_to_send == (
142+
b'\x00\x00\x0f\x00\x08\x00\x00\x00\x01'
143+
b'\x05some data\x00\x00\x00\x00\x00'
144+
)
145+
146+
buffer = h2.frame_buffer.FrameBuffer(server=False)
147+
buffer.max_frame_size = 65535
148+
buffer.add_data(data_to_send)
149+
data_frame = list(buffer)[0]
150+
sanity_check_data_frame(
151+
data_frame=data_frame,
152+
expected_flow_controlled_length=len(b'some data') + 1 + 5,
153+
expect_padded_flag=True,
154+
expected_data_frame_pad_length=5
155+
)
156+
157+
def test_sending_data_with_zero_length_padding(self):
158+
"""
159+
Single data frames with zero-length padding are encoded
160+
correctly.
161+
"""
162+
c = h2.connection.H2Connection()
163+
c.initiate_connection()
164+
c.send_headers(1, self.example_request_headers)
165+
166+
# Clear the data, then send some data.
167+
c.clear_outbound_data_buffer()
168+
events = c.send_data(1, b'some data', pad_length=0)
169+
assert not events
170+
data_to_send = c.data_to_send()
171+
assert data_to_send == (
172+
b'\x00\x00\x0a\x00\x08\x00\x00\x00\x01'
173+
b'\x00some data'
174+
)
175+
176+
buffer = h2.frame_buffer.FrameBuffer(server=False)
177+
buffer.max_frame_size = 65535
178+
buffer.add_data(data_to_send)
179+
data_frame = list(buffer)[0]
180+
sanity_check_data_frame(
181+
data_frame=data_frame,
182+
expected_flow_controlled_length=len(b'some data') + 1,
183+
expect_padded_flag=True,
184+
expected_data_frame_pad_length=0
185+
)
186+
187+
@pytest.mark.parametrize("expected_error,pad_length", [
188+
(None, 0),
189+
(None, 255),
190+
(None, None),
191+
(ValueError, -1),
192+
(ValueError, 256),
193+
(TypeError, 'invalid'),
194+
(TypeError, ''),
195+
(TypeError, '10'),
196+
(TypeError, {}),
197+
(TypeError, ['1', '2', '3']),
198+
(TypeError, []),
199+
(TypeError, 1.5),
200+
(TypeError, 1.0),
201+
(TypeError, -1.0),
202+
])
203+
def test_sending_data_with_invalid_padding_length(self,
204+
expected_error,
205+
pad_length):
206+
"""
207+
``send_data`` with a ``pad_length`` parameter that is an integer
208+
outside the range of [0, 255] throws a ``ValueError``, and a
209+
``pad_length`` parameter which is not an ``integer`` type
210+
throws a ``TypeError``.
211+
"""
212+
c = h2.connection.H2Connection()
213+
c.initiate_connection()
214+
c.send_headers(1, self.example_request_headers)
215+
216+
c.clear_outbound_data_buffer()
217+
if expected_error is not None:
218+
with pytest.raises(expected_error):
219+
c.send_data(1, b'some data', pad_length=pad_length)
220+
else:
221+
c.send_data(1, b'some data', pad_length=pad_length)
222+
116223
def test_closing_stream_sending_data(self, frame_factory):
117224
"""
118225
We can close a stream with a data frame.
@@ -1576,3 +1683,27 @@ def test_receiving_goaway_frame_with_unknown_error(self, frame_factory):
15761683
assert c.state_machine.state == h2.connection.ConnectionState.CLOSED
15771684

15781685
assert not c.data_to_send()
1686+
1687+
1688+
def sanity_check_data_frame(data_frame,
1689+
expected_flow_controlled_length,
1690+
expect_padded_flag,
1691+
expected_data_frame_pad_length):
1692+
"""
1693+
``data_frame`` is a frame of type ``hyperframe.frame.DataFrame``,
1694+
and the ``flags`` and ``flow_controlled_length`` of ``data_frame``
1695+
match expectations.
1696+
"""
1697+
1698+
assert isinstance(data_frame, hyperframe.frame.DataFrame)
1699+
1700+
assert(
1701+
(data_frame.flow_controlled_length ==
1702+
expected_flow_controlled_length)
1703+
)
1704+
if expect_padded_flag:
1705+
assert 'PADDED' in data_frame.flags
1706+
else:
1707+
assert 'PADDED' not in data_frame.flags
1708+
1709+
assert data_frame.pad_length == expected_data_frame_pad_length

test/test_flow_control_window.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,23 @@ def test_flow_control_decreases_with_sent_data(self):
5252
remaining_length = self.DEFAULT_FLOW_WINDOW - len(b'some data')
5353
assert (c.local_flow_control_window(1) == remaining_length)
5454

55+
@pytest.mark.parametrize("pad_length", [5, 0])
56+
def test_flow_control_decreases_with_sent_data_with_padding(self,
57+
pad_length):
58+
"""
59+
When padded data is sent on a stream, the flow control window drops
60+
by the length of the padding plus 1 for the 1-byte padding length
61+
field.
62+
"""
63+
c = h2.connection.H2Connection()
64+
c.send_headers(1, self.example_request_headers)
65+
66+
c.send_data(1, b'some data', pad_length=pad_length)
67+
remaining_length = (
68+
self.DEFAULT_FLOW_WINDOW - len(b'some data') - pad_length - 1
69+
)
70+
assert c.local_flow_control_window(1) == remaining_length
71+
5572
def test_flow_control_decreases_with_received_data(self, frame_factory):
5673
"""
5774
When data is received on a stream, the remote flow control window
@@ -70,7 +87,7 @@ def test_flow_control_decreases_with_received_data(self, frame_factory):
7087
def test_flow_control_decreases_with_padded_data(self, frame_factory):
7188
"""
7289
When padded data is received on a stream, the remote flow control
73-
window should drop by an amount that includes the padding.
90+
window drops by an amount that includes the padding.
7491
"""
7592
c = h2.connection.H2Connection(client_side=False)
7693
c.receive_data(frame_factory.preamble())

0 commit comments

Comments
 (0)