blob: a698ed3bc24b4e85a880fe80d6b25b0480abdb75 [file]
'''
Test the ESI plugin.
'''
# 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 os
import re
Test.Summary = '''
Test the ESI plugin.
'''
Test.SkipUnless(Condition.PluginExists('esi.so'),)
# An enum of expected plugin behaviors for Cache-Control headers.
class CcBehaviorT():
REMOVE_CC = 0
MAKE_PRIVATE = 1
class EsiTest():
"""
A class that encapsulates the configuration and execution of a set of EPI
test cases.
"""
""" static: The same server Process is used across all tests. """
_server = None
""" static: A counter to keep the ATS process names unique across tests. """
_ts_counter = 0
""" static: A counter to keep any output file names unique across tests. """
_output_counter = 0
""" The ATS process for this set of test cases. """
_ts = None
def __init__(self, plugin_config, cc_behavior=CcBehaviorT.REMOVE_CC):
"""
Args:
plugin_config (str): The base config line to place in plugin.config for
the ATS process.
cc_behavior (CcBehaviorT): The expected behavior of the ESI plugin
with respect to the Cache-Control header.
"""
if EsiTest._server is None:
EsiTest._server = EsiTest._create_server()
self._plugin_config = plugin_config
self._cc_behavior = cc_behavior
self._ts = EsiTest._create_ats(self, plugin_config)
@staticmethod
def _create_server():
"""
Create and start a server process.
"""
# Configure our server.
server = Test.MakeOriginServer("server")
# Generate the set of ESI responses derived right from our ESI docs.
# See:
# doc/admin-guide/plugins/esi.en.rst
request_header = {
"headers": ("GET /esi.php HTTP/1.1\r\n"
"Host: www.example.com\r\n"
"Content-Length: 0\r\n\r\n"),
"timestamp": "1469733493.993",
"body": ""
}
esi_body = r'''<?php header('X-Esi: 1'); ?>
<html>
<body>
Hello, <esi:include src="http://www.example.com/date.php"/>
</body>
</html>
'''
response_header = {
"headers":
(
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html\r\n"
"X-Esi: 1\r\n"
"Connection: close\r\n"
"Content-Length: {}\r\n"
"Cache-Control: max-age=300\r\n"
"\r\n".format(len(esi_body))),
"timestamp": "1469733493.993",
"body": esi_body
}
server.addResponse("sessionfile.log", request_header, response_header)
request_header = {
"headers": ("GET /date.php HTTP/1.1\r\n"
"Host: www.example.com\r\n"
"Content-Length: 0\r\n\r\n"),
"timestamp": "1469733493.993",
"body": ""
}
date_body = r'''<?php
header ("Cache-control: no-cache");
echo date('l jS \of F Y h:i:s A');
?>
'''
response_header = {
"headers":
(
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html\r\n"
"Connection: close\r\n"
"Content-Length: {}\r\n"
"Cache-Control: max-age=300\r\n"
"\r\n".format(len(date_body))),
"timestamp": "1469733493.993",
"body": date_body
}
server.addResponse("sessionfile.log", request_header, response_header)
# Verify correct functionality with an empty body.
request_header = {
"headers": ("GET /expect_empty_body HTTP/1.1\r\n"
"Host: www.example.com\r\n"
"Content-Length: 0\r\n\r\n"),
"timestamp": "1469733493.993",
"body": ""
}
response_header = {
"headers":
(
"HTTP/1.1 200 OK\r\n"
"X-ESI: On\r\n"
"Content-Length: 0\r\n"
"Connection: close\r\n"
"Content-Type: text/html; charset=UTF-8\r\n"
"\r\n"),
"timestamp": "1469733493.993",
"body": ""
}
server.addResponse("sessionfile.log", request_header, response_header)
# Create a run to start the server.
tr = Test.AddTestRun("Start the server.")
tr.Processes.Default.StartBefore(server)
tr.Processes.Default.Command = "echo starting the server"
tr.Processes.Default.ReturnCode = 0
tr.StillRunningAfter = server
return server
@staticmethod
def _create_ats(self, plugin_config):
"""
Create and start an ATS process.
"""
EsiTest._ts_counter += 1
# Configure ATS with a vanilla ESI plugin configuration.
ts = Test.MakeATSProcess("ts{}".format(EsiTest._ts_counter))
ts.Disk.records_config.update({
'proxy.config.diags.debug.enabled': 1,
'proxy.config.diags.debug.tags': 'http|plugin_esi',
})
ts.Disk.remap_config.AddLine(f'map http://www.example.com/ http://127.0.0.1:{EsiTest._server.Variables.Port}')
ts.Disk.plugin_config.AddLine(plugin_config)
# Create a run to start the ATS process.
tr = Test.AddTestRun("Start the ATS process.")
tr.Processes.Default.StartBefore(ts)
tr.Processes.Default.Command = "echo starting ATS"
tr.Processes.Default.ReturnCode = 0
tr.StillRunningAfter = ts
return ts
def _configure_client_output_expectations(self, client_process):
client_process.Streams.stderr = "gold/esi_headers.gold"
client_process.Streams.stdout = "gold/esi_body.gold"
if self._cc_behavior == CcBehaviorT.REMOVE_CC:
client_process.Streams.stderr += Testers.ExcludesExpression(
'cache-control:', 'The Cache-Control field should not be present in the response', reflags=re.IGNORECASE)
client_process.Streams.stderr += Testers.ExcludesExpression(
'expires:', 'The Expires field should not be present in the response', reflags=re.IGNORECASE)
if self._cc_behavior == CcBehaviorT.MAKE_PRIVATE:
client_process.Streams.stderr += Testers.ContainsExpression(
'cache-control:.*max-age=0, private',
'The private response directive should be present in the response',
reflags=re.IGNORECASE)
client_process.Streams.stderr += Testers.ContainsExpression(
'expires: -1', 'The Expires field should be set to -1', reflags=re.IGNORECASE)
def run_cases_expecting_gzip(self):
# Test 1: Verify basic ESI functionality.
tr = Test.AddTestRun(f"First request for esi.php: not cached: {self._plugin_config}")
tr.MakeCurlCommand(
f'http://127.0.0.1:{self._ts.Variables.port}/esi.php -H"Host: www.example.com" '
'-H"Accept: */*" --verbose',
ts=self._ts)
tr.Processes.Default.ReturnCode = 0
self._configure_client_output_expectations(tr.Processes.Default)
tr.StillRunningAfter = self._server
tr.StillRunningAfter = self._ts
# Test 2: Repeat the above, should now be cached.
tr = Test.AddTestRun(f"Second request for esi.php: will be cached: {self._plugin_config}")
tr.MakeCurlCommand(
f'http://127.0.0.1:{self._ts.Variables.port}/esi.php -H"Host: www.example.com" '
'-H"Accept: */*" --verbose',
ts=self._ts)
tr.Processes.Default.ReturnCode = 0
self._configure_client_output_expectations(tr.Processes.Default)
tr.StillRunningAfter = self._server
tr.StillRunningAfter = self._ts
# Test 3: Verify the ESI plugin can gzip a response when the client accepts it.
tr = Test.AddTestRun(f"Verify the ESI plugin can gzip a response: {self._plugin_config}")
EsiTest._output_counter += 1
unzipped_body_file = os.path.join(tr.RunDirectory, f"non_empty_curl_output_{EsiTest._output_counter}")
gzipped_body_file = unzipped_body_file + ".gz"
tr.MakeCurlCommand(
f'http://127.0.0.1:{self._ts.Variables.port}/esi.php -H"Host: www.example.com" '
f'-H "Accept-Encoding: gzip" -H"Accept: */*" --verbose --output {gzipped_body_file}',
ts=self._ts)
tr.Processes.Default.ReturnCode = 0
tr.Processes.Default.Ready = When.FileExists(gzipped_body_file)
tr.Processes.Default.Streams.stderr = "gold/esi_gzipped.gold"
tr.StillRunningAfter = self._server
tr.StillRunningAfter = self._ts
zipped_body_disk_file = tr.Disk.File(gzipped_body_file)
zipped_body_disk_file.Exists = True
# Now, unzip the file and make sure its size is the expected body.
tr = Test.AddTestRun(f"Verify the file unzips to the expected body: {self._plugin_config}")
tr.Processes.Default.Command = f"gunzip {gzipped_body_file}"
tr.Processes.Default.Ready = When.FileExists(unzipped_body_file)
tr.Processes.Default.ReturnCode = 0
unzipped_body_disk_file = tr.Disk.File(unzipped_body_file)
unzipped_body_disk_file.Content = "gold/esi_body.gold"
# Test 4: Verify correct handling of a gzipped empty response body.
tr = Test.AddTestRun(f"Verify we can handle an empty response: {self._plugin_config}")
EsiTest._output_counter += 1
empty_body_file = os.path.join(tr.RunDirectory, f"empty_curl_output_{EsiTest._output_counter}")
gzipped_empty_body = empty_body_file + ".gz"
tr.MakeCurlCommand(
f'http://127.0.0.1:{self._ts.Variables.port}/expect_empty_body '
'-H"Host: www.example.com" -H"Accept-Encoding: gzip" -H"Accept: */*" '
f'--verbose --output {gzipped_empty_body}',
ts=self._ts)
tr.Processes.Default.ReturnCode = 0
tr.Processes.Default.Ready = When.FileExists(gzipped_empty_body)
tr.Processes.Default.Streams.stderr = "gold/empty_response_body.gold"
tr.StillRunningAfter = self._server
tr.StillRunningAfter = self._ts
# The gzipped output file should be greater than 0, even though 0 bytes are
# compressed.
gz_disk_file = tr.Disk.File(gzipped_empty_body)
gz_disk_file.Size = Testers.GreaterThan(0)
# Now, unzip the file and make sure its size is the original 0 size body.
tr = Test.AddTestRun(f"Verify the file unzips to a zero sized file: {self._plugin_config}")
tr.Processes.Default.Command = f"gunzip {gzipped_empty_body}"
tr.Processes.Default.Ready = When.FileExists(empty_body_file)
tr.Processes.Default.ReturnCode = 0
unzipped_disk_file = tr.Disk.File(empty_body_file)
unzipped_disk_file.Size = 0
def run_case_max_doc_size_too_small(self):
tr = Test.AddTestRun(f"Max doc size too small: {self._plugin_config}")
tr.MakeCurlCommand(
f'http://127.0.0.1:{self._ts.Variables.port}/esi.php '
'-H"Host: www.example.com" -H"Accept: */*" --verbose',
ts=self._ts)
tr.Processes.Default.ReturnCode = 0
self._ts.Disk.diags_log.Content = Testers.ContainsExpression(
r"ERROR: \[_setup\] Cannot allow attempted doc of size 121; Max allowed size is 100 for URL \[.*esi\.php.*\]",
"max doc size test should have doc size error log")
tr.StillRunningAfter = self._server
tr.StillRunningAfter = self._ts
def run_cases_expecting_no_gzip(self):
# Test 1: Run an ESI test where the client does not accept gzip.
tr = Test.AddTestRun(f"First request for esi.php: gzip not accepted: {self._plugin_config}")
tr.MakeCurlCommand(
f'http://127.0.0.1:{self._ts.Variables.port}/esi.php '
'-H"Host: www.example.com" -H"Accept: */*" --verbose',
ts=self._ts)
tr.Processes.Default.ReturnCode = 0
self._configure_client_output_expectations(tr.Processes.Default)
tr.StillRunningAfter = self._server
tr.StillRunningAfter = self._ts
# Test 2: Verify the ESI plugin does not gzip the response even if the
# client accepts the gzip encoding.
tr = Test.AddTestRun(f"Verify the ESI plugin refuses to gzip responses with: {self._plugin_config}")
tr.MakeCurlCommand(
f'http://127.0.0.1:{self._ts.Variables.port}/esi.php '
'-H"Host: www.example.com" -H "Accept-Encoding: gzip" -H"Accept: */*" --verbose',
ts=self._ts)
tr.Processes.Default.ReturnCode = 0
self._configure_client_output_expectations(tr.Processes.Default)
tr.StillRunningAfter = self._server
tr.StillRunningAfter = self._ts
def run_cases_expecting_no_transformation(self):
tr = Test.AddTestRun(f"Verify the ESI plugin does not transform responses: {self._plugin_config}")
client = tr.MakeCurlCommand(
f'http://127.0.0.1:{self._ts.Variables.port}/esi.php '
'-H"Host: www.example.com" -H"Accept: */*" --verbose',
ts=self._ts)
client.ReturnCode = 0
# Expect no transformation: the tag should be present without any transformation.
client.Streams.stdout += Testers.ContainsExpression(
'Hello, <esi:include src="http://www.example.com/date.php"/>',
'The response should not be transformed',
reflags=re.IGNORECASE)
tr.StillRunningAfter = self._server
tr.StillRunningAfter = self._ts
#
# Configure and run the test cases.
#
# Run the tests with ESI configured with no parameters.
vanilla_test = EsiTest(plugin_config='esi.so', cc_behavior=CcBehaviorT.REMOVE_CC)
vanilla_test.run_cases_expecting_gzip()
private_response_test = EsiTest(plugin_config='esi.so --private-response', cc_behavior=CcBehaviorT.MAKE_PRIVATE)
private_response_test.run_cases_expecting_gzip()
# For these test cases, the behavior should remain the same with
# --first-byte-flush set.
first_byte_flush_test = EsiTest(plugin_config='esi.so --first-byte-flush')
first_byte_flush_test.run_cases_expecting_gzip()
# For these test cases, the behavior should remain the same with
# --packed-node-support set.
#
# Packed node support is incomplete and the following test does not work. Our
# documentation advises users not to use the --packed-node-support feature.
# This test is left here, commented out, so that it is conveniently available
# for and potential future development on this feature if desired.
#
# packed_node_support_test = EsiTest(plugin_config='esi.so --packed-node-support')
# packed_node_support_test.run_cases_expecting_gzip()
# Run a set of cases verifying that the plugin does not zip content if
# --disable-gzip-output is set.
gzip_disabled_test = EsiTest(plugin_config='esi.so --disable-gzip-output')
gzip_disabled_test.run_cases_expecting_no_gzip()
# Run the tests with too small max doc size.
max_doc_100_test = EsiTest(plugin_config='esi.so --max-doc-size 100')
max_doc_100_test.run_case_max_doc_size_too_small()
# Run the tests with no default, but sufficient, max doc size.
max_doc_2K_test = EsiTest(plugin_config='esi.so --max-doc-size 2K')
max_doc_2K_test.run_cases_expecting_gzip()
max_doc_20M_test = EsiTest(plugin_config='esi.so --max-doc-size 20M')
max_doc_20M_test.run_cases_expecting_gzip()
# The test doesn't use 304 redirect, so restricting the allowed response codes to 200
# should not affect the test.
allowed_response_codes_test = EsiTest(plugin_config='esi.so --allowed-response-codes 200')
allowed_response_codes_test.run_cases_expecting_gzip()
# Do not allow transforming the 200 OK response. Since the test uses a 200 OK response,
# the plugin should not transform it.
response_not_allowed_test = EsiTest(plugin_config='esi.so --allowed-response-codes 304')
response_not_allowed_test.run_cases_expecting_no_transformation()