You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by am...@apache.org on 2018/02/16 21:02:41 UTC
[trafficserver] branch master updated: Add TLS Bridge plugin to
plugins/experimental.
This is an automated email from the ASF dual-hosted git repository.
amc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/master by this push:
new e3c64e1 Add TLS Bridge plugin to plugins/experimental.
e3c64e1 is described below
commit e3c64e1f3646d52df2cba4795ffc7294a735f3d8
Author: Alan M.Carroll <so...@oath.com>
AuthorDate: Fri Nov 10 15:16:30 2017 +0000
Add TLS Bridge plugin to plugins/experimental.
(cherry picked from Oath internal commit 0edc61f7637a1680d3d40e69491431ff4571cd95)
---
.../plugins/example-plugins/index.en.rst | 1 +
.../plugins/example-plugins/tls_bridge.en.rst | 245 ++++++++
doc/ext/local-config.py.in | 2 +-
doc/uml/TLS-Bridge-Messages.uml | 80 +++
doc/uml/TLS-Bridge-Plugin.uml | 81 +++
plugins/Makefile.am | 1 +
plugins/experimental/tls_bridge/Makefile.inc | 23 +
plugins/experimental/tls_bridge/README | 20 +
plugins/experimental/tls_bridge/regex.cc | 126 +++++
plugins/experimental/tls_bridge/regex.h | 63 +++
plugins/experimental/tls_bridge/tls_bridge.cc | 630 +++++++++++++++++++++
11 files changed, 1271 insertions(+), 1 deletion(-)
diff --git a/doc/developer-guide/plugins/example-plugins/index.en.rst b/doc/developer-guide/plugins/example-plugins/index.en.rst
index 5141eee..7da4a35 100644
--- a/doc/developer-guide/plugins/example-plugins/index.en.rst
+++ b/doc/developer-guide/plugins/example-plugins/index.en.rst
@@ -28,6 +28,7 @@ Example Plugins
basic-authorization/index.en
blacklist/index.en
query_remap/index.en
+ tls_bridge.en
.. _developer-plugins-header-based-examples:
diff --git a/doc/developer-guide/plugins/example-plugins/tls_bridge.en.rst b/doc/developer-guide/plugins/example-plugins/tls_bridge.en.rst
new file mode 100644
index 0000000..31cce7d
--- /dev/null
+++ b/doc/developer-guide/plugins/example-plugins/tls_bridge.en.rst
@@ -0,0 +1,245 @@
+.. 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.
+
+.. include:: ../../../common.defs
+
+.. highlight:: cpp
+.. default-domain:: cpp
+.. |Name| replace:: TLS Bridge
+
+|Name|
+**********
+
+This plugin is used to provide secured TLS tunnels for connections between a Client and a Service
+via two gateway |TS| instances. By configuring the |TS| instances the level of security in the
+tunnel can be easily controlled for all communications across the tunnels.
+
+Description
+===========
+
+The tunnel is sustained by two instances of |TS|.
+
+.. uml::
+ :align: center
+
+ hide empty members
+
+ cloud "Cloud\nUntrusted\nNetworks" as Cloud
+ node "Ingress ATS"
+ node "Peer ATS"
+
+ [Client] <--> [Ingress ATS] : Unsecure
+ [Ingress ATS] <-> [Cloud] : Secure
+ [Cloud] <-> [Peer ATS] : Secure
+ [Peer ATS] <-u-> [Service] : Unsecure
+
+ [Ingress ATS] ..> [tls_bridge\nPlugin] : Uses
+
+
+The ingress |TS| accepts a connection from the Client. This connection gets intercepted by the
+|Name| plugin inside |TS|. The plugin then makes a TLS connection to the peer |TS| using the
+configured level of security. The original request from the Client to the ingress |TS| is then sent
+to the peer |TS| to create a connection from the peer |TS| to the Service. After this the
+Client has a virtual circut to the Service and can use any TCP based communication (including TLS).
+Effectively the plugin causes the connectivity to work as if the Client had done the ``CONNECT``
+directly to the peer |TS|. Note this means the DNS lookup for the Service is done by the peer |TS|,
+not the ingress |TS|.
+
+The plugin is configured with a mapping of Service names to peer |TS| instances. The Service
+names are URLs which will in the original HTTP request made by the Client after connecting to the
+ingress |TS|. This means the FQDN for the Service is not resolved in the environment of the peer
+|TS| and not the ingress |TS|.
+
+Configuration
+=============
+
+|Name| requires at least two instances of |TS| (Ingress and Peer).
+
+#. Disable caching on |TS| in ``records.config``::
+
+ CONFIG proxy.config.http.cache.http INT 0
+
+#. Configure the ports.
+
+ * The Peer |TS| must be listening on an SSL enabled proxy port. For instance, if the proxy port for the Peer is 4443, then configuration in ``records.config`` would have::
+
+ CONFIG proxy.config.http.server_ports STRING 4443:ssl
+
+ * The Ingress |TS| must allow ``CONNECT`` to the Peer proxy port. This would be set in ``records.config`` by::
+
+ CONFIG proxy.config.http.connect_ports STRING 4443
+
+ The Ingress |TS| also needs ``proxy.config.http.server_ports`` configured to have proxy ports
+ to which the Client can connect.
+
+#. Remap is not required, however, |TS| requires remap in order to accept the request. This can be done by disabling the remap requirement::
+
+ CONFIG proxy.config.url_remap.remap_required INT 0
+
+ In this case |TS| will act as an open proxy which is unlikely to be a good idea. |TS| will need
+ to run in a restricted environment or use access control (via ``ip_allow.config`` or
+ ``iptables``).
+
+#. Configure the Ingress |TS| to verify the Peer server certificate::
+
+ CONFIG proxy.config.ssl.client.verify.server INT 1
+
+#. Configure Certificate Authority used by the Ingress |TS| to verify the Peer server certificate. If this
+ is a directory all of the certificates in the directory are treated as Certificate Authorites. ::
+
+ CONFIG proxy.config.ssl.client.CA.cert.filename STRING </path/to/CA_certificate_file_name>
+
+#. Configure the Ingress |TS| to provide a client certificate::
+
+ CONFIG proxy.config.ssl.client.cert.path STRING </path/to/certificate/dir>
+ CONFIG proxy.config.ssl.client.cert.filename STRING <server_certificate_file_name>
+
+#. Configure the Peer |TS| to verify the Ingress client certificate::
+
+ CONFIG proxy.config.ssl.client.certification_level INT 2
+
+#. Enable the |Name| plugin in ``plugin.config``. The plugin is configured by arguments in
+ ``plugin.config``. These are arguments are in pairs of a *destination* and a *peer*. The
+ destination is a anchored regular expression which is matched against the host name in the Client
+ ``CONNECT``. The destinations are checked in order and the first match is used to select the Peer
+ |TS|. The peer should be an FQDN or IP address with an optional port. For the example above, if
+ the Peer |TS| was named "peer.example.com" on port 4443 and the Service at ``*.service.com``, the
+ peer argument would be "peer.example.com:4443". In ``plugin.config`` this would be::
+
+ tls_bridge.so .*[.]service[.]com peer.example.com:4443
+
+Notes
+=====
+
+|Name| is distinct from more basic Layer 4 Routing available in |TS|. For the latter there is no
+intercept or change of the TLS exchange between the Client and the Service. The exchange looks like
+this
+
+.. uml::
+ :align: center
+
+ actor Client
+ participant "Ingress TS" as Ingress
+ participant Service
+
+ Client <-[#green]> Ingress : //TCP Connect//
+ Client -[#blue]-> Ingress : <font color="blue">TLS: ""CLIENT HELLO""</font>
+ note over Ingress : Map SNI to upstream Service
+ Ingress <-[#green]> Service : //TCP Connect//
+ Ingress -[#blue]-> Service : <font color="blue">TLS: ""CLIENT HELLO""</font>
+ note right : Duplicate of data from Client.
+ note over Ingress : Forward bytes between Client <&arrow-thick-left> <&arrow-thick-right> Service
+ Client <--> Service
+
+The key points are
+
+* |TS| does no TLS negotiation at all. The properties of the connection between the Ingress |TS|
+ and the Service are completely determined by the Client and Server negotation.
+* No packets are modified, the ""CLIENT HELLO"" sent by the Ingress |TS| is an exact copy of that
+ sent to the Ingress |TS| by the Client. It is only examined for the SNI data in order to select
+ the Service.
+
+Implementation
+==============
+
+The |Name| plugin uses :code:`TSHttpTxnIntercept` to gain control of the ingress Client session.
+If the session is valid then a separate connection to the peer |TS| is created using
+:code:`TSHttpConnect`.
+
+After the ingress |TS| connects to the peer |TS| it sends a duplicate of the Client ``CONNECT``
+request. This is processed by the peer |TS| to connect on to the Service. After this both |TS|
+instances then tunnel data between the Client and the Service, in effect becoming a transparent
+tunnel.
+
+The overall exchange looks like the following:
+
+.. uml::
+ :align: center
+
+ @startuml
+
+ box "Client Network" #DDFFDD
+ actor Client
+ entity "User Agent\nVConn" as lvc
+ participant "Ingress ATS" as ingress
+ entity "Upstream\nVConn" as rvc
+ end box
+ box "Corporate Network" #DDDDFF
+ participant "Peer ATS" as peer
+ database Service
+ end box
+
+ Client -> ingress : TCP or TLS connect
+ activate lvc
+ Client -> ingress : HTTP CONNECT
+ ingress -> lvc : Intercept Transaction
+ ingress -> peer : TLS connect
+ activate rvc
+ note over ingress,peer : Secure Tunnel
+ ingress -> peer : HTTP CONNECT
+ note over peer : DNS for Service is\ndone here.
+ peer -> Service : TCP Connect
+
+ note over Client, Service : At this point data can flow between the Client and Server\nover the secure link as a virtual connection, including any TLS handshake.
+ Client <--> Service
+ lvc <-> ingress : <&arrow-thick-left> Move data <&arrow-thick-right>
+ ingress <-> rvc : <&arrow-thick-left> Move data <&arrow-thick-right>
+ note over ingress : Plugin explicitlys moves this data.
+
+ @enduml
+
+
+A detailed view of the plugin operation.
+
+.. image:: ../../../uml/images/TLS-Bridge-Plugin.svg
+ :align: center
+
+A sequence diagram focusing on the request / response data flow. There is a :code:`NetVConn` for the
+connection to the Peer |TS| which is omitted for clarity.
+
+* Blue dotted lines are request or response data
+* Green lines are network connections.
+* Red lines are programmatic interactions.
+* Black lines are hook call backs.
+
+The :code:`200 OK` sent from the Peer |TS| is parsed and consumed by the plugin. An non-:code:`200` response
+means there was an error and the tunnel is shut down. To deal with the Client response clean up the
+response code is stored and used later during cleanup.
+
+.. image:: ../../../uml/images/TLS-Bridge-Messages.svg
+ :align: center
+
+A restartable state machine is used to recognize the end of the Peer |TS| response. The initial part
+of the response is easy because all that is needed is to wait until there is sufficient data for a
+minimal parse. The end can be an arbitrary distance in to the stream and may not all be in the same
+socket read.
+
+.. uml::
+ :align: center
+
+ @startuml
+ [*] -r> State_0
+ State_0 --> State_1 : CR
+ State_1 --> State_0 : *
+ State_1 --> State_1 : CR
+ State_1 --> State_2 : LF
+ State_2 --> State_3 : CR
+ State_2 --> State_0 : *
+ State_3 -r> [*] : LF
+ State_3 --> State_1 : CR
+ State_3 --> State_0 : *
+ @enduml
diff --git a/doc/ext/local-config.py.in b/doc/ext/local-config.py.in
index 5851686..cefb5df 100644
--- a/doc/ext/local-config.py.in
+++ b/doc/ext/local-config.py.in
@@ -14,5 +14,5 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-plantuml='@JAVA@ -jar ${PLANTUML_JAR}'
+plantuml='@JAVA@ -jar {}'.format(os.environ['PLANTUML_JAR'])
plantuml_output_format='svg'
diff --git a/doc/uml/TLS-Bridge-Messages.uml b/doc/uml/TLS-Bridge-Messages.uml
new file mode 100644
index 0000000..dc0ca20
--- /dev/null
+++ b/doc/uml/TLS-Bridge-Messages.uml
@@ -0,0 +1,80 @@
+' Licensed 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.
+
+@startuml
+
+scale max 720 width
+
+actor Client
+box "Ingress ATS" #DDFFDD
+entity "Client\nNetVConn" as uanet
+participant "Ingress\nATS" as ingress
+entity "Client\nVConn" as uavc
+entity "TLS Bridge" as plugin
+entity "Peer\nVConn" as peervc
+end box
+box "Peer ATS" #DDDDFF
+participant "Peer\nATS" as peer
+end box
+participant Service
+
+Client <-[#green]> ingress : <font color="green">//TCP//</font> Handshake
+activate uanet
+Client -[#blue]-> uanet : <font color="blue">""CONNECT"" Service</font>
+uanet -> ingress : //Parse request//
+ingress -[#black]> plugin : ""READ_REQUEST_HDR_HOOK""
+plugin -> ingress : //TSHttpTxnIntercept()//
+ingress -> uavc : //Create//
+activate uavc
+uanet -[#blue]-> ingress : <font color="blue">""CONNECT"" Service</font>
+ingress -[#blue]-> uavc : <font color="blue">""CONNECT"" Service</font>
+ingress -[#black]> plugin : ""TS_EVENT_NET_ACCEPT""
+note right : Client VConn is passed in event data.
+
+plugin -\ ingress : //TSHttpConnect()//
+ingress -> peervc : //create//
+activate peervc
+ingress -/ plugin : //return Peer VConn//
+
+plugin -[#blue]-> peervc : <font color="blue">""CONNECT"" Peer</font>
+peervc -> ingress : //parse request//
+ingress <-[#green]> peer : <font color="green">//TCP//</font> Handshake
+ingress <-[#green]> peer : <font color="green">//TLS//</font> Handshake
+ingress -[#blue]-> peervc : <font color="blue">""200 OK""</font>
+peervc -[#blue]-> plugin : <font color="blue">""200 OK""</font>
+note left
+This signals a raw TLS connection
+nto the Peer ATS. The response is
+parsed and consumed by Plugin.
+end note
+
+note over plugin : Plugin switches to byte forwarding.
+uavc -[#blue]-> plugin : <font color="blue">""CONNECT"" Service</font>
+note left: Original Client Request.
+plugin -[#blue]-> peervc : <font color="blue">""CONNECT"" Service</font>
+peervc -[#blue]-> peer : <font color="blue">""CONNECT"" Service</font>
+peer <-[#green]> Service : <font color="green">//TCP//</font> Handshake
+peer <-[#green]> Service : <font color="green">//TLS//</font> Handshake
+note left : Optional, based on 'Service'
+peer -[#blue]-> peervc : <font color="blue">""200 OK""</font>
+peervc -[#blue]-> plugin : <font color="blue">""200 OK""</font>
+plugin -[#blue]-> uavc : <font color="blue">""200 OK""</font>
+uavc -[#blue]-> ingress : <font color="blue">""200 OK""</font>
+note left
+ATS updates the incoming response based on
+local configuration. This means what goes out
+to the Client may be different than what the
+plugin wrote (forwarded from Peer ATS).
+end note
+ingress -[#black]> plugin : ""SEND_RESPONSE_HDR_HOOK""
+note right : Plugin cleans up response here.
+ingress -[#blue]-> uanet : <font color="blue">""200 OK""</font>
+uanet -[#blue]-> Client : <font color="blue">""200 OK""</font>
+
+Client <-[#green]> Service : //TCP/TLS Connect//
+
+@enduml
diff --git a/doc/uml/TLS-Bridge-Plugin.uml b/doc/uml/TLS-Bridge-Plugin.uml
new file mode 100644
index 0000000..ac617ce
--- /dev/null
+++ b/doc/uml/TLS-Bridge-Plugin.uml
@@ -0,0 +1,81 @@
+' Licensed 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.
+
+@startuml
+
+scale max 720 width
+
+ReadRequestHdr : Check for ""CONNECT""
+ReadRequestHdr : =====
+ReadRequestHdr : Find Peer for Service.
+
+Intercept : Intercept Client Transaction.
+Intercept : =====
+Intercept : Initialize Bridge Context.
+
+Accept : Initialize ""VConn"" data.
+Accept : =====
+Accept : Create internal transaction.
+Accept : =====
+Accept : Set up Client side tunnel.
+Accept : =====
+Accept : ""CONNECT"" to Peer via internal transaction.
+
+Tunnel : Move data.
+
+state "Flow To Peer" as FlowToPeer
+FlowToPeer : Move data from Client ""TSIOBufferReader""\nto Peer ""TSIOBuffer"".
+FlowToPeer : =====
+FlowToPeer : Reenable VIOs
+
+state "Flow To Client" as FlowToClient
+FlowToClient : Move data from Peer ""TSIOBufferReader""\nto Client ""TSIOBuffer"".
+FlowToClient : =====
+FlowToClient : Reenable VIOs
+
+state "Wait For Peer Response" as WaitForPeerResponse {
+ WaitForStatusCode : Parse for status code.
+
+ WaitForResponseEnd : Parse for double newline.
+
+ BadStatus : Set error data\nin Client Response.
+
+ PeerReady : Update Client Response.
+ PeerReady : =====
+ PeerReady : Set up peer tunnel.
+ PeerReady : =====
+ PeerReady : Start Tunneling.
+
+ [*] --> WaitForStatusCode
+ WaitForStatusCode --> WaitForResponseEnd
+ WaitForStatusCode --> BadStatus
+ BadStatus --> [*]
+ WaitForResponseEnd --> PeerReady
+ PeerReady --> [*]
+}
+
+[*] --> ReadRequestHdr : ""CONNECT"" Service
+ReadRequestHdr --> [*] : Not matched.
+ReadRequestHdr --> Intercept
+Intercept --> Accept : ""TS_EVENT_NET_ACCEPT""
+Accept -r-> WaitForPeerResponse
+WaitForPeerResponse --> WaitForPeerResponse : ""TS_EVENT_VCONN_READ_READY""
+WaitForPeerResponse --> Tunnel : 200 OK
+WaitForPeerResponse -u-> [*] : Peer connect failure
+
+Tunnel --> FlowToClient : ""TS_EVENT_VCONN_READ_READY""\nPeer VIO
+FlowToClient --> Tunnel
+Tunnel --> FlowToPeer : ""TS_EVENT_VCONN_READY_READY""\nClient VIO
+FlowToPeer --> Tunnel
+
+Tunnel -right-> Shutdown : ""TS_EVENT_VCONN_EOS""
+
+Shutdown : Close Client VConn
+Shutdown : =====
+Shutdown : Close Upstream VConn
+
+@enduml
diff --git a/plugins/Makefile.am b/plugins/Makefile.am
index 9b9aefc..20337dc 100644
--- a/plugins/Makefile.am
+++ b/plugins/Makefile.am
@@ -74,6 +74,7 @@ include experimental/sslheaders/Makefile.inc
include experimental/stale_while_revalidate/Makefile.inc
include experimental/stream_editor/Makefile.inc
include experimental/system_stats/Makefile.inc
+include experimental/tls_bridge/Makefile.inc
include experimental/ts_lua/Makefile.inc
include experimental/url_sig/Makefile.inc
diff --git a/plugins/experimental/tls_bridge/Makefile.inc b/plugins/experimental/tls_bridge/Makefile.inc
new file mode 100644
index 0000000..e791028
--- /dev/null
+++ b/plugins/experimental/tls_bridge/Makefile.inc
@@ -0,0 +1,23 @@
+# 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.
+
+pkglib_LTLIBRARIES += experimental/tls_bridge/tls_bridge.la
+
+experimental_tls_bridge_tls_bridge_la_SOURCES = \
+ experimental/tls_bridge/tls_bridge.cc \
+ experimental/tls_bridge/regex.h \
+ experimental/tls_bridge/regex.cc
+
diff --git a/plugins/experimental/tls_bridge/README b/plugins/experimental/tls_bridge/README
new file mode 100644
index 0000000..51afa46
--- /dev/null
+++ b/plugins/experimental/tls_bridge/README
@@ -0,0 +1,20 @@
+ATS (Apache Traffic Server) TLS Bridge Plugin
+
+This plugin enables a TLS bridge from an "Ingress" ATS instance to a "Peer" ATS instance using
+the `CONNECT` method. A Client sends a CONNECT to the Ingress, which sets up a TLS connection
+to the Peer. The Peer then processes the original Client CONNECT. The behavior is as if the Client
+sent the CONNECT to the Peer.
+
+Configuration is done in 'plugin.config' by passing pairs of arguments. Each pair is a service and a peer.
+The service is an anchored regular expression which is checked against the host name in the Client
+CONNECT. The peer is an FQDN or IP address with an optional port. If matched, the TLS bridge is created
+to the peer using the specified FQDN and port.
+
+Example:
+
+ tls_bridge.so .*[.]service[.]com peer.example.com:4443
+
+This will create a bridge to 'peer.example.com' on port 4443 for CONNECT requests that end in '.service.com'.
+
+Version 7.0.0 [11/10/17, solidwallofcode]
+ - [YTSATS-1633] Initial version.
diff --git a/plugins/experimental/tls_bridge/regex.cc b/plugins/experimental/tls_bridge/regex.cc
new file mode 100644
index 0000000..3e47c40
--- /dev/null
+++ b/plugins/experimental/tls_bridge/regex.cc
@@ -0,0 +1,126 @@
+/** @file
+
+ PCRE support wrapper.
+
+ @section license License
+
+ 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.
+ */
+
+#include "regex.h"
+
+#ifdef PCRE_CONFIG_JIT
+#include <pthread.h>
+
+struct RegexThreadKey {
+ RegexThreadKey() { pthread_key_create(&this->key, (void (*)(void *)) & pcre_jit_stack_free); }
+ pthread_key_t key;
+};
+
+static RegexThreadKey k;
+
+static pcre_jit_stack *
+get_jit_stack(void *)
+{
+ pcre_jit_stack *jit_stack;
+
+ if ((jit_stack = (pcre_jit_stack *)pthread_getspecific(k.key)) == nullptr) {
+ jit_stack = pcre_jit_stack_alloc(8192, 1024 * 1024); // 1 page min and 1MB max
+ pthread_setspecific(k.key, (void *)jit_stack);
+ }
+
+ return jit_stack;
+}
+#endif
+
+bool
+Regex::compile(const char *pattern, const unsigned flags)
+{
+ const char *error;
+ int erroffset;
+ int options = 0;
+ int study_opts = 0;
+
+ if (regex)
+ return false;
+
+ if (flags & CASE_INSENSITIVE) {
+ options |= PCRE_CASELESS;
+ }
+
+ if (flags & ANCHORED) {
+ options |= PCRE_ANCHORED;
+ }
+
+ regex = pcre_compile(pattern, options, &error, &erroffset, nullptr);
+ if (error) {
+ regex = nullptr;
+ return false;
+ }
+
+#ifdef PCRE_CONFIG_JIT
+ study_opts |= PCRE_STUDY_JIT_COMPILE;
+#endif
+
+ regex_extra = pcre_study(regex, study_opts, &error);
+
+#ifdef PCRE_CONFIG_JIT
+ if (regex_extra)
+ pcre_assign_jit_stack(regex_extra, &get_jit_stack, nullptr);
+#endif
+
+ return true;
+}
+
+int
+Regex::get_capture_count()
+{
+ int captures = -1;
+ if (pcre_fullinfo(regex, regex_extra, PCRE_INFO_CAPTURECOUNT, &captures) != 0) {
+ return -1;
+ }
+
+ return captures;
+}
+
+bool
+Regex::exec(ts::string_view src) const
+{
+ int ovector[30];
+ return exec(src, ovector, 30);
+}
+
+bool
+Regex::exec(ts::string_view src, int *ovector, int ovecsize) const
+{
+ int rv;
+
+ rv = pcre_exec(regex, regex_extra, src.data(), src.size(), 0, 0, ovector, ovecsize);
+ return rv > 0 ? true : false;
+}
+
+Regex::~Regex()
+{
+ if (regex_extra)
+#ifdef PCRE_CONFIG_JIT
+ pcre_free_study(regex_extra);
+#else
+ pcre_free(regex_extra);
+#endif
+ if (regex)
+ pcre_free(regex);
+}
diff --git a/plugins/experimental/tls_bridge/regex.h b/plugins/experimental/tls_bridge/regex.h
new file mode 100644
index 0000000..6c73c26
--- /dev/null
+++ b/plugins/experimental/tls_bridge/regex.h
@@ -0,0 +1,63 @@
+/** @file
+
+ Wrapper to make PCRE handling easier.
+
+ @section license License
+
+ 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.
+ */
+
+#ifndef __TS_REGEX_H__
+#define __TS_REGEX_H__
+
+#include <pcre.h>
+#include <algorithm>
+#include <ts/string_view.h>
+
+class Regex
+{
+ using self_type = Regex;
+
+public:
+ enum Flag {
+ CASE_INSENSITIVE = 0x0001, // default is case sensitive
+ UNANCHORED = 0x0002, // default (for DFA) is to anchor at the first matching position
+ ANCHORED = 0x0004, // default (for Regex) is unanchored
+ };
+
+ Regex() = default;
+ Regex(self_type &&that);
+
+ bool compile(const char *pattern, const unsigned flags = 0);
+ // It is safe to call exec() concurrently on the same object instance
+ bool exec(ts::string_view src) const;
+ bool exec(ts::string_view src, int *ovector, int ovecsize) const;
+ int get_capture_count();
+ ~Regex();
+
+private:
+ pcre *regex = nullptr;
+ pcre_extra *regex_extra = nullptr;
+};
+
+inline Regex::Regex(self_type &&that)
+{
+ std::swap(regex, that.regex);
+ std::swap(regex_extra, that.regex_extra);
+}
+
+#endif /* __TS_REGEX_H__ */
diff --git a/plugins/experimental/tls_bridge/tls_bridge.cc b/plugins/experimental/tls_bridge/tls_bridge.cc
new file mode 100644
index 0000000..0dc9905
--- /dev/null
+++ b/plugins/experimental/tls_bridge/tls_bridge.cc
@@ -0,0 +1,630 @@
+/*
+ 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.
+*/
+
+#include "ts/ts.h"
+#include <atomic>
+#include <vector>
+#include <inttypes.h>
+#include "ts/TextView.h"
+#include "regex.h"
+
+#define PLUGIN_NAME "TLS Bridge"
+#define PLUGIN_TAG "tls-bridge"
+
+using ts::TextView;
+
+// Base format string for making the internal CONNECT.
+char const CONNECT_FORMAT[] = "CONNECT https:%.*s HTTP/1.1\r\n\r\n";
+
+const TextView METHOD_CONNECT{TS_HTTP_METHOD_CONNECT, TS_HTTP_LEN_CONNECT};
+
+/* ------------------------------------------------------------------------------------ */
+// Utility functions
+
+/** Remove a field from a header.
+ @a field is the name of the field, which is removed from the header specified by @a mbuf and @a hdr_loc.
+*/
+void
+Hdr_Remove_Field(TSMBuffer mbuf, TSMLoc hdr_loc, TextView field)
+{
+ TSMLoc field_loc;
+ if (TS_NULL_MLOC != (field_loc = TSMimeHdrFieldFind(mbuf, hdr_loc, field.data(), field.size()))) {
+ TSMimeHdrFieldDestroy(mbuf, hdr_loc, field_loc);
+ TSHandleMLocRelease(mbuf, hdr_loc, field_loc);
+ }
+}
+
+/* ------------------------------------------------------------------------------------ */
+/** Configuration data.
+ This is a mapping of regular expressions to peer destinations. For an inbound CONNECT the destination
+ is matched against the regular expressions. If matched the associated peer is used, otherwise the
+ transaction is not intercepted.
+ */
+class BridgeConfig
+{
+ using self_type = BridgeConfig;
+
+ /// Configuration item, regex -> destination.
+ struct Item {
+ using self_type = BridgeConfig;
+
+ /// Construct an item.
+ Item(const char *pattern, Regex &&r, const char *dest) : _pattern(pattern), _r(std::move(r)), _dest(dest) {}
+
+ std::string _pattern; ///< Original configuration regular expression.
+ Regex _r; ///< Compiled regex.
+ std::string _dest; ///< Destination if matched.
+ };
+
+public:
+ /// Load the configuration from the command line args.
+ void load_config(int argc, const char *argv[]);
+ /// Get the number of configured matches.
+ int count() const;
+ /// Find a match for @a name.
+ /// @return The destination or an empty view if no match.
+ TextView match(TextView name);
+
+private:
+ /// Configuration item storage.
+ std::vector<Item> _items;
+};
+
+inline int
+BridgeConfig::count() const
+{
+ return _items.size();
+}
+
+void
+BridgeConfig::load_config(int argc, const char *argv[])
+{
+ for (int i = 0; i < argc; i += 2) {
+ Regex r;
+ if (i + 1 >= argc) {
+ TSError("%s: Destination regular expression without peer", PLUGIN_TAG);
+ } else {
+ if (r.compile(argv[i]), Regex::ANCHORED) {
+ _items.emplace_back(argv[i], std::move(r), argv[i + 1]);
+ } else {
+ TSError("%s: Failed to compile regular expression '%s'", PLUGIN_TAG, argv[i]);
+ }
+ }
+ }
+}
+
+TextView
+BridgeConfig::match(TextView name)
+{
+ for (auto &item : _items) {
+ if (item._r.exec(name)) {
+ return {item._dest};
+ }
+ }
+ return {};
+}
+
+/// Global instance of the configuration.
+BridgeConfig Config;
+
+/* ------------------------------------------------------------------------------------ */
+/// Operational Context object.
+/// This holds all the data and methods for driving a TLS bridge.
+struct Bridge {
+ /// An I/O operation wrapper.
+ struct Op {
+ TSVIO _vio = nullptr; ///< VIO for operation.
+ TSIOBuffer _buff = nullptr; ///< Buffer for operation.
+ TSIOBufferReader _reader = nullptr; ///< Reader for operation.
+
+ /// Initialize - set up buffer and reader.
+ void init();
+ /// Clean up.
+ void close();
+ };
+
+ /// Per VConn data.
+ struct VCData {
+ TSVConn _vc = nullptr; ///< The virtual connection.
+ Op _write; ///< Write operational data.
+ Op _read; ///< Read operational data.
+
+ /// Initialize - assign the VC and set up the IOBuffers and readers.
+ void init(TSVConn vc);
+ /// Start a read operation of size @a n.
+ void do_read(TSCont cont, int64_t n);
+ /// Start a write operation of size @a n.
+ void do_write(TSCont cont, int64_t n);
+
+ /// Get a view of the available data in the first block.
+ /// This does @b not consume the data, it is a peek.
+ TextView first_block_data();
+ /// Get amount of available data for the read operation, if any.
+ int64_t available_size();
+ /// Consume @a n bytes of data.
+ void consume(int64_t n);
+ /// Close out the connection.
+ void do_close();
+ };
+
+ TSCont _self_cont; ///< The continuation that handles events for @a this.
+ TSHttpTxn _ua_txn; ///< User Agent transaction.
+ TextView _peer; ///< ATS peer for upstream connection.
+ VCData _ua; ///< User agent connection.
+ VCData _out; ///< Outbound connection.
+
+ sockaddr const *_ua_addr; ///< User Agent address, needed for outbound connect.
+
+ /// Parsing state for the response of the internal connect.
+ enum OutboundState {
+ PRE, ///< Not ready to try it yet.
+ OPEN, ///< Initial internal CONNECT sent.
+ OK, ///< Received '200' local response.
+ READY, ///< Received local response terminal.
+ STREAM, ///< In byte streaming mode.
+ EOS, ///< Streaming is done.
+ ERROR, ///< Upstream connection failure.
+ } _out_resp_state = PRE;
+ /// Track depth into outbound response terminal.
+ int _out_terminal_pos = 0;
+ /// Response code from upstream CONNECT
+ TSHttpStatus _out_response_code = TS_HTTP_STATUS_NONE;
+ /// Response reason, if not TS_HTTP_STATUS_OK
+ std::string _out_response_reason;
+ /// Is the response to the user agent suspended?
+ bool _ua_response_suspended = false;
+
+ /// Bridge requires a continuation for scheduling and the transaction.
+ Bridge(TSCont cont, TSHttpTxn txn, TextView peer);
+
+ /// Called when the intercept (user agent) connection is set up.
+ void net_accept(TSVConn ua_vc);
+ /// Called when data is ready.
+ void read_ready(TSVIO vio);
+ /// Outbound reader, waiting for response code.
+ /// @return @c true if response code found and moved to next state.
+ bool check_outbound_OK();
+ /// Outbound reader, waiting for response termination.
+ /// @return @c true if terminal found and moved to next state.
+ bool check_outbound_terminal();
+ /// Outbound bulk reader.
+ void outbound_reader(TSVIO vio);
+ /// Handle EOS.
+ void eos(TSVIO vio);
+ /// Interfere with sending response to the user agent.
+ void send_response_cb();
+ /// Adjust the UA response to correspond to the actual upstream result.
+ void update_ua_response();
+
+ /// Move data from the outbound READ to the UA WRITE.
+ void flow_to_ua();
+ /// Move data from the UA READ to the outbound WRITE.
+ void flow_to_outbound();
+};
+
+/// Used to generate IDs for the plugin connections.
+std::atomic<int64_t> ConnectionCounter{0};
+
+Bridge::Bridge(TSCont cont, TSHttpTxn txn, TextView peer) : _self_cont(cont), _ua_txn(txn), _peer(peer)
+{
+ _ua_addr = TSHttpTxnClientAddrGet(_ua_txn);
+}
+
+void
+Bridge::net_accept(TSVConn vc)
+{
+ char buff[1024];
+ int64_t n = snprintf(buff, sizeof(buff) - 1, CONNECT_FORMAT, static_cast<int>(_peer.size()), _peer.data());
+
+ TSDebug(PLUGIN_TAG, "Received UA VConn");
+ // UA side intercepted.
+ _ua.init(vc);
+ _ua.do_read(_self_cont, INT64_MAX);
+ _ua.do_write(_self_cont, INT64_MAX);
+ // Start up the outbound connect.
+ _out.init(TSHttpConnectWithPluginId(_ua_addr, PLUGIN_TAG, ConnectionCounter++));
+ _out_resp_state = OPEN;
+ TSIOBufferWrite(_out._write._buff, buff, n);
+ _out.do_write(_self_cont, n);
+ TSVIOReenable(_out._write._vio);
+
+ // Need to verify and strip off the outbound TS response to the internal connect.
+ _out.do_read(_self_cont, INT64_MAX);
+}
+
+void
+Bridge::read_ready(TSVIO vio)
+{
+ using ts::TextView;
+
+ TSDebug(PLUGIN_TAG, "READ READY");
+ if (vio == _out._read._vio) {
+ switch (_out_resp_state) {
+ case PRE:
+ break; // this should never happen.
+ case ERROR:
+ break;
+ case EOS:
+ break;
+ case OPEN:
+ if (!this->check_outbound_OK() || _out_resp_state != OK)
+ break;
+ // FALL THROUGH
+ case OK:
+ if (!this->check_outbound_terminal() || _out_resp_state != READY)
+ break;
+ // FALL THROUGH
+ case READY:
+ // Do setup for flowing upstream data to user agent.
+ _out.do_write(_self_cont, INT64_MAX);
+ TSVIOReenable(_out._write._vio);
+ _out_resp_state = STREAM;
+ // FALL THROUGH
+ case STREAM:
+ this->flow_to_ua();
+ break;
+ }
+ } else if (vio == _ua._read._vio) {
+ this->flow_to_outbound();
+ }
+}
+
+bool
+Bridge::check_outbound_OK()
+{
+ bool zret = false;
+ TextView raw{_out.first_block_data()};
+
+ // Only need to check the first block - it's guaranteed to be big enough to hold the status line
+ // and the status line is always the first part of the response.
+ // Looking for 'HTTP/#.# ### Reason text ...' where '#' is a digit.
+ if (raw.size() > (8 + 3 + 1 + 3)) { // if not at least this much data, no chance of success.
+ TextView block{raw};
+ // Sigh, just unroll the check.
+ if (block[0] == 'H' && block[1] == 'T' && block[2] == 'T' && block[3] == 'P' && block[4] == '/') {
+ block += 5;
+ if (block[1] == '.' && ((block[0] == '1' && (block[2] == '0' || block[2] == '1')) || (block[0] == '0' && block[2] == '9'))) {
+ block += 3;
+ block.ltrim_if(&isspace);
+ TextView code = block.take_prefix_if(&isspace);
+ TSHttpStatus c = static_cast<TSHttpStatus>(ts::svtoi(code));
+ if (TS_HTTP_STATUS_OK == c) {
+ _out_resp_state = OK;
+ } else {
+ // Save the reason provided from upstream.
+ TextView reason = block.take_prefix_at('\r');
+ _out_response_reason.assign(reason.data(), reason.size());
+ _out_resp_state = ERROR;
+ }
+ // 519 is POOMA, useful for debugging, but may want to change this later.
+ _out_response_code = c ? c : static_cast<TSHttpStatus>(519);
+ if (_ua_response_suspended) {
+ this->update_ua_response();
+ TSHttpTxnReenable(_ua_txn, TS_EVENT_HTTP_CONTINUE);
+ _ua_response_suspended = false;
+ TSDebug(PLUGIN_TAG, "TXN resumed");
+ }
+ _out.consume(block.data() - raw.data());
+ zret = true;
+ TSDebug(PLUGIN_TAG, "Outbound status %d", c);
+ }
+ }
+ }
+ return zret;
+}
+
+bool
+Bridge::check_outbound_terminal()
+{
+ bool zret = false;
+ TextView block;
+
+ // Need to be more careful here than with the status check because the terminator can
+ // be a large distance in to the response.
+ while (READY != _out_resp_state && !(block = _out.first_block_data()).empty()) { // data is available
+ // Loop through the bytes in the block data.
+ int64_t n_bytes = 0;
+ while (block) {
+ char c = *block;
+ if ('\r' == c) {
+ if (_out_terminal_pos == 2)
+ _out_terminal_pos = 3;
+ else
+ _out_terminal_pos = 1;
+ } else if ('\n' == c) {
+ if (_out_terminal_pos == 3) {
+ _out_terminal_pos = 4;
+ _out_resp_state = READY;
+ zret = true;
+ TSDebug(PLUGIN_TAG, "Outbound ready");
+ } else if (_out_terminal_pos == 1) {
+ _out_terminal_pos = 2;
+ } else {
+ _out_terminal_pos = 0;
+ }
+ } else {
+ _out_terminal_pos = 0;
+ }
+ ++block;
+ ++n_bytes;
+ }
+ _out.consume(n_bytes);
+ }
+ return zret;
+}
+
+void
+Bridge::flow_to_ua()
+{
+ int64_t avail = _out.available_size();
+ if (avail > 0) {
+ int64_t n = TSIOBufferCopy(_ua._write._buff, _out._read._reader, avail, 0);
+ // Assert for now, need to handle this more gracefully.
+ TSAssert(n == avail);
+
+ _out.consume(n);
+ TSDebug(PLUGIN_TAG, "Wrote %" PRId64 " bytes to UA", n);
+ TSVIOReenable(_ua._write._vio);
+ TSVIOReenable(_out._read._vio);
+ }
+}
+
+void
+Bridge::flow_to_outbound()
+{
+ int64_t avail = _ua.available_size();
+ if (avail > 0) {
+ int64_t n = TSIOBufferCopy(_out._write._buff, _ua._read._reader, avail, 0);
+ // Assert for now, need to handle this more gracefully.
+ TSAssert(n == avail);
+
+ _ua.consume(n);
+ TSDebug(PLUGIN_TAG, "Wrote %" PRId64 " bytes to upstream", n);
+ TSVIOReenable(_out._write._vio);
+ TSVIOReenable(_ua._read._vio);
+ }
+}
+
+void
+Bridge::eos(TSVIO vio)
+{
+ if (vio == _out._write._vio || vio == _out._read._vio) {
+ TSDebug(PLUGIN_TAG, "EOS upstream");
+ } else if (vio == _ua._write._vio || vio == _ua._read._vio) {
+ TSDebug(PLUGIN_TAG, "EOS user agent");
+ } else {
+ TSDebug(PLUGIN_TAG, "EOS from unknown VIO");
+ }
+ _out.do_close();
+ _ua.do_close();
+ _out_resp_state = EOS;
+ if (_ua_response_suspended)
+ TSHttpTxnReenable(_ua_txn, TS_EVENT_HTTP_CONTINUE);
+}
+
+void
+Bridge::send_response_cb()
+{
+ // If the upstream response hasn't been parsed yet, make the UA response wait for that.
+ // Set a flag so the upstream response parser knows to update response and reenable.
+ if (_out_resp_state < OK) {
+ _ua_response_suspended = true;
+ TSDebug(PLUGIN_TAG, "TXN suspended");
+ } else { // Already have all the data needed to do the update, so do it and move on.
+ this->update_ua_response();
+ TSHttpTxnReenable(_ua_txn, TS_EVENT_HTTP_CONTINUE);
+ }
+}
+
+void
+Bridge::update_ua_response()
+{
+ TSMBuffer mbuf;
+ TSMLoc hdr_loc;
+ if (TS_SUCCESS == TSHttpTxnClientRespGet(_ua_txn, &mbuf, &hdr_loc)) {
+ // A 200 for @a out_response_code only means there wasn't an internal failure on the upstream
+ // CONNECT. Network and other failures get reported in this response. This response code will
+ // be more accurate, so use it unless it's 200, in which case use the stored response code if
+ // that's not 200.
+ TSHttpStatus status = TSHttpHdrStatusGet(mbuf, hdr_loc);
+ if (TS_HTTP_STATUS_OK == status && TS_HTTP_STATUS_OK != _out_response_code) {
+ TSHttpHdrStatusSet(mbuf, hdr_loc, _out_response_code);
+ if (!_out_response_reason.empty())
+ TSHttpHdrReasonSet(mbuf, hdr_loc, _out_response_reason.data(), _out_response_reason.size());
+ }
+ // TS insists on adding these fields, despite it being a CONNECT.
+ Hdr_Remove_Field(mbuf, hdr_loc, {TS_MIME_FIELD_TRANSFER_ENCODING, TS_MIME_LEN_TRANSFER_ENCODING});
+ Hdr_Remove_Field(mbuf, hdr_loc, {TS_MIME_FIELD_AGE, TS_MIME_LEN_AGE});
+ Hdr_Remove_Field(mbuf, hdr_loc, {TS_MIME_FIELD_PROXY_CONNECTION, TS_MIME_LEN_PROXY_CONNECTION});
+ TSHandleMLocRelease(mbuf, TS_NULL_MLOC, hdr_loc);
+ } else {
+ TSDebug(PLUGIN_TAG, "Failed to retrieve client response");
+ }
+}
+
+void
+Bridge::VCData::init(TSVConn vc)
+{
+ _vc = vc;
+ _write.init();
+ _read.init();
+}
+
+void
+Bridge::VCData::do_read(TSCont cont, int64_t n)
+{
+ _read._vio = TSVConnRead(_vc, cont, _read._buff, n);
+}
+
+void
+Bridge::VCData::do_write(TSCont cont, int64_t n)
+{
+ _write._vio = TSVConnWrite(_vc, cont, _write._reader, n);
+}
+
+void
+Bridge::VCData::do_close()
+{
+ if (_vc) {
+ TSVConnClose(_vc);
+ _vc = nullptr;
+ }
+ _write.close();
+ _read.close();
+}
+
+int64_t
+Bridge::VCData::available_size()
+{
+ return TSIOBufferReaderAvail(_read._reader);
+}
+
+TextView
+Bridge::VCData::first_block_data()
+{
+ TSIOBufferBlock b = TSIOBufferReaderStart(_read._reader);
+ if (b) {
+ int64_t k;
+ const char *s = TSIOBufferBlockReadStart(b, _read._reader, &k);
+ return {s, static_cast<size_t>(k)};
+ }
+ return {nullptr, 0};
+}
+
+void
+Bridge::VCData::consume(int64_t n)
+{
+ TSIOBufferReaderConsume(_read._reader, n);
+}
+
+void
+Bridge::Op::init()
+{
+ _buff = TSIOBufferCreate();
+ _reader = TSIOBufferReaderAlloc(_buff);
+}
+
+void
+Bridge::Op::close()
+{
+ if (_reader) {
+ TSIOBufferReaderFree(_reader);
+ _reader = nullptr;
+ }
+ if (_buff) {
+ TSIOBufferDestroy(_buff);
+ _buff = nullptr;
+ }
+}
+
+/* ------------------------------------------------------------------------------------ */
+// Basically a dispatcher - look up the Bridge instance and call the appropriate method.
+int
+CB_Exec(TSCont contp, TSEvent ev_idx, void *data)
+{
+ auto ctx = static_cast<Bridge *>(TSContDataGet(contp));
+
+ switch (ev_idx) {
+ case TS_EVENT_NET_ACCEPT:
+ ctx->net_accept(static_cast<TSVConn>(data));
+ break;
+ case TS_EVENT_VCONN_READ_READY:
+ case TS_EVENT_VCONN_READ_COMPLETE:
+ ctx->read_ready(static_cast<TSVIO>(data));
+ break;
+ case TS_EVENT_VCONN_WRITE_READY:
+ break;
+ case TS_EVENT_VCONN_WRITE_COMPLETE:
+ break;
+ case TS_EVENT_VCONN_INACTIVITY_TIMEOUT:
+ case TS_EVENT_VCONN_ACTIVE_TIMEOUT:
+ case TS_EVENT_VCONN_EOS:
+ ctx->eos(static_cast<TSVIO>(data));
+ break;
+ case TS_EVENT_HTTP_SEND_RESPONSE_HDR:
+ TSDebug(PLUGIN_TAG, "SEND_RESPONSE_HDR");
+ ctx->send_response_cb();
+ break;
+ case TS_EVENT_HTTP_TXN_CLOSE:
+ TSDebug(PLUGIN_TAG, "TXN_CLOSE: cleanup");
+ delete ctx;
+ break;
+ default:
+ TSDebug(PLUGIN_TAG, "Event %d", ev_idx);
+ break;
+ }
+ return TS_EVENT_CONTINUE;
+}
+
+// Handle a new transaction - check if it should be intercepted and if so do the intercept.
+int
+CB_Read_Request_Hdr(TSCont contp, TSEvent ev_idx, void *data)
+{
+ auto txn = static_cast<TSHttpTxn>(data);
+ TSMBuffer mbuf;
+ TSMLoc hdr_loc;
+
+ if (!TSHttpTxnIsInternal(txn)) {
+ if (TS_SUCCESS == TSHttpTxnClientReqGet(txn, &mbuf, &hdr_loc)) {
+ int method_len;
+ const char *method_data = TSHttpHdrMethodGet(mbuf, hdr_loc, &method_len);
+ if (TextView{method_data, method_len} == METHOD_CONNECT) {
+ int host_len = 0;
+ const char *host_name = TSHttpHdrHostGet(mbuf, hdr_loc, &host_len);
+ TextView peer{Config.match({host_name, host_len})};
+ if (peer) {
+ // Everything checks, let's intercept.
+ auto actor = TSContCreate(CB_Exec, TSContMutexGet(reinterpret_cast<TSCont>(txn)));
+ auto ctx = new Bridge(actor, txn, peer);
+
+ TSDebug(PLUGIN_TAG, "Intercepting transaction %" PRIu64 " to '%.*s' via '%.*s'", TSHttpTxnIdGet(txn), host_len, host_name,
+ static_cast<int>(peer.size()), peer.data());
+
+ TSContDataSet(actor, ctx);
+ // Need to play games with the response, delaying it until upstream connection is done.
+ // Also may potentiall modify it to correspond to the upstream result.
+ TSHttpTxnHookAdd(txn, TS_HTTP_SEND_RESPONSE_HDR_HOOK, actor);
+ // Arrange for cleanup.
+ TSHttpTxnHookAdd(txn, TS_HTTP_TXN_CLOSE_HOOK, actor);
+ // Grab the transaction
+ TSHttpTxnIntercept(actor, txn);
+ }
+ }
+ }
+ }
+ TSHttpTxnReenable(txn, TS_EVENT_HTTP_CONTINUE);
+ return TS_EVENT_CONTINUE;
+}
+
+/* ------------------------------------------------------------------------------------ */
+
+void
+TSPluginInit(int argc, char const *argv[])
+{
+ TSPluginRegistrationInfo info{PLUGIN_NAME, "Oath:", "solidwallofcode@oath.com"};
+
+ if (TSPluginRegister(&info) != TS_SUCCESS)
+ TSError(PLUGIN_NAME ": plugin registration failed.");
+
+ Config.load_config(argc - 1, argv + 1);
+ if (Config.count() <= 0)
+ TSError("%s: No destinations defined, plugin disabled", PLUGIN_TAG);
+
+ TSCont contp = TSContCreate(CB_Read_Request_Hdr, TSMutexCreate());
+ TSHttpHookAdd(TS_HTTP_READ_REQUEST_HDR_HOOK, contp);
+}
--
To stop receiving notification emails like this one, please contact
amc@apache.org.