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

Reply via email to