You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by bc...@apache.org on 2022/06/14 17:02:41 UTC

[trafficserver] 02/04: Fix Multiplexer POST/PUT Body Handling (#8439)

This is an automated email from the ASF dual-hosted git repository.

bcall pushed a commit to branch 9.1.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git

commit 5e4a4c24621051af904598484d4d32f603b6661f
Author: Brian Neradt <br...@gmail.com>
AuthorDate: Mon Oct 25 10:32:56 2021 -0500

    Fix Multiplexer POST/PUT Body Handling (#8439)
    
    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.
    
    (cherry picked from commit 636d42af56779a6bac4a774745f688f7702247f6)
---
 plugins/multiplexer/dispatch.cc                    |   9 +-
 plugins/multiplexer/dispatch.h                     |   8 +-
 .../pluginTest/multiplexer/gold/multiplexer.gold   |   1 -
 .../pluginTest/multiplexer/multiplexer.test.py     | 224 ++++++++++++++++++---
 .../replays/multiplexer_copy.replay.yaml           | 113 +++++++++++
 .../replays/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(-)

diff --git a/plugins/multiplexer/dispatch.cc b/plugins/multiplexer/dispatch.cc
index 493501ec9..849204b8b 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 4cba3919a..652e148f5 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<TSReturnCode>(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 39572941d..000000000
--- 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 18a0ac73c..df1ebd9dc 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 000000000..c4ceb9094
--- /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 000000000..d655d488e
--- /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 000000000..6db3db834
--- /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 000000000..4609a5f18
--- /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 } ]