From eb288b9204193db71233953a7bfe4ab7772a9581 Mon Sep 17 00:00:00 2001 From: Brian Neradt Date: Tue, 19 Oct 2021 00:22:58 +0000 Subject: [PATCH] Fix Multiplexer POST/PUT Body Handling The previous Multiplexer Plugin test only verified that the plugin would load without error. This updates the test to verify it can multiplex to multiple hosts, exercising both HTTP and HTTPS connections to the multiplexed hosts. While doing this, I added POST and PUT request method tests. This showed that the plugin could not handle request bodies and simply failed a misconfigured assertion when the request body was processed. This change also fixes that bug in the plugin. --- plugins/multiplexer/dispatch.cc | 9 +- plugins/multiplexer/dispatch.h | 8 +- .../multiplexer/gold/multiplexer.gold | 1 - .../multiplexer/multiplexer.test.py | 224 +++++++++++++++--- .../replays/multiplexer_copy.replay.yaml | 113 +++++++++ .../multiplexer_copy_skip_post.replay.yaml | 55 +++++ .../replays/multiplexer_original.replay.yaml | 122 ++++++++++ ...multiplexer_original_skip_post.replay.yaml | 122 ++++++++++ 8 files changed, 620 insertions(+), 34 deletions(-) delete mode 100644 tests/gold_tests/pluginTest/multiplexer/gold/multiplexer.gold create mode 100644 tests/gold_tests/pluginTest/multiplexer/replays/multiplexer_copy.replay.yaml create mode 100644 tests/gold_tests/pluginTest/multiplexer/replays/multiplexer_copy_skip_post.replay.yaml create mode 100644 tests/gold_tests/pluginTest/multiplexer/replays/multiplexer_original.replay.yaml create mode 100644 tests/gold_tests/pluginTest/multiplexer/replays/multiplexer_original_skip_post.replay.yaml diff --git a/plugins/multiplexer/dispatch.cc b/plugins/multiplexer/dispatch.cc index 493501ec98f..849204b8b8b 100644 --- a/plugins/multiplexer/dispatch.cc +++ b/plugins/multiplexer/dispatch.cc @@ -86,8 +86,13 @@ copy(const TSIOBufferReader &r, const TSIOBuffer b) const void *const pointer = TSIOBufferBlockReadStart(block, r, &size); if (pointer != nullptr && size > 0) { - CHECK(TSIOBufferWrite(b, pointer, size) == size); - length += size; + auto const num_written = TSIOBufferWrite(b, pointer, size); + if (num_written != size) { + TSError("[" PLUGIN_TAG "] did not write the expected number of body bytes. " + "Wrote: %" PRId64 ", expected: %" PRId64, + num_written, size); + } + length += num_written; } } diff --git a/plugins/multiplexer/dispatch.h b/plugins/multiplexer/dispatch.h index 4cba3919a1f..652e148f54e 100644 --- a/plugins/multiplexer/dispatch.h +++ b/plugins/multiplexer/dispatch.h @@ -49,10 +49,10 @@ #else // Check if expression X returns a value that implicitly converts to bool false (such as TS_SUCCESS). -#define CHECK(X) \ - { \ - static_assert(!TS_SUCCESS); \ - assert(!(X)); \ +#define CHECK(X) \ + { \ + const TSReturnCode r = static_cast(X); \ + assert(r == TS_SUCCESS); \ } #endif diff --git a/tests/gold_tests/pluginTest/multiplexer/gold/multiplexer.gold b/tests/gold_tests/pluginTest/multiplexer/gold/multiplexer.gold deleted file mode 100644 index 39572941dd1..00000000000 --- a/tests/gold_tests/pluginTest/multiplexer/gold/multiplexer.gold +++ /dev/null @@ -1 +0,0 @@ -``DIAG: (multiplexer)`` diff --git a/tests/gold_tests/pluginTest/multiplexer/multiplexer.test.py b/tests/gold_tests/pluginTest/multiplexer/multiplexer.test.py index 18a0ac73ce2..df1ebd9dc07 100644 --- a/tests/gold_tests/pluginTest/multiplexer/multiplexer.test.py +++ b/tests/gold_tests/pluginTest/multiplexer/multiplexer.test.py @@ -16,37 +16,207 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os + Test.Summary = ''' -Test experimental/multiplexer. +Test the Multiplexer plugin. ''' Test.SkipUnless( Condition.PluginExists('multiplexer.so') ) -Test.ContinueOnFail = False -# Define default ATS -ts = Test.MakeATSProcess("ts") -server = Test.MakeOriginServer("server") - -request_header = {"headers": "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionfile.log", request_header, response_header) - - -ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 1, - 'proxy.config.diags.debug.tags': 'multiplexer', -}) -ts.Disk.remap_config.AddLine( - 'map http://www.example.com http://127.0.0.1:{0} @plugin=multiplexer.so'.format(server.Variables.Port) -) -# For now, just make sure the plugin loads without error. -tr = Test.AddTestRun() -tr.Processes.Default.Command = 'curl --silent --proxy 127.0.0.1:{0} "http://www.example.com" -H "Proxy-Connection: close"'.format( - ts.Variables.port) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port)) -tr.Processes.Default.StartBefore(Test.Processes.ts) -ts.Streams.stderr = "gold/multiplexer.gold" -tr.StillRunningAfter = ts + +class MultiplexerTestBase: + """ + Encapsulates the base configuration used by each test. + """ + + client_counter = 0 + server_counter = 0 + ts_counter = 0 + + def __init__(self, replay_file, multiplexed_host_replay_file, skip_post): + self.replay_file = replay_file + self.multiplexed_host_replay_file = multiplexed_host_replay_file + + self.setupServers() + self.setupTS(skip_post) + + def setupServers(self): + counter = MultiplexerTestBase.server_counter + MultiplexerTestBase.server_counter += 1 + self.server_origin = Test.MakeVerifierServerProcess( + f"server_origin_{counter}", self.replay_file) + self.server_http = Test.MakeVerifierServerProcess( + f"server_http_{counter}", self.multiplexed_host_replay_file) + self.server_https = Test.MakeVerifierServerProcess( + f"server_https_{counter}", self.multiplexed_host_replay_file) + + # The origin should never receive "X-Multiplexer: copy" + self.server_origin.Streams.All += Testers.ExcludesExpression( + '"X-Multiplexer": "copy"', + 'Verify the original server target never receives a "copy".') + + # Nor should the multiplexed hosts receive an "original" X-Multiplexer value. + self.server_http.Streams.All += Testers.ExcludesExpression( + '"X-Multiplexer": "original"', + 'Verify the HTTP multiplexed host does not receive an "original".') + self.server_https.Streams.All += Testers.ExcludesExpression( + '"X-Multiplexer": "original"', + 'Verify the HTTPS multiplexed host does not receive an "original".') + + # In addition, the original server should always receive the POST and + # PUT requests. + self.server_origin.Streams.All += Testers.ContainsExpression( + '"uuid": "POST"', + "Verify the client's original target received the POST transaction.") + self.server_origin.Streams.All += Testers.ContainsExpression( + '"uuid": "PUT"', + "Verify the client's original target received the PUT transaction.") + + # Under all configurations, the GET request should be multiplexed. + self.server_origin.Streams.All += Testers.ContainsExpression( + '"X-Multiplexer": "original"', + 'Verify the client\'s original target received the "original" request.') + self.server_origin.Streams.All += Testers.ContainsExpression( + '"uuid": "GET"', + "Verify the client's original target received the GET request.") + + self.server_http.Streams.All += Testers.ContainsExpression( + '"X-Multiplexer": "copy"', + 'Verify the HTTP server received a "copy" of the request.') + self.server_http.Streams.All += Testers.ContainsExpression( + '"uuid": "GET"', + "Verify the HTTP server received the GET request.") + + self.server_https.Streams.All += Testers.ContainsExpression( + '"X-Multiplexer": "copy"', + 'Verify the HTTPS server received a "copy" of the request.') + self.server_https.Streams.All += Testers.ContainsExpression( + '"uuid": "GET"', + "Verify the HTTPS server received the GET request.") + + # Verify that the HTTPS server receives a TLS connection. + self.server_https.Streams.All += Testers.ContainsExpression( + 'Finished accept using TLSSession', + "Verify the HTTPS was indeed used by the HTTPS server.") + + def setupTS(self, skip_post): + counter = MultiplexerTestBase.ts_counter + MultiplexerTestBase.ts_counter += 1 + self.ts = Test.MakeATSProcess(f"ts_{counter}", enable_tls=True, enable_cache=False) + self.ts.addDefaultSSLFiles() + self.ts.Disk.records_config.update({ + "proxy.config.ssl.server.cert.path": f'{self.ts.Variables.SSLDir}', + "proxy.config.ssl.server.private_key.path": f'{self.ts.Variables.SSLDir}', + "proxy.config.ssl.client.verify.server.policy": 'PERMISSIVE', + 'proxy.config.ssl.keylog_file': '/tmp/tls_session_keys.txt', + + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'multiplexer', + }) + self.ts.Disk.ssl_multicert_config.AddLine( + 'dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key' + ) + skip_remap_param = '' + if skip_post: + skip_remap_param = ' @pparam=proxy.config.multiplexer.skip_post_put=1' + self.ts.Disk.remap_config.AddLines([ + f'map https://origin.server.com https://127.0.0.1:{self.server_origin.Variables.https_port} ' + f'@plugin=multiplexer.so @pparam=nontls.server.com @pparam=tls.server.com' + f'{skip_remap_param}', + + # Now create remap entries for the multiplexed hosts: one that + # verifies HTTP, and another that verifies HTTPS. + f'map http://nontls.server.com http://127.0.0.1:{self.server_http.Variables.http_port}', + f'map http://tls.server.com https://127.0.0.1:{self.server_https.Variables.https_port}', + ]) + + def run(self): + tr = Test.AddTestRun() + tr.Processes.Default.StartBefore(self.server_origin) + tr.Processes.Default.StartBefore(self.server_http) + tr.Processes.Default.StartBefore(self.server_https) + tr.Processes.Default.StartBefore(self.ts) + + counter = MultiplexerTestBase.client_counter + MultiplexerTestBase.client_counter += 1 + tr.AddVerifierClientProcess( + f"client_{counter}", + self.replay_file, + https_ports=[self.ts.Variables.ssl_port]) + + +class MultiplexerTest(MultiplexerTestBase): + """ + Exercise multiplexing without skip_post configuration. + """ + + replay_file = os.path.join("replays", "multiplexer_original.replay.yaml") + multiplexed_host_replay_file = os.path.join("replays", "multiplexer_copy.replay.yaml") + + def __init__(self): + super().__init__( + MultiplexerTest.replay_file, + MultiplexerTest.multiplexed_host_replay_file, + skip_post=False) + + def setupServers(self): + super().setupServers() + + # Both of the multiplexed hosts should receive the POST because skip_post + # is disabled. + self.server_http.Streams.All += Testers.ContainsExpression( + '"uuid": "POST"', + "Verify the HTTP server received the POST request.") + self.server_https.Streams.All += Testers.ContainsExpression( + '"uuid": "POST"', + "Verify the HTTPS server received the POST request.") + + # Same with PUT + self.server_http.Streams.All += Testers.ContainsExpression( + '"uuid": "PUT"', + "Verify the HTTP server received the PUT request.") + self.server_https.Streams.All += Testers.ContainsExpression( + '"uuid": "PUT"', + "Verify the HTTPS server received the PUT request.") + + +class MultiplexerSkipPostTest(MultiplexerTestBase): + """ + Exercise multiplexing with skip_post configuration. + """ + + replay_file = os.path.join("replays", "multiplexer_original_skip_post.replay.yaml") + multiplexed_host_replay_file = os.path.join("replays", "multiplexer_copy_skip_post.replay.yaml") + + def __init__(self): + super().__init__( + MultiplexerSkipPostTest.replay_file, + MultiplexerSkipPostTest.multiplexed_host_replay_file, + skip_post=True) + + def setupServers(self): + super().setupServers() + + # Neither of the multiplexed hosts should receive the POST because skip_post + # is enabled. + self.server_http.Streams.All += Testers.ExcludesExpression( + '"uuid": "POST"', + "Verify the HTTP server did not receive the POST request.") + self.server_https.Streams.All += Testers.ExcludesExpression( + '"uuid": "POST"', + "Verify the HTTPS server did not receive the POST request.") + + # Same with PUT. + self.server_http.Streams.All += Testers.ExcludesExpression( + '"uuid": "PUT"', + "Verify the HTTP server did not receive the PUT request.") + self.server_https.Streams.All += Testers.ExcludesExpression( + '"uuid": "PUT"', + "Verify the HTTPS server did not receive the PUT request.") + + +MultiplexerTest().run() +MultiplexerSkipPostTest().run() diff --git a/tests/gold_tests/pluginTest/multiplexer/replays/multiplexer_copy.replay.yaml b/tests/gold_tests/pluginTest/multiplexer/replays/multiplexer_copy.replay.yaml new file mode 100644 index 00000000000..c4ceb90943a --- /dev/null +++ b/tests/gold_tests/pluginTest/multiplexer/replays/multiplexer_copy.replay.yaml @@ -0,0 +1,113 @@ +# 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. + +meta: + version: "1.0" + +sessions: +- protocol: + - name: http + - name: tls + - name: tcp + - name: ip + + transactions: + - client-request: + method: "GET" + version: "1.1" + url: /path/get + headers: + fields: + - [ Host, origin.server.com ] + - [ Content-Length, 0 ] + - [ X-Request, first ] + - [ uuid, GET ] + + proxy-request: + method: "GET" + headers: + fields: + - [ X-Request, { value: first, as: equal } ] + - [ X-Multiplexer, { value: copy, as: equal } ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Content-Length, 32 ] + - [ X-Response, first ] + + # There is no client since this response terminates at ATS, so no need for + # proxy-response. + + - client-request: + method: "POST" + version: "1.1" + url: /path/post + headers: + fields: + - [ Host, origin.server.com ] + - [ Content-Length, 8 ] + - [ X-Request, second ] + - [ uuid, POST ] + + proxy-request: + method: "POST" + headers: + fields: + - [ X-Request, { value: second, as: equal } ] + - [ X-Multiplexer, { value: copy, as: equal } ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Content-Length, 32 ] + - [ X-Response, second ] + + # There is no client since this response terminates at ATS, so no need for + # proxy-response. + + - client-request: + method: "PUT" + version: "1.1" + url: /path/put + headers: + fields: + - [ Host, origin.server.com ] + - [ Content-Length, 8 ] + - [ X-Request, third ] + - [ uuid, PUT ] + + proxy-request: + method: "PUT" + headers: + fields: + - [ X-Request, { value: third, as: equal } ] + - [ X-Multiplexer, { value: copy, as: equal } ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Content-Length, 32 ] + - [ X-Response, third ] + + # There is no client since this response terminates at ATS, so no need for + # proxy-response. diff --git a/tests/gold_tests/pluginTest/multiplexer/replays/multiplexer_copy_skip_post.replay.yaml b/tests/gold_tests/pluginTest/multiplexer/replays/multiplexer_copy_skip_post.replay.yaml new file mode 100644 index 00000000000..d655d488e9a --- /dev/null +++ b/tests/gold_tests/pluginTest/multiplexer/replays/multiplexer_copy_skip_post.replay.yaml @@ -0,0 +1,55 @@ +# 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. + +meta: + version: "1.0" + +sessions: +- protocol: + - name: http + - name: tls + - name: tcp + - name: ip + + transactions: + - client-request: + method: "GET" + version: "1.1" + url: /path/get + headers: + fields: + - [ Host, origin.server.com ] + - [ Content-Length, 0 ] + - [ X-Request, first ] + - [ uuid, GET ] + + proxy-request: + method: "GET" + headers: + fields: + - [ X-Request, { value: first, as: equal } ] + - [ X-Multiplexer, { value: copy, as: equal } ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Content-Length, 32 ] + - [ X-Response, first ] + + # Since POST and POST requests are skipped, the multiplexed hosts should + # not receive them. diff --git a/tests/gold_tests/pluginTest/multiplexer/replays/multiplexer_original.replay.yaml b/tests/gold_tests/pluginTest/multiplexer/replays/multiplexer_original.replay.yaml new file mode 100644 index 00000000000..6db3db834d6 --- /dev/null +++ b/tests/gold_tests/pluginTest/multiplexer/replays/multiplexer_original.replay.yaml @@ -0,0 +1,122 @@ +# 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. + +meta: + version: "1.0" + +sessions: +- protocol: + - name: http + - name: tls + - name: tcp + - name: ip + + transactions: + - client-request: + method: "GET" + version: "1.1" + url: /path/get + headers: + fields: + - [ Host, origin.server.com ] + - [ Content-Length, 0 ] + - [ X-Request, first ] + - [ uuid, GET ] + + proxy-request: + method: "GET" + headers: + fields: + - [ X-Request, { value: first, as: equal } ] + - [ X-Multiplexer, { value: original, as: equal } ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Content-Length, 32 ] + - [ X-Response, first ] + + proxy-response: + status: 200 + headers: + fields: + - [ X-Response, { value: first, as: equal } ] + + - client-request: + method: "POST" + version: "1.1" + url: /path/post + headers: + fields: + - [ Host, origin.server.com ] + - [ Content-Length, 8 ] + - [ X-Request, second ] + - [ uuid, POST ] + + proxy-request: + method: "POST" + headers: + fields: + - [ X-Request, { value: second, as: equal } ] + - [ X-Multiplexer, { value: original, as: equal } ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Content-Length, 32 ] + - [ X-Response, second ] + + proxy-response: + status: 200 + headers: + fields: + - [ X-Response, { value: second, as: equal } ] + + - client-request: + method: "PUT" + version: "1.1" + url: /path/put + headers: + fields: + - [ Host, origin.server.com ] + - [ Content-Length, 8 ] + - [ X-Request, third ] + - [ uuid, PUT ] + + proxy-request: + method: "PUT" + headers: + fields: + - [ X-Request, { value: third, as: equal } ] + - [ X-Multiplexer, { value: original, as: equal } ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Content-Length, 32 ] + - [ X-Response, third ] + + proxy-response: + status: 200 + headers: + fields: + - [ X-Response, { value: third, as: equal } ] diff --git a/tests/gold_tests/pluginTest/multiplexer/replays/multiplexer_original_skip_post.replay.yaml b/tests/gold_tests/pluginTest/multiplexer/replays/multiplexer_original_skip_post.replay.yaml new file mode 100644 index 00000000000..4609a5f18c3 --- /dev/null +++ b/tests/gold_tests/pluginTest/multiplexer/replays/multiplexer_original_skip_post.replay.yaml @@ -0,0 +1,122 @@ +# 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. + +meta: + version: "1.0" + +sessions: +- protocol: + - name: http + - name: tls + - name: tcp + - name: ip + + transactions: + - client-request: + method: "GET" + version: "1.1" + url: /a/path + headers: + fields: + - [ Host, origin.server.com ] + - [ Content-Length, 0 ] + - [ X-Request, first ] + - [ uuid, GET ] + + proxy-request: + method: "GET" + headers: + fields: + - [ X-Request, { value: first, as: equal } ] + - [ X-Multiplexer, { value: original, as: equal } ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Content-Length, 32 ] + - [ X-Response, first ] + + proxy-response: + status: 200 + headers: + fields: + - [ X-Response, { value: first, as: equal } ] + + - client-request: + method: "POST" + version: "1.1" + url: /another/path + headers: + fields: + - [ Host, origin.server.com ] + - [ Content-Length, 8 ] + - [ X-Request, second ] + - [ uuid, POST ] + + # Since POST requests are "skipped", there will be no X-Multiplexer headers. + proxy-request: + method: "POST" + headers: + fields: + - [ X-Request, { value: second, as: equal } ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Content-Length, 32 ] + - [ X-Response, second ] + + proxy-response: + status: 200 + headers: + fields: + - [ X-Response, { value: second, as: equal } ] + + - client-request: + method: "PUT" + version: "1.1" + url: /path/put + headers: + fields: + - [ Host, origin.server.com ] + - [ Content-Length, 8 ] + - [ X-Request, third ] + - [ uuid, PUT ] + + # Since POST requests are "skipped", there will be no X-Multiplexer headers. + proxy-request: + method: "PUT" + headers: + fields: + - [ X-Request, { value: third, as: equal } ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Content-Length, 32 ] + - [ X-Response, third ] + + proxy-response: + status: 200 + headers: + fields: + - [ X-Response, { value: third, as: equal } ]