Skip to content
Open
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
4 changes: 2 additions & 2 deletions doc/admin-guide/files/records.yaml.en.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5078,15 +5078,15 @@ HTTP/2 Configuration

This is the threshold of sampling stream number to start checking the stream error rate.

.. ts:cv:: CONFIG proxy.config.http2.max_settings_per_frame INT 7
.. ts:cv:: CONFIG proxy.config.http2.max_settings_per_frame INT 16
:reloadable:

Specifies how many settings in an HTTP/2 SETTINGS frame |TS| accepts.
Clients exceeded this limit will be immediately disconnected with an error
code of ENHANCE_YOUR_CALM.
Any negative value configures no limit to the number of settings received.

.. ts:cv:: CONFIG proxy.config.http2.max_settings_per_minute INT 14
.. ts:cv:: CONFIG proxy.config.http2.max_settings_per_minute INT 32
:reloadable:

Specifies how many settings in HTTP/2 SETTINGS frames |TS| accept for a minute.
Expand Down
4 changes: 2 additions & 2 deletions src/proxy/http2/HTTP2.cc
Original file line number Diff line number Diff line change
Expand Up @@ -488,8 +488,8 @@ uint32_t Http2::no_activity_timeout_out = 120;

float Http2::stream_error_rate_threshold = 0.1;
uint32_t Http2::stream_error_sampling_threshold = 10;
int32_t Http2::max_settings_per_frame = 7;
int32_t Http2::max_settings_per_minute = 14;
int32_t Http2::max_settings_per_frame = 16;
int32_t Http2::max_settings_per_minute = 32;
int32_t Http2::max_settings_frames_per_minute = 14;
int32_t Http2::max_ping_frames_per_minute = 60;
int32_t Http2::max_priority_frames_per_minute = 120;
Expand Down
4 changes: 2 additions & 2 deletions src/records/RecordsConfig.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1372,9 +1372,9 @@ static constexpr RecordElement RecordsConfig[] =
,
{RECT_CONFIG, "proxy.config.http2.stream_error_sampling_threshold", RECD_INT, "10", RECU_DYNAMIC, RR_NULL, RECC_STR, "^[0-9]+$", RECA_NULL}
,
{RECT_CONFIG, "proxy.config.http2.max_settings_per_frame", RECD_INT, "7", RECU_DYNAMIC, RR_NULL, RECC_STR, "^-?[0-9]+$", RECA_NULL}
{RECT_CONFIG, "proxy.config.http2.max_settings_per_frame", RECD_INT, "16", RECU_DYNAMIC, RR_NULL, RECC_STR, "^-?[0-9]+$", RECA_NULL}
,
{RECT_CONFIG, "proxy.config.http2.max_settings_per_minute", RECD_INT, "14", RECU_DYNAMIC, RR_NULL, RECC_STR, "^-?[0-9]+$", RECA_NULL}
{RECT_CONFIG, "proxy.config.http2.max_settings_per_minute", RECD_INT, "32", RECU_DYNAMIC, RR_NULL, RECC_STR, "^-?[0-9]+$", RECA_NULL}
,
{RECT_CONFIG, "proxy.config.http2.max_settings_frames_per_minute", RECD_INT, "14", RECU_DYNAMIC, RR_NULL, RECC_STR, "^-?[0-9]+$", RECA_NULL}
,
Expand Down
156 changes: 156 additions & 0 deletions tests/gold_tests/h2/clients/h2_extension_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
#!/usr/bin/env python3

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Send an HTTP/2 request after an extension-rich SETTINGS frame."""

import argparse
import socket
import ssl
import sys
from typing import Tuple

import hpack

H2_PREFACE = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"

TYPE_DATA = 0x00
TYPE_HEADERS = 0x01
TYPE_RST_STREAM = 0x03
TYPE_SETTINGS = 0x04
TYPE_GOAWAY = 0x07

FLAG_ACK = 0x01
FLAG_END_STREAM = 0x01
FLAG_END_HEADERS = 0x04


def make_frame(frame_type: int, flags: int = 0, stream_id: int = 0, payload: bytes = b"") -> bytes:
return len(payload).to_bytes(3, "big") + bytes([frame_type, flags]) + (stream_id & 0x7FFFFFFF).to_bytes(4, "big") + payload


def make_setting(setting_id: int, value: int) -> bytes:
return setting_id.to_bytes(2, "big") + value.to_bytes(4, "big")


def make_socket(port: int) -> ssl.SSLSocket:
socket.setdefaulttimeout(5)

ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
ctx.set_alpn_protocols(["h2"])

raw_socket = socket.create_connection(("127.0.0.1", port))
tls_socket = ctx.wrap_socket(raw_socket, server_hostname="localhost")
if tls_socket.selected_alpn_protocol() != "h2":
raise RuntimeError(f"failed to negotiate h2, got {tls_socket.selected_alpn_protocol()!r}")
return tls_socket


def recv_exact(sock: ssl.SSLSocket, size: int) -> bytes:
data = bytearray()
while len(data) < size:
chunk = sock.recv(size - len(data))
if not chunk:
raise EOFError("socket closed")
data.extend(chunk)
return bytes(data)


def read_frame(sock: ssl.SSLSocket) -> Tuple[int, int, int, bytes]:
header = recv_exact(sock, 9)
length = int.from_bytes(header[0:3], "big")
frame_type = header[3]
flags = header[4]
stream_id = int.from_bytes(header[5:9], "big") & 0x7FFFFFFF
return frame_type, flags, stream_id, recv_exact(sock, length)


def make_settings_payload() -> bytes:
settings = [
(0x01, 4096),
(0x02, 0),
(0x05, 16384),
(0x06, 131072),
(0x03, 100),
(0x04, 65535),
(0x2B61, 65535),
(0x2B62, 65535),
(0x2B63, 65535),
(0x2B64, 10),
(0x2B65, 10),
]
return b"".join(make_setting(setting_id, value) for setting_id, value in settings)


def run(port: int) -> int:
encoder = hpack.Encoder()
decoder = hpack.Decoder()
request_headers = encoder.encode(
[
(":method", "GET"),
(":scheme", "https"),
(":authority", "www.example.com"),
(":path", "/"),
])

with make_socket(port) as sock:
sock.sendall(H2_PREFACE)
sock.sendall(make_frame(TYPE_SETTINGS, payload=make_settings_payload()))
sock.sendall(make_frame(TYPE_HEADERS, FLAG_END_HEADERS | FLAG_END_STREAM, 1, request_headers))

response_status = None
try:
while True:
frame_type, flags, stream_id, payload = read_frame(sock)
if frame_type == TYPE_SETTINGS and not (flags & FLAG_ACK):
sock.sendall(make_frame(TYPE_SETTINGS, FLAG_ACK))
continue
if frame_type == TYPE_GOAWAY:
error_code = int.from_bytes(payload[4:8], "big")
print(f"Received GOAWAY with error code {error_code}", file=sys.stderr)
return 1
if frame_type == TYPE_RST_STREAM and stream_id == 1:
error_code = int.from_bytes(payload[0:4], "big")
print(f"Received RST_STREAM with error code {error_code}", file=sys.stderr)
return 1
if frame_type == TYPE_HEADERS and stream_id == 1:
if not (flags & FLAG_END_HEADERS):
print("Received an unexpected CONTINUATION sequence", file=sys.stderr)
return 1
response_headers = decoder.decode(payload)
response_status = dict(response_headers).get(":status")
if stream_id == 1 and frame_type in (TYPE_HEADERS, TYPE_DATA) and (flags & FLAG_END_STREAM):
if response_status == "200":
print("Received 200 response")
return 0
print(f"Received response status {response_status!r}", file=sys.stderr)
return 1
except (EOFError, socket.timeout) as exc:
print(f"Connection ended before the response completed: {exc}", file=sys.stderr)
return 1


def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("port", type=int, help="ATS TLS port")
args = parser.parse_args()
return run(args.port)


if __name__ == "__main__":
raise SystemExit(main())
9 changes: 9 additions & 0 deletions tests/gold_tests/h2/http2.test.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@

ts.Setup.CopyAs('h2client.py', Test.RunDirectory)
ts.Setup.CopyAs('h2active_timeout.py', Test.RunDirectory)
ts.Setup.CopyAs('clients/h2_extension_settings.py', Test.RunDirectory)

settings_limit_ts = Test.MakeATSProcess("ts_settings_limit", enable_tls=True, enable_cache=False)
settings_limit_ts.addDefaultSSLFiles()
Expand Down Expand Up @@ -275,3 +276,11 @@
tr.Processes.Default.StartBefore(settings_limit_ts)
tr.Processes.Default.Streams.stdout += Testers.ContainsExpression(
"Received GOAWAY with error code 11", "Received ENHANCE_YOUR_CALM GOAWAY.")

# Test Case 11: Extension settings fit within the default SETTINGS limits.
tr = Test.AddTestRun("HTTP/2 extension settings")
tr.Processes.Default.Command = f'{sys.executable} h2_extension_settings.py {ts.Variables.ssl_port}'
tr.Processes.Default.ReturnCode = 0
tr.Processes.Default.Streams.stdout = Testers.ContainsExpression(
"Received 200 response", "The request following the extension settings should succeed.")
tr.StillRunningAfter = server
4 changes: 2 additions & 2 deletions tests/gold_tests/records/gold/full_records.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,8 @@ records:
max_ping_frames_per_minute: 60
max_priority_frames_per_minute: 120
max_settings_frames_per_minute: 14
max_settings_per_frame: 7
max_settings_per_minute: 14
max_settings_per_frame: 16
max_settings_per_minute: 32
min_avg_window_update: 2560.0
min_concurrent_streams_in: 10
no_activity_timeout_in: 120
Expand Down
4 changes: 2 additions & 2 deletions tests/gold_tests/records/legacy_config/full_records.config
Original file line number Diff line number Diff line change
Expand Up @@ -413,8 +413,8 @@ CONFIG proxy.config.http2.push_diary_size INT 256
CONFIG proxy.config.http2.zombie_debug_timeout_in INT 0
CONFIG proxy.config.http2.stream_error_rate_threshold FLOAT 0.1
CONFIG proxy.config.http2.stream_error_sampling_threshold INT 10
CONFIG proxy.config.http2.max_settings_per_frame INT 7
CONFIG proxy.config.http2.max_settings_per_minute INT 14
CONFIG proxy.config.http2.max_settings_per_frame INT 16
CONFIG proxy.config.http2.max_settings_per_minute INT 32
CONFIG proxy.config.http2.max_settings_frames_per_minute INT 14
CONFIG proxy.config.http2.max_ping_frames_per_minute INT 60
CONFIG proxy.config.http2.max_priority_frames_per_minute INT 120
Expand Down