This is an automated email from the ASF dual-hosted git repository.
bneradt pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/master by this push:
new 461677bfc4 Adding a 103 Early Hints Autest (#12007)
461677bfc4 is described below
commit 461677bfc48b0e082b9abcc6a53a88af5eabbad8
Author: Brian Neradt <[email protected]>
AuthorDate: Mon Feb 3 17:36:00 2025 -0600
Adding a 103 Early Hints Autest (#12007)
This adds an autest to cover 103 early hints behavior. The test passes
and found no issues.
---
tests/gold_tests/early_hints/early_hints.test.py | 158 +++++++++++++++++++++
tests/gold_tests/early_hints/early_hints_server.py | 108 ++++++++++++++
tests/gold_tests/slow_post/quick_server.test.py | 5 +-
.../{gold_tests/slow_post => tools}/http_utils.py | 0
4 files changed, 270 insertions(+), 1 deletion(-)
diff --git a/tests/gold_tests/early_hints/early_hints.test.py
b/tests/gold_tests/early_hints/early_hints.test.py
new file mode 100644
index 0000000000..aa9648bcbd
--- /dev/null
+++ b/tests/gold_tests/early_hints/early_hints.test.py
@@ -0,0 +1,158 @@
+'''
+Verify correct handling of 103 Early Hints responses.
+'''
+# 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.
+
+from enum import Enum, auto
+import os
+from ports import get_port
+import re
+import sys
+
+
+class Protocol(Enum):
+ HTTP = auto()
+ HTTPS = auto()
+ HTTP2 = auto()
+
+ @classmethod
+ def to_string(cls, protocol):
+ if protocol == cls.HTTP:
+ return 'HTTP'
+ elif protocol == cls.HTTPS:
+ return 'HTTPS'
+ elif protocol == cls.HTTP2:
+ return 'HTTP2'
+ else:
+ return None
+
+
+class TestEarlyHints:
+ '''Verify that ATS can properly handle a 103 response.'''
+
+ _early_hints_server = 'early_hints_server.py'
+
+ def __init__(self, protocol: Protocol):
+ '''Create a test run for the given protocol.
+ :param protocol: The protocol the client will use for the test.
+ '''
+ self._protocol = protocol
+ self._protocol_str = Protocol.to_string(protocol)
+ tr = Test.AddTestRun(f'Early hints with client protocol:
{self._protocol_str}')
+ self._configure_dns(tr)
+ self._configure_server(tr)
+ ts = self._configure_ts(tr)
+ self._copy_scripts(tr, ts)
+ self._configure_client(tr)
+
+ def _configure_dns(self, tr: 'TestRun'):
+ '''Configure the DNS for the test run.
+ :param tr: The TestRun for the DNS process.
+ '''
+ dns = tr.MakeDNServer(f'dns_{self._protocol_str}', default='127.0.0.1')
+ self._dns = dns
+ return dns
+
+ def _configure_server(self, tr: 'TestRun'):
+ '''Configure the origin server for the test run.
+
+ :param tr: The TestRun for the origin server.
+ '''
+ tr.Setup.Copy(self._early_hints_server)
+ server = tr.Processes.Process(f'server_{self._protocol_str}')
+ server_port = get_port(server, "http_port")
+ server.Command = \
+ f'{sys.executable} {self._early_hints_server} 127.0.0.1
{server_port} '
+ server.Ready = When.PortOpenv4(server_port)
+
+ self._server = server
+ return server
+
+ def _configure_ts(self, tr: 'TestRun'):
+ '''Configure the traffic server for the test run.
+
+ :param tr: The TestRun for the traffic server.
+ '''
+ ts = Test.MakeATSProcess(f'ts_{self._protocol_str}', enable_tls=True)
+ self._ts = ts
+ ts.Disk.remap_config.AddLine(f'map /
http://backend.server.com:{self._server.Variables.http_port}')
+ ts.addDefaultSSLFiles()
+ ts.Disk.ssl_multicert_config.AddLine('dest_ip=*
ssl_cert_name=server.pem ssl_key_name=server.key')
+ ts.Disk.records_config.update(
+ {
+ 'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir,
+ 'proxy.config.ssl.server.private_key.path':
ts.Variables.SSLDir,
+ 'proxy.config.dns.nameservers':
f'127.0.0.1:{self._dns.Variables.Port}',
+ 'proxy.config.dns.resolv_conf': 'NULL',
+ 'proxy.config.diags.debug.enabled': 1,
+ 'proxy.config.diags.debug.tags': 'http',
+ })
+ return ts
+
+ def _copy_scripts(self, tr: 'TestRun', ts: 'TestATSProcess'):
+ '''Copy the python server and helper files to the test run directory.
+ :param tr: The TestRun for the server.
+ :param ts: The TestATSProcess for the traffic server. This is needed
+ for the ATS tools directory it stores in Variables.
+ '''
+ tr.Setup.Copy(self._early_hints_server)
+ tools_dir = ts.Variables.AtsTestToolsDir
+ http_utils = os.path.join(tools_dir, 'http_utils.py')
+ tr.Setup.CopyAs(http_utils, Test.RunDirectory)
+
+ def _configure_client(self, tr: 'TestRun'):
+ '''Configure a client to use the given protocol to ATS.
+ :param tr: The TestRun for the client.
+ '''
+ client = tr.Processes.Default
+ if self._protocol == Protocol.HTTP:
+ protocol_arg = '--http1.1'
+ scheme = 'http'
+ ts_port = self._ts.Variables.port
+ elif self._protocol == Protocol.HTTPS:
+ protocol_arg = '-k --http1.1'
+ scheme = 'https'
+ ts_port = self._ts.Variables.ssl_port
+ elif self._protocol == Protocol.HTTP2:
+ protocol_arg = '-k --http2'
+ scheme = 'https'
+ ts_port = self._ts.Variables.ssl_port
+ client.Command = (
+ f'curl -v {protocol_arg} '
+ f'--resolve "server.com:{ts_port}:127.0.0.1" '
+ f'-H "Host: server.com" '
+ f'{scheme}://server.com:{ts_port}/{self._protocol_str}')
+
+ client.ReturnCode = 0
+ self._ts.StartBefore(self._dns)
+ self._ts.StartBefore(self._server)
+ client.StartBefore(self._ts)
+
+ # Note that the server is configured to send two 103 responses.
+ client.Streams.All += Testers.ContainsExpression(
+ 'HTTP/.* 103.*HTTP/.* 103',
+ 'Verify that two 103 Early Hints responses were received.',
+ reflags=re.MULTILINE | re.DOTALL)
+ client.Streams.All += Testers.ContainsExpression(
+ 'ink: </style.css>; rel=preload', 'Verify preload link header was
received.')
+ client.Streams.All += Testers.ContainsExpression('HTTP/.* 200',
'Verify 200 OK response was received.')
+ client.Streams.All += Testers.ContainsExpression('10bytebody', 'Verify
the body to the 200 OK was received.')
+
+
+TestEarlyHints(Protocol.HTTP)
+TestEarlyHints(Protocol.HTTPS)
+TestEarlyHints(Protocol.HTTP2)
diff --git a/tests/gold_tests/early_hints/early_hints_server.py
b/tests/gold_tests/early_hints/early_hints_server.py
new file mode 100644
index 0000000000..d03c305987
--- /dev/null
+++ b/tests/gold_tests/early_hints/early_hints_server.py
@@ -0,0 +1,108 @@
+#!/usr/bin/env python3
+"""A server that replies with a 103 Early Hints response."""
+
+# 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.
+
+import argparse
+from http_utils import (wait_for_headers_complete,
determine_outstanding_bytes_to_read, drain_socket)
+import socket
+import sys
+import time
+
+
+def parse_args() -> argparse.Namespace:
+ '''Parse command line arguments.
+ :return: The parsed command line arguments.
+ '''
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("address", help="Address to listen on")
+ parser.add_argument("port", type=int, default=8080, help="The port to
listen on")
+ parser.add_argument("--num-103", type=int, default=2, help="Number of 103
responses to send before the 200 OK")
+ return parser.parse_args()
+
+
+def get_listening_socket(address: str, port: int) -> socket.socket:
+ """Create a listening socket.
+
+ :param address: The address to listen on.
+ :param port: The port to listen on.
+ :returns: A listening socket.
+ """
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ sock.bind((address, port))
+ sock.listen(1)
+ return sock
+
+
+def accept_connection(sock: socket.socket) -> socket.socket:
+ """Accept a connection.
+
+ :param sock: The socket to accept a connection on.
+ :returns: The accepted socket.
+ """
+ return sock.accept()[0]
+
+
+def send_responses(sock: socket.socket, num_103_responses: int) -> None:
+ """Send an HTTP response.
+
+ :param sock: The socket to write to.
+ :param num_103_responses: The number of 103 responses to send before the
+ 200 OK.
+ """
+ for _ in range(num_103_responses):
+ response = ('HTTP/1.1 103 Early Hints\r\n'
+ 'Link: </style.css>; rel=preload\r\n\r\n')
+ print(f'Sending:\n{response}')
+ sock.sendall(response.encode("utf-8"))
+ time.sleep(0.1)
+ response = ('HTTP/1.1 200 OK\r\n'
+ 'Content-Length: 10\r\n\r\n')
+ print(f'Sending:\n{response}')
+ sock.sendall(response.encode("utf-8"))
+ time.sleep(0.1)
+ body = b'10bytebody'
+ print(f'Sending body:\n{body.decode()}')
+ sock.sendall(body)
+
+
+def main() -> int:
+ '''Start the server that replies with 103 respones.
+ :return: The exit status of the server.
+ '''
+ args = parse_args()
+
+ with get_listening_socket(args.address, args.port) as listening_sock:
+ print(f"Listening on {args.address}:{args.port}")
+
+ read_bytes: bytes = b""
+ while len(read_bytes) == 0:
+ with accept_connection(listening_sock) as sock:
+ read_bytes = wait_for_headers_complete(sock)
+ if len(read_bytes) == 0:
+ # This is probably the PortOpenv4 test. Try again.
+ print("No bytes read on this connection. Trying again.")
+ sock.close()
+ continue
+
+ send_responses(sock, args.num_103)
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/tests/gold_tests/slow_post/quick_server.test.py
b/tests/gold_tests/slow_post/quick_server.test.py
index 69b0755752..fe25040798 100644
--- a/tests/gold_tests/slow_post/quick_server.test.py
+++ b/tests/gold_tests/slow_post/quick_server.test.py
@@ -16,6 +16,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import os
from ports import get_port
import sys
@@ -100,8 +101,10 @@ class QuickServerTest:
self._configure_server(tr)
self._configure_traffic_server(tr)
+ tools_dir = self._ts.Variables.AtsTestToolsDir
+ http_utils = os.path.join(tools_dir, 'http_utils.py')
tr.Setup.CopyAs(self._init_file, Test.RunDirectory)
- tr.Setup.CopyAs(self._http_utils, Test.RunDirectory)
+ tr.Setup.CopyAs(http_utils, Test.RunDirectory)
tr.Setup.CopyAs(self._slow_post_client, Test.RunDirectory)
tr.Setup.CopyAs(self._quick_server, Test.RunDirectory)
diff --git a/tests/gold_tests/slow_post/http_utils.py
b/tests/tools/http_utils.py
similarity index 100%
rename from tests/gold_tests/slow_post/http_utils.py
rename to tests/tools/http_utils.py