diff --git a/tests/Pipfile b/tests/Pipfile index 68c30514718..14d5bda01c7 100644 --- a/tests/Pipfile +++ b/tests/Pipfile @@ -29,7 +29,7 @@ pyflakes = "*" autest = "==1.10.2" traffic-replay = "*" # this should install TRLib, MicroServer, MicroDNS, Traffic-Replay -hyper = "*" +h2 = "*" dnslib = "*" # These are likely to be available via yum/dnf or apt-get requests = "*" diff --git a/tests/gold_tests/h2/gold/bigfile.gold b/tests/gold_tests/h2/gold/bigfile.gold index 5fd92155bec..55e3daaa870 100644 --- a/tests/gold_tests/h2/gold/bigfile.gold +++ b/tests/gold_tests/h2/gold/bigfile.gold @@ -1,12 +1,11 @@ -Content length = 191414 - -Body length = 191414 - +`` + content-length: 191414 +`` +Response fully received: 191414 bytes Content success - -Content length = 191414 - -Body length = 191414 - +`` + content-length: 191414 +`` +Response fully received: 191414 bytes Content success - +`` diff --git a/tests/gold_tests/h2/gold/chunked.gold b/tests/gold_tests/h2/gold/chunked.gold index 836d51a2884..c09ab52a60f 100644 --- a/tests/gold_tests/h2/gold/chunked.gold +++ b/tests/gold_tests/h2/gold/chunked.gold @@ -1,6 +1,8 @@ -HTTP/2 200 -date: {} -server: ATS/{} +Response received: + :status: 200 + server: `` + date: `` + age: `` `` microserverapachetrafficserver `` diff --git a/tests/gold_tests/h2/gold/remap-200.gold b/tests/gold_tests/h2/gold/remap-200.gold index 5f7e6ecd5f7..5275ee8321c 100644 --- a/tests/gold_tests/h2/gold/remap-200.gold +++ b/tests/gold_tests/h2/gold/remap-200.gold @@ -1,4 +1,8 @@ -HTTP/2 200 -date: {} -server: ATS/{} - +Response received: + :status: 200 + server: `` + date: `` + age: `` +Response fully received: 0 bytes +Content success +`` diff --git a/tests/gold_tests/h2/h2active_timeout.py b/tests/gold_tests/h2/h2active_timeout.py index 01659d36c5a..c19d00fcbbd 100644 --- a/tests/gold_tests/h2/h2active_timeout.py +++ b/tests/gold_tests/h2/h2active_timeout.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 ''' +An h2 client built to trigger active timeout. ''' # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file @@ -18,45 +19,130 @@ # See the License for the specific language governing permissions and # limitations under the License. -from hyper import HTTPConnection -import hyper +import socket +import ssl + +import h2.connection +import h2.events + import argparse import time -def makerequest(port, active_timeout): - hyper.tls._context = hyper.tls.init_context() - hyper.tls._context.check_hostname = False - hyper.tls._context.verify_mode = hyper.compat.ssl.CERT_NONE +def get_socket(port: int) -> socket.socket: + """Create a TLS-wrapped socket. + + :param port: The port to connect to. + + :returns: A TLS-wrapped socket. + """ + + SERVER_NAME = 'localhost' + SERVER_PORT = port + + # generic socket and ssl configuration + socket.setdefaulttimeout(15) + + # Configure an ssl client side context which will not check the server's certificate. + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + ctx.set_alpn_protocols(['h2']) + + # open a socket to the server and initiate TLS/SSL + tls_socket = socket.create_connection((SERVER_NAME, SERVER_PORT)) + tls_socket = ctx.wrap_socket(tls_socket, server_hostname=SERVER_NAME) + return tls_socket + + +def makerequest(port: int, path: str, delay: int) -> None: + """Establish an HTTP/2 connection and send a request. + + :param port: The port to connect to. + :param path: The path to request. + :param delay: The delay to wait between sending requests in a stream. + """ + + tls_socket = get_socket(port) - conn = HTTPConnection('localhost:{0}'.format(port), secure=True) + h2_connection = h2.connection.H2Connection() + h2_connection.initiate_connection() + tls_socket.sendall(h2_connection.data_to_send()) + headers = [ + (':method', 'GET'), + (':path', path), + (':authority', 'localhost'), + (':scheme', 'https'), + ] + + h2_connection.send_headers(1, headers, end_stream=True) + tls_socket.sendall(h2_connection.data_to_send()) + + # delay, triggering ATS timeout. + time.sleep(delay) + + # The following should fail due to the timeout. try: - # delay after sending the first request - # so the H2 session active timeout triggers - # Then the next request should fail - req_id = conn.request('GET', '/') - time.sleep(active_timeout) - response = conn.get_response(req_id) - req_id = conn.request('GET', '/') - response = conn.get_response(req_id) - except Exception: - print('CONNECTION_TIMEOUT') - return + # Send a second request. + h2_connection.send_headers(3, headers, end_stream=True) + tls_socket.sendall(h2_connection.data_to_send()) + + response_stream_ended = False + body = b'' + while not response_stream_ended: + # read raw data from the socket + data = tls_socket.recv(65536 * 1024) + if not data: + break + + # feed raw data into h2, and process resulting events + events = h2_connection.receive_data(data) + for event in events: + if isinstance(event, h2.events.ResponseReceived): + # response headers received + print("Response received:") + for header in event.headers: + print(f' {header[0].decode()}: {header[1].decode()}') + if isinstance(event, h2.events.DataReceived): + # update flow control so the server doesn't starve us + h2_connection.acknowledge_received_data(event.flow_controlled_length, event.stream_id) + # more response body data received + body += event.data + if isinstance(event, h2.events.StreamEnded): + # response body completed, let's exit the loop + response_stream_ended = True + break + # send any pending data to the server + tls_socket.sendall(h2_connection.data_to_send()) - print('NO_TIMEOUT') + print(f"Response fully received: {len(body)} bytes") + + body_str = body.decode('utf-8') + + # tell the server we are closing the h2 connection + h2_connection.close_connection() + tls_socket.sendall(h2_connection.data_to_send()) + + # close the socket + tls_socket.close() + except Exception: + print("CONNECTION_TIMEOUT") def main(): parser = argparse.ArgumentParser() - parser.add_argument("--port", "-p", + parser.add_argument("port", type=int, help="Port to use") - parser.add_argument("--delay", "-d", + parser.add_argument("path", + help="The path to request") + parser.add_argument("delay", type=int, - help="Time to delay in seconds") + help="The number of seconds to delay betwen requests in a stream") args = parser.parse_args() - makerequest(args.port, args.delay) + + makerequest(args.port, args.path, args.delay) if __name__ == '__main__': diff --git a/tests/gold_tests/h2/h2bigclient.py b/tests/gold_tests/h2/h2bigclient.py deleted file mode 100644 index ab73da24e03..00000000000 --- a/tests/gold_tests/h2/h2bigclient.py +++ /dev/null @@ -1,85 +0,0 @@ -#!/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. - -from hyper import HTTPConnection -import hyper -import argparse - - -def getResponseString(response): - typestr = str(type(response)) - if typestr.find('HTTP20') != -1: - string = "HTTP/2 {0}\r\n".format(response.status) - else: - string = "HTTP {0}\r\n".format(response.status) - string += 'date: ' + response.headers.get('date')[0].decode('utf-8') + "\r\n" - string += 'server: ' + response.headers.get('Server')[0].decode('utf-8') + "\r\n" - return string - - -def makerequest(port): - hyper.tls._context = hyper.tls.init_context() - hyper.tls._context.check_hostname = False - hyper.tls._context.verify_mode = hyper.compat.ssl.CERT_NONE - - conn = HTTPConnection('localhost:{0}'.format(port), secure=True) - - # Fetch the object twice so we know at least one time comes from cache - # Exploring timing options - sites = ['/bigfile', '/bigfile'] - request_ids = [] - for site in sites: - request_id = conn.request('GET', url=site) - request_ids.append(request_id) - - # get responses - for req_id in request_ids: - response = conn.get_response(req_id) - body = response.read() - cl = response.headers.get('Content-Length')[0] - print("Content length = {}\r\n".format(int(cl))) - print("Body length = {}\r\n".format(len(body))) - error = 0 - if chr(body[0]) != 'a': - error = 1 - print("First char {}".format(body[0])) - i = 1 - while i < len(body) and not error: - error = chr(body[i]) != 'b' - if error: - print("bad char {} at {}".format(body[i], i)) - i = i + 1 - if not error: - print("Content success\r\n") - else: - print("Content fail\r\n") - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("--port", "-p", - type=int, - help="Port to use") - args = parser.parse_args() - makerequest(args.port) - - -if __name__ == '__main__': - main() diff --git a/tests/gold_tests/h2/h2chunked.py b/tests/gold_tests/h2/h2chunked.py deleted file mode 100644 index 975276115f5..00000000000 --- a/tests/gold_tests/h2/h2chunked.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/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. - -from hyper import HTTPConnection -import hyper -import argparse - - -def getResponseString(response): - typestr = str(type(response)) - if typestr.find('HTTP20') != -1: - string = "HTTP/2 {0}\r\n".format(response.status) - else: - string = "HTTP {0}\r\n".format(response.status) - string += 'date: ' + response.headers.get('date')[0].decode('utf-8') + "\r\n" - string += 'server: ' + response.headers.get('Server')[0].decode('utf-8') + "\r\n" - return string - - -def makerequest(port, _url): - hyper.tls._context = hyper.tls.init_context() - hyper.tls._context.check_hostname = False - hyper.tls._context.verify_mode = hyper.compat.ssl.CERT_NONE - - conn = HTTPConnection('localhost:{0}'.format(port), secure=True) - - sites = {'/'} - request_ids = [] - for _ in sites: - request_id = conn.request('GET', url=_url) - request_ids.append(request_id) - - # get responses - for req_id in request_ids: - response = conn.get_response(req_id) - body = response.read() - print(getResponseString(response)) - print(body.decode('utf-8')) - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("--port", "-p", - type=int, - help="Port to use") - parser.add_argument("--url", "-u", - type=str, - help="url") - args = parser.parse_args() - makerequest(args.port, args.url) - - -if __name__ == '__main__': - main() diff --git a/tests/gold_tests/h2/h2client.py b/tests/gold_tests/h2/h2client.py index b3599cf6d41..412a2e546de 100644 --- a/tests/gold_tests/h2/h2client.py +++ b/tests/gold_tests/h2/h2client.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 ''' +A basic, ad-hoc HTTP/2 client. ''' # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file @@ -18,48 +19,146 @@ # See the License for the specific language governing permissions and # limitations under the License. -from hyper import HTTPConnection -import hyper +import socket +import ssl + +import h2.connection +import h2.events + import argparse -def getResponseString(response): - typestr = str(type(response)) - if typestr.find('HTTP20') != -1: - string = "HTTP/2 {0}\r\n".format(response.status) - else: - string = "HTTP {0}\r\n".format(response.status) - string += 'date: ' + response.headers.get('date')[0].decode('utf-8') + "\r\n" - string += 'server: ' + response.headers.get('Server')[0].decode('utf-8') + "\r\n" - return string +def get_socket(port: int) -> socket.socket: + """Create a TLS-wrapped socket. + + :param port: The port to connect to. + + :returns: A TLS-wrapped socket. + """ + + SERVER_NAME = 'localhost' + SERVER_PORT = port + + # generic socket and ssl configuration + socket.setdefaulttimeout(15) + + # Configure an ssl client side context which will not check the server's certificate. + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + ctx.set_alpn_protocols(['h2']) + + # open a socket to the server and initiate TLS/SSL + tls_socket = socket.create_connection((SERVER_NAME, SERVER_PORT)) + tls_socket = ctx.wrap_socket(tls_socket, server_hostname=SERVER_NAME) + return tls_socket + + +def makerequest(port: int, path: str, verify_default_body: bool, print_body: bool) -> None: + """Establish an HTTP/2 connection and send a request. + :param port: The port to connect to. + :param path: The path to request. + :param verify_default_body: Whether to verify the default response body. + :param print_body: Whether to print the response body. + """ -def makerequest(port): - hyper.tls._context = hyper.tls.init_context() - hyper.tls._context.check_hostname = False - hyper.tls._context.verify_mode = hyper.compat.ssl.CERT_NONE + tls_socket = get_socket(port) - conn = HTTPConnection('localhost:{0}'.format(port), secure=True) + h2_connection = h2.connection.H2Connection() + h2_connection.initiate_connection() + tls_socket.sendall(h2_connection.data_to_send()) - sites = {'/'} - request_ids = [] - for site in sites: - request_id = conn.request('GET', url=site) - request_ids.append(request_id) + headers = [ + (':method', 'GET'), + (':path', path), + (':authority', 'localhost'), + (':scheme', 'https'), + ] - # get responses - for req_id in request_ids: - response = conn.get_response(req_id) - print(getResponseString(response)) + h2_connection.send_headers(1, headers, end_stream=True) + tls_socket.sendall(h2_connection.data_to_send()) + + response_stream_ended = False + body = b'' + while not response_stream_ended: + # read raw data from the socket + data = tls_socket.recv(65536 * 1024) + if not data: + break + + # feed raw data into h2, and process resulting events + events = h2_connection.receive_data(data) + for event in events: + if isinstance(event, h2.events.ResponseReceived): + # response headers received + print("Response received:") + for header in event.headers: + print(f' {header[0].decode()}: {header[1].decode()}') + if isinstance(event, h2.events.DataReceived): + # update flow control so the server doesn't starve us + h2_connection.acknowledge_received_data(event.flow_controlled_length, event.stream_id) + # more response body data received + body += event.data + if isinstance(event, h2.events.StreamEnded): + # response body completed, let's exit the loop + response_stream_ended = True + break + # send any pending data to the server + tls_socket.sendall(h2_connection.data_to_send()) + + print(f"Response fully received: {len(body)} bytes") + + body_str = body.decode('utf-8') + + if print_body: + print(body_str) + + if verify_default_body: + body_ok = True + if len(body_str) > 0: + if body_str[0] != 'a': + print("ERROR: First byte of response body is not 'a'") + body_ok = False + for i in range(1, len(body_str)): + if body_str[i] != 'b': + print(f"ERROR: Byte {i} of response body is not 'b'") + body_ok = False + break + if body_ok: + print("Content success") + else: + print("Content failure") + + # tell the server we are closing the h2 connection + h2_connection.close_connection() + tls_socket.sendall(h2_connection.data_to_send()) + + # close the socket + tls_socket.close() def main(): parser = argparse.ArgumentParser() - parser.add_argument("--port", "-p", + parser.add_argument("port", type=int, help="Port to use") + parser.add_argument("path", + help="The path to request") + parser.add_argument("--repeat", + type=int, + default=1, + help="Number of times to repeat the request") + parser.add_argument("--verify_default_body", + action="store_true", + help="Verify the default body content: abbb...") + parser.add_argument("--print_body", + action="store_true", + help="Print the response body") args = parser.parse_args() - makerequest(args.port) + + for i in range(args.repeat): + makerequest(args.port, args.path, args.verify_default_body, args.print_body) if __name__ == '__main__': diff --git a/tests/gold_tests/h2/http2.test.py b/tests/gold_tests/h2/http2.test.py index 11e6fb109b7..3f8e63964fb 100644 --- a/tests/gold_tests/h2/http2.test.py +++ b/tests/gold_tests/h2/http2.test.py @@ -135,8 +135,6 @@ }) ts.Setup.CopyAs('h2client.py', Test.RunDirectory) -ts.Setup.CopyAs('h2bigclient.py', Test.RunDirectory) -ts.Setup.CopyAs('h2chunked.py', Test.RunDirectory) ts.Setup.CopyAs('h2active_timeout.py', Test.RunDirectory) # ---- @@ -145,7 +143,7 @@ # Test Case 1: basic H2 interaction tr = Test.AddTestRun() -tr.Processes.Default.Command = f'{sys.executable} h2client.py -p {ts.Variables.ssl_port}' +tr.Processes.Default.Command = f'{sys.executable} h2client.py {ts.Variables.ssl_port} / --verify_default_body' tr.Processes.Default.ReturnCode = 0 tr.Processes.Default.StartBefore(server) tr.Processes.Default.StartBefore(Test.Processes.ts) @@ -154,14 +152,14 @@ # Test Case 2: Make sure all the big file gets back. Regression test for issue 1646 tr = Test.AddTestRun() -tr.Processes.Default.Command = f'{sys.executable} h2bigclient.py -p {ts.Variables.ssl_port}' +tr.Processes.Default.Command = f'{sys.executable} h2client.py {ts.Variables.ssl_port} /bigfile --repeat 2 --verify_default_body' tr.Processes.Default.ReturnCode = 0 tr.Processes.Default.Streams.stdout = "gold/bigfile.gold" tr.StillRunningAfter = server # Test Case 3: Chunked content tr = Test.AddTestRun() -tr.Processes.Default.Command = f'{sys.executable} h2chunked.py -p {ts.Variables.ssl_port} -u /test2' +tr.Processes.Default.Command = f'{sys.executable} h2client.py {ts.Variables.ssl_port} /test2 --print_body' tr.Processes.Default.ReturnCode = 0 tr.Processes.Default.Streams.stdout = "gold/chunked.gold" tr.StillRunningAfter = server @@ -179,7 +177,7 @@ # Test Case 5: h2_active_timeout tr = Test.AddTestRun() -tr.Processes.Default.Command = f'{sys.executable} h2active_timeout.py -p {ts.Variables.ssl_port} -d 4' +tr.Processes.Default.Command = f'{sys.executable} h2active_timeout.py {ts.Variables.ssl_port} / 4' tr.Processes.Default.ReturnCode = 0 tr.Processes.Default.Streams.All = "gold/active_timeout.gold" tr.StillRunningAfter = server diff --git a/tests/gold_tests/headers/gold/bad_method.gold b/tests/gold_tests/headers/gold/bad_method.gold index 3a9558b1cd3..635506f6c96 100644 --- a/tests/gold_tests/headers/gold/bad_method.gold +++ b/tests/gold_tests/headers/gold/bad_method.gold @@ -1,23 +1,17 @@ HTTP/1.1 501 Unsupported method ('gET') Content-Type: text/html;charset=utf-8 -Content-Length: 496 +Content-Length: `` Date: `` Age: 0 Connection: keep-alive Server: ATS/`` - - -
- -Error code: 501
Message: Unsupported method ('gET').
-Error code explanation: HTTPStatus.NOT_IMPLEMENTED - Server does not support this operation.
+`` HTTP/1.1 200 OK diff --git a/tests/gold_tests/tls/test-nc-s_client.sh b/tests/gold_tests/tls/test-nc-s_client.sh index 8aaf1192987..4252f4c2b4e 100644 --- a/tests/gold_tests/tls/test-nc-s_client.sh +++ b/tests/gold_tests/tls/test-nc-s_client.sh @@ -15,5 +15,12 @@ # 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. + +# See https://github.com/apache/trafficserver/issues/9880 +ignore_unexpecte_eof='' +if openssl s_client --help 2>&1 | grep -q ignore_unexpected_eof +then + ignore_unexpected_eof='-ignore_unexpected_eof' +fi nc -l -p $1 -c 'echo -e "This is a reply"' -o test.out & -echo "This is a test" | openssl s_client -servername bar.com -connect localhost:$2 -ign_eof +echo "This is a test" | openssl s_client -servername bar.com -connect localhost:$2 -ign_eof ${ignore_unexpected_eof}