Skip to content
Merged
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
21 changes: 12 additions & 9 deletions tests/gold_tests/chunked_encoding/chunked_encoding_h2.test.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import os
import sys

Test.Summary = '''
Test interaction of H2 and chunked encoding
'''
Expand All @@ -36,12 +39,13 @@
# add ssl materials like key, certificates for the server
ts.addDefaultSSLFiles()

origin_server = os.path.join(Test.TestDirectory, "chunked_encoding_h2_server.py")
delay_server = Test.Processes.Process(
"delay-server", "bash -c '" + Test.TestDirectory + "/delay-server.sh {} outserver1'".format(Test.Variables.upstream_port))
"delay-server", f'{sys.executable} "{origin_server}" 127.0.0.1 {Test.Variables.upstream_port} outserver1 delayed-chunked')
server2 = Test.Processes.Process(
"server2", "bash -c '" + Test.TestDirectory + "/server2.sh {} outserver2'".format(Test.Variables.upstream_port2))
"server2", f'{sys.executable} "{origin_server}" 127.0.0.1 {Test.Variables.upstream_port2} outserver2 content-length')
server3 = Test.Processes.Process(
"server3", "bash -c '" + Test.TestDirectory + "/server3.sh {} outserver3'".format(Test.Variables.upstream_port3))
"server3", f'{sys.executable} "{origin_server}" 127.0.0.1 {Test.Variables.upstream_port3} outserver3 chunked')
Comment thread
JosiahWI marked this conversation as resolved.

ts.Disk.records_config.update(
{
Expand All @@ -64,9 +68,8 @@
ssl_key_name: server.key
""".split("\n"))

# Using netcat as a cheap origin server in case 1 so we can insert a delay in sending back the response.
# Replaced microserver for cases 2 and 3 as well because I was getting python exceptions when running
# microserver if chunked encoding headers were specified for the request headers
# Use a raw origin server in case 1 so the final chunk can be delayed. Use it
# for cases 2 and 3 as well because microserver rejects chunked request headers.

# H2 GET request
# chunked response without content-length
Expand All @@ -76,7 +79,7 @@
tr.Processes.Default.Command = 'nghttp -vv https://127.0.0.1:{}/delay-chunked-response'.format(ts.Variables.ssl_port)
tr.Processes.Default.ReturnCode = 0
tr.Processes.Default.StartBefore(Test.Processes.ts)
tr.Processes.Default.StartBefore(delay_server)
tr.Processes.Default.StartBefore(delay_server, ready=When.PortOpen(Test.Variables.upstream_port))
tr.Processes.Default.Streams.All = Testers.ExcludesExpression("RST_STREAM", "Delayed chunk close should not cause reset")
tr.Processes.Default.Streams.All += Testers.ExcludesExpression("< content-length", "Should return chunked")
tr.Processes.Default.Streams.All += Testers.ContainsExpression(":status: 200", "Should get successful response")
Expand All @@ -87,7 +90,7 @@
# HTTP2 POST: www.example.com Host, chunked body
server2_out = Test.Disk.File("outserver2")
tr = Test.AddTestRun()
tr.Processes.Default.StartBefore(server2)
tr.Processes.Default.StartBefore(server2, ready=When.PortOpen(Test.Variables.upstream_port2))
tr.MakeCurlCommand(
'--http2 -k https://127.0.0.1:{}/post-full --verbose -H "Transfer-encoding: chunked" -d "Knock knock"'.format(
ts.Variables.ssl_port),
Expand All @@ -103,7 +106,7 @@
# HTTP2 POST: chunked post body and chunked response
server3_out = Test.Disk.File("outserver3")
tr = Test.AddTestRun()
tr.Processes.Default.StartBefore(server3)
tr.Processes.Default.StartBefore(server3, ready=When.PortOpen(Test.Variables.upstream_port3))
tr.MakeCurlCommand(
'--http2 -k https://127.0.0.1:{}/post-chunked --verbose -H "Transfer-encoding: chunked" -d "Knock knock"'.format(
ts.Variables.ssl_port),
Expand Down
115 changes: 115 additions & 0 deletions tests/gold_tests/chunked_encoding/chunked_encoding_h2_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""Serve one raw HTTP request for the chunked HTTP/2 AuTest."""

# 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 pathlib import Path
import socket
import sys
import time


def parse_args() -> argparse.Namespace:
"""Parse the command-line arguments."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("address", help="Address on which to listen.")
parser.add_argument("port", type=int, help="Port on which to listen.")
parser.add_argument("output", type=Path, help="File in which to record the request.")
parser.add_argument("response", choices=("delayed-chunked", "content-length", "chunked"), help="Response to send.")
return parser.parse_args()


def make_listening_socket(address: str, port: int) -> socket.socket:
"""Create and return a listening TCP socket."""
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listener.bind((address, port))
listener.listen(1)
return listener


def receive_request(conn: socket.socket) -> bytes:
"""Receive one HTTP request, including its declared body."""
request = b""
while b"\r\n\r\n" not in request:
data = conn.recv(4096)
if not data:
return request
request += data

header, _, body = request.partition(b"\r\n\r\n")
content_length = 0
is_chunked = False
for field in header.split(b"\r\n")[1:]:
name, separator, value = field.partition(b":")
if not separator:
continue
name = name.strip().lower()
value = value.strip().lower()
if name == b"content-length":
content_length = int(value)
elif name == b"transfer-encoding" and b"chunked" in value:
is_chunked = True

if is_chunked:
while not (body.startswith(b"0\r\n\r\n") or b"\r\n0\r\n\r\n" in body):
data = conn.recv(4096)
if not data:
break
body += data
else:
while len(body) < content_length:
data = conn.recv(4096)
if not data:
break
body += data

return header + b"\r\n\r\n" + body


def send_response(conn: socket.socket, response: str) -> None:
"""Send the selected raw HTTP response."""
if response == "delayed-chunked":
conn.sendall(b"HTTP/1.1 200\r\nTransfer-encoding: chunked\r\n\r\n")
conn.sendall(b"F\r\n123456789012345\r\n")
time.sleep(1)
conn.sendall(b"0\r\n\r\n")
elif response == "content-length":
conn.sendall(b"HTTP/1.1 200\r\nContent-length: 15\r\n\r\n123456789012345")
else:
conn.sendall(b"HTTP/1.1 200\r\nTransfer-encoding: chunked\r\n\r\nF\r\n123456789012345\r\n0\r\n\r\n")


def main() -> int:
"""Ignore readiness probes, serve one request, and exit."""
args = parse_args()
with make_listening_socket(args.address, args.port) as listener:
while True:
conn, _ = listener.accept()
with conn:
request = receive_request(conn)
if not request:
# When.PortOpen probes the listener without sending data.
continue
args.output.write_bytes(request)
send_response(conn, args.response)
return 0


if __name__ == "__main__":
sys.exit(main())
43 changes: 0 additions & 43 deletions tests/gold_tests/chunked_encoding/delay-server.sh

This file was deleted.

41 changes: 0 additions & 41 deletions tests/gold_tests/chunked_encoding/server2.sh

This file was deleted.

41 changes: 0 additions & 41 deletions tests/gold_tests/chunked_encoding/server3.sh

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ sessions:
- [ Connection, close ]
- [ Cache-Control, "max-age=1" ]
- [ X-Response, oversized-origin-response ]
# The preceding fields serialize to 215 bytes. X-Padding adds 42 wire
# bytes (name, separator, 29-byte value, and CRLF), bringing the header
# to the 256-byte limit plus the one-byte overflow sentinel. This makes
# the memory rejection independent of body segmentation.
- [ X-Padding, aaaaaaaaaaaaaaaaaaaaaaaaaaaaa ]
content:
size: 512

Expand Down