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

bneradt 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 c5430e6a85 Support gRPC traffic (#9987)
c5430e6a85 is described below

commit c5430e6a85e14db59d4d7ccc579036b3f0792c44
Author: Brian Neradt <[email protected]>
AuthorDate: Wed Jan 24 10:15:02 2024 -0600

    Support gRPC traffic (#9987)
    
    Now that we support HTTP/2 to origin, we can now support gRPC traffic end 
to end. This patch includes the following:
    
    * A new gRPC autest.
    * Updates from @keesspoelstra to support tunneling HTTP/2 trailers.
    * An update to preserve the "te: trailers" header which the server requires 
in order to transmit gRPC traffic.
    * A rename of Http2Stream::send_request to send_headers.
---
 include/proxy/http/HttpSM.h             |   1 +
 include/proxy/http2/Http2Stream.h       |  10 ++-
 include/tscore/ink_string++.h           |   1 +
 src/proxy/hdrs/HdrToken.cc              |   3 -
 src/proxy/http/HttpSM.cc                |  26 +++++-
 src/proxy/http/HttpTransactHeaders.cc   |  18 +++-
 src/proxy/http2/Http2ConnectionState.cc |  17 ++--
 src/proxy/http2/Http2Stream.cc          |  81 ++++++++++++------
 tests/Pipfile                           |   4 +
 tests/gold_tests/h2/grpc/grpc.test.py   | 143 ++++++++++++++++++++++++++++++++
 tests/gold_tests/h2/grpc/grpc_client.py |  74 +++++++++++++++++
 tests/gold_tests/h2/grpc/grpc_server.py |  81 ++++++++++++++++++
 tests/gold_tests/h2/grpc/simple.proto   |  38 +++++++++
 13 files changed, 451 insertions(+), 46 deletions(-)

diff --git a/include/proxy/http/HttpSM.h b/include/proxy/http/HttpSM.h
index ae14d4d11d..6f28523d0b 100644
--- a/include/proxy/http/HttpSM.h
+++ b/include/proxy/http/HttpSM.h
@@ -446,6 +446,7 @@ private:
   void perform_cache_write_action();
   void perform_transform_cache_write_action();
   void setup_blind_tunnel(bool send_response_hdr, IOBufferReader *initial = 
nullptr);
+  void setup_tunnel_handler_trailer(HttpTunnelProducer *p);
   HttpTunnelProducer *setup_server_transfer_to_transform();
   HttpTunnelProducer *setup_transfer_from_transform();
   HttpTunnelProducer *setup_cache_transfer_to_transform();
diff --git a/include/proxy/http2/Http2Stream.h 
b/include/proxy/http2/Http2Stream.h
index ffb811cd71..72515f1fb6 100644
--- a/include/proxy/http2/Http2Stream.h
+++ b/include/proxy/http2/Http2Stream.h
@@ -82,7 +82,7 @@ public:
   void set_expect_receive_trailer() override;
 
   Http2ErrorCode decode_header_blocks(HpackHandle &hpack_handle, uint32_t 
maximum_table_size);
-  void send_request(Http2ConnectionState &cstate);
+  void send_headers(Http2ConnectionState &cstate);
   void initiating_close();
   bool is_outbound_connection() const;
   bool is_tunneling() const;
@@ -217,6 +217,12 @@ private:
   History<HISTORY_DEFAULT_SIZE> _history;
   Milestones<Http2StreamMilestone, 
static_cast<size_t>(Http2StreamMilestone::LAST_ENTRY)> _milestones;
 
+  /** Any headers received while this is true are trailing headers.
+   *
+   * This is set to true when processing DATA frames are done. Therefore any
+   * headers seen after that point are trailing headers. The qualification
+   * "possible" is added because the peer may or may not send trailing headers.
+   */
   bool _trailing_header_is_possible = false;
   bool _expect_send_trailer         = false;
   bool _expect_receive_trailer      = false;
@@ -373,7 +379,7 @@ Http2Stream::reset_send_headers()
   this->_send_header.create(HTTP_TYPE_RESPONSE);
 }
 
-// Check entire DATA payload length if content-length: header is exist
+// Check entire DATA payload length if content-length: header exists
 inline void
 Http2Stream::increment_data_length(uint64_t length)
 {
diff --git a/include/tscore/ink_string++.h b/include/tscore/ink_string++.h
index 9d737edcc1..c8e9a1ada9 100644
--- a/include/tscore/ink_string++.h
+++ b/include/tscore/ink_string++.h
@@ -32,6 +32,7 @@
 
 #pragma once
 #include <cstdio>
+#include <cstring>
 #include <strings.h>
 
 /***********************************************************************
diff --git a/src/proxy/hdrs/HdrToken.cc b/src/proxy/hdrs/HdrToken.cc
index 2b7a30b7fb..104350feab 100644
--- a/src/proxy/hdrs/HdrToken.cc
+++ b/src/proxy/hdrs/HdrToken.cc
@@ -230,9 +230,6 @@ static HdrTokenFieldInfo 
_hdrtoken_strs_field_initializers[] = {
   {"Strict-Transport-Security", MIME_SLOTID_NONE,                
MIME_PRESENCE_NONE,                (HTIF_MULTVALS)                              
},
   {"Subject",                   MIME_SLOTID_NONE,                
MIME_PRESENCE_SUBJECT,             HTIF_NONE                                    
},
   {"Summary",                   MIME_SLOTID_NONE,                
MIME_PRESENCE_SUMMARY,             HTIF_NONE                                    
},
- // TODO: In the past we have observed issues with having hop-by-hop in here
-  // for gRPC. We plan to work on gRPC in a future. We should experiment with
-  // this and verify that it works as expected.
   {"TE",                        MIME_SLOTID_TE,                  
MIME_PRESENCE_TE,                  (HTIF_COMMAS | HTIF_MULTVALS | 
HTIF_HOPBYHOP)},
   {"Transfer-Encoding",         MIME_SLOTID_TRANSFER_ENCODING,   
MIME_PRESENCE_TRANSFER_ENCODING,
    (HTIF_COMMAS | HTIF_MULTVALS | HTIF_HOPBYHOP)                               
                                                                  },
diff --git a/src/proxy/http/HttpSM.cc b/src/proxy/http/HttpSM.cc
index 21d446c4f1..b3b2aa4ca1 100644
--- a/src/proxy/http/HttpSM.cc
+++ b/src/proxy/http/HttpSM.cc
@@ -2780,6 +2780,23 @@ HttpSM::tunnel_handler_post(int event, void *data)
   return 0;
 }
 
+void
+HttpSM::setup_tunnel_handler_trailer(HttpTunnelProducer *p)
+{
+  p->read_success               = true;
+  t_state.current.server->state = HttpTransact::TRANSACTION_COMPLETE;
+  t_state.current.server->abort = HttpTransact::DIDNOT_ABORT;
+
+  SMDbg(dbg_ctl_http, "Wait for the trailing header");
+
+  // Swap out the default hander to set up the new tunnel for the trailer 
exchange.
+  HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::tunnel_handler_trailer);
+  if (_ua.get_txn()) {
+    _ua.get_txn()->set_expect_send_trailer();
+  }
+  tunnel.local_finish_all(p);
+}
+
 int
 HttpSM::tunnel_handler_trailer(int event, void *data)
 {
@@ -3085,6 +3102,10 @@ HttpSM::tunnel_handler_server(int event, 
HttpTunnelProducer *p)
         tunnel.append_message_to_producer_buffer(p, reason, reason_len);
         }
       */
+      if (server_txn->expect_receive_trailer()) {
+        setup_tunnel_handler_trailer(p);
+        return 0;
+      }
       tunnel.local_finish_all(p);
     }
     break;
@@ -3111,10 +3132,7 @@ HttpSM::tunnel_handler_server(int event, 
HttpTunnelProducer *p)
       }
     }
     if (server_txn->expect_receive_trailer()) {
-      SMDbg(dbg_ctl_http, "wait for that trailing header");
-      // Swap out the default hander to set up the new tunnel for the trailer 
exchange.
-      HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::tunnel_handler_trailer);
-      tunnel.local_finish_all(p);
+      setup_tunnel_handler_trailer(p);
       return 0;
     }
     break;
diff --git a/src/proxy/http/HttpTransactHeaders.cc 
b/src/proxy/http/HttpTransactHeaders.cc
index 3384c62863..9080263001 100644
--- a/src/proxy/http/HttpTransactHeaders.cc
+++ b/src/proxy/http/HttpTransactHeaders.cc
@@ -33,6 +33,7 @@
 #include "proxy/hdrs/HTTP.h"
 #include "proxy/hdrs/HdrUtils.h"
 #include "proxy/hdrs/HttpCompat.h"
+#include "proxy/hdrs/MIME.h"
 #include "proxy/http/HttpSM.h"
 #include "proxy/PoolableSession.h"
 
@@ -222,7 +223,9 @@ HttpTransactHeaders::copy_header_fields(HTTPHdr *src_hdr, 
HTTPHdr *new_hdr, bool
   //         my opinion error prone and if the client doesn't follow the spec
   //         we'll have problems with the TE being forwarded to the server
   //         and us caching the transfer encoded documents and then
-  //         serving it to a client that can not handle it
+  //         serving it to a client that can not handle it. The exception
+  //         to this is that we will allow "TE: trailers" to be forwarded
+  //         because that is required for gRPC traffic.
   //      2) Transfer encoding is copied.  If the transfer encoding
   //         is changed for example by dechunking, the transfer encoding
   //         should be modified when the decision is made to dechunk it
@@ -235,10 +238,19 @@ HttpTransactHeaders::copy_header_fields(HTTPHdr *src_hdr, 
HTTPHdr *new_hdr, bool
     int field_flags = hdrtoken_index_to_flags(field.m_wks_idx);
 
     if (field_flags & HTIF_HOPBYHOP) {
+      std::string_view name(field.name_get());
+      std::string_view value(field.value_get());
+      bool const is_te_trailers = name == MIME_FIELD_TE && value == "trailers";
+      if (is_te_trailers) {
+        // te: trailers is used by gRPC, do not delete it.
+        continue;
+      }
+
       // Delete header if not in special proxy_auth retention mode
-      if ((!retain_proxy_auth_hdrs) || (!(field_flags & HTIF_PROXYAUTH))) {
-        new_hdr->field_delete(&field);
+      if (retain_proxy_auth_hdrs && (field_flags & HTIF_PROXYAUTH)) {
+        continue;
       }
+      new_hdr->field_delete(&field);
     } else if (field.m_wks_idx == MIME_WKSIDX_DATE) {
       date_hdr = true;
     }
diff --git a/src/proxy/http2/Http2ConnectionState.cc 
b/src/proxy/http2/Http2ConnectionState.cc
index 9f716cdc70..0dd8e53d94 100644
--- a/src/proxy/http2/Http2ConnectionState.cc
+++ b/src/proxy/http2/Http2ConnectionState.cc
@@ -475,16 +475,16 @@ Http2ConnectionState::rcv_headers_frame(const Http2Frame 
&frame)
       stream->mark_milestone(Http2StreamMilestone::START_TXN);
       stream->new_transaction(frame.is_from_early_data());
       // Send request header to SM
-      stream->send_request(*this);
+      stream->send_headers(*this);
     } else {
       // If this is a trailer, first signal to the SM that the body is done
       if (stream->trailing_header_is_possible()) {
         stream->set_expect_receive_trailer();
         // Propagate the  trailer header
-        stream->send_request(*this);
+        stream->send_headers(*this);
       } else {
         // Propagate the response
-        stream->send_request(*this);
+        stream->send_headers(*this);
       }
     }
     // Give a chance to send response before reading next frame.
@@ -1061,7 +1061,7 @@ Http2ConnectionState::rcv_continuation_frame(const 
Http2Frame &frame)
     // "from_early_data" flag from the associated HEADERS frame.
     stream->new_transaction(frame.is_from_early_data());
     // Send request header to SM
-    stream->send_request(*this);
+    stream->send_headers(*this);
     // Give a chance to send response before reading next frame.
     this->session->interrupt_reading_frames();
   } else {
@@ -2132,15 +2132,18 @@ Http2ConnectionState::send_a_data_frame(Http2Stream 
*stream, size_t &payload_len
   stream->update_sent_count(payload_length);
 
   // Are we at the end?
+  // We have no payload to send but might expect data from either trailer or 
body
+  // TODO(KS): does the expect send trailer and empty payload need a flush, or 
does it
+  //           warrant a separate flow with NO_ERROR?
   // If we return here, we never send the END_STREAM in the case of a early 
terminating OS.
   // OK if there is no body yet. Otherwise continue on to send a DATA frame 
and delete the stream
-  if (!stream->is_write_vio_done() && payload_length == 0) {
+  if ((!stream->is_write_vio_done() || stream->expect_send_trailer()) && 
payload_length == 0) {
     Http2StreamDebug(this->session, stream->get_id(), "No payload");
     this->session->flush();
     return Http2SendDataFrameResult::NO_PAYLOAD;
   }
 
-  if (stream->is_write_vio_done()) {
+  if (stream->is_write_vio_done() && 
!resp_reader->is_read_avail_more_than(payload_length) && 
!stream->expect_send_trailer()) {
     Http2StreamDebug(this->session, stream->get_id(), "End of Data Frame");
     flags |= HTTP2_FLAGS_DATA_END_STREAM;
   }
@@ -2430,7 +2433,7 @@ Http2ConnectionState::send_push_promise_frame(Http2Stream 
*stream, URL &url, con
   stream->set_receive_headers(hdr);
   stream->new_transaction();
   stream->receive_end_stream = true; // No more data with the request
-  stream->send_request(*this);
+  stream->send_headers(*this);
 
   return true;
 }
diff --git a/src/proxy/http2/Http2Stream.cc b/src/proxy/http2/Http2Stream.cc
index fac543b6bf..d958151fc4 100644
--- a/src/proxy/http2/Http2Stream.cc
+++ b/src/proxy/http2/Http2Stream.cc
@@ -29,6 +29,7 @@
 #include "proxy/http/HttpDebugNames.h"
 #include "proxy/http/HttpSM.h"
 #include "tscore/HTTPVersion.h"
+#include "tscore/ink_assert.h"
 
 #include <numeric>
 
@@ -259,45 +260,47 @@ Http2Stream::decode_header_blocks(HpackHandle 
&hpack_handle, uint32_t maximum_ta
 }
 
 void
-Http2Stream::send_request(Http2ConnectionState &cstate)
+Http2Stream::send_headers(Http2ConnectionState &cstate)
 {
   if (closed) {
     return;
   }
   REMEMBER(NO_EVENT, this->reentrancy_count);
 
-  // Convert header to HTTP/1.1 format
-  if (http2_convert_header_from_2_to_1_1(&_receive_header) == 
PARSE_RESULT_ERROR) {
-    Http2StreamDebug("Error converting HTTP/2 headers to HTTP/1.1.");
-    if (_receive_header.type_get() == HTTP_TYPE_REQUEST) {
-      // There's no way to cause Bad Request directly at this time.
-      // Set an invalid method so it causes an error later.
-      _receive_header.method_set("\xffVOID", 1);
+  // Convert header to HTTP/1.1 format. Trailing headers need no conversion
+  // because they, by definition, do not contain pseudo headers.
+  if (this->trailing_header_is_possible()) {
+    Http2StreamDebug("trailing header: Skipping send_headers initialization.");
+  } else {
+    if (http2_convert_header_from_2_to_1_1(&_receive_header) == 
PARSE_RESULT_ERROR) {
+      Http2StreamDebug("Error converting HTTP/2 headers to HTTP/1.1.");
+      if (_receive_header.type_get() == HTTP_TYPE_REQUEST) {
+        // There's no way to cause Bad Request directly at this time.
+        // Set an invalid method so it causes an error later.
+        _receive_header.method_set("\xffVOID", 1);
+      }
     }
-  }
 
-  if (_receive_header.type_get() == HTTP_TYPE_REQUEST) {
-    // Check whether the request uses CONNECT method
-    int method_len;
-    const char *method = _receive_header.method_get(&method_len);
-    if (method_len == HTTP_LEN_CONNECT && strncmp(method, HTTP_METHOD_CONNECT, 
HTTP_LEN_CONNECT) == 0) {
-      this->_is_tunneling = true;
+    if (_receive_header.type_get() == HTTP_TYPE_REQUEST) {
+      // Check whether the request uses CONNECT method
+      int method_len;
+      const char *method = _receive_header.method_get(&method_len);
+      if (method_len == HTTP_LEN_CONNECT && strncmp(method, 
HTTP_METHOD_CONNECT, HTTP_LEN_CONNECT) == 0) {
+        this->_is_tunneling = true;
+      }
     }
+    ink_release_assert(this->_sm != nullptr);
+    this->_http_sm_id = this->_sm->sm_id;
   }
 
-  if (this->expect_send_trailer()) {
-    // Send read complete to terminate previous data tunnel
-    this->read_vio.nbytes = this->read_vio.ndone;
-    this->signal_read_event(VC_EVENT_READ_COMPLETE);
-  }
-
-  ink_release_assert(this->_sm != nullptr);
-  this->_http_sm_id = this->_sm->sm_id;
-
   // Write header to a buffer.  Borrowing logic from 
HttpSM::write_header_into_buffer.
   // Seems like a function like this ought to be in HTTPHdr directly
   int bufindex;
   int dumpoffset = 0;
+  // The name dumpoffset is used here for parity with
+  // HttpSM::write_header_into_buffer, but create an alias for clarity in the
+  // use of this variable below this loop.
+  int &num_header_bytes = dumpoffset;
   int done, tmp;
   do {
     bufindex             = 0;
@@ -315,7 +318,7 @@ Http2Stream::send_request(Http2ConnectionState &cstate)
     }
   } while (!done);
 
-  if (bufindex == 0) {
+  if (num_header_bytes == 0) {
     // No data to signal read event
     return;
   }
@@ -323,11 +326,35 @@ Http2Stream::send_request(Http2ConnectionState &cstate)
   // Is the _sm ready to process the header?
   if (this->read_vio.nbytes > 0) {
     if (this->receive_end_stream) {
-      this->read_vio.nbytes = bufindex;
-      this->read_vio.ndone  = bufindex;
+      // These headers may be standard or trailer headers:
+      //
+      // * If they are standard, then there is no body (note again that the
+      // END_STREAM flag was sent with them), data_length will be 0, and
+      // num_header_bytes will simply be the length of the headers.
+      //
+      // * If they are trailers, then the tunnel behind the SM was set up after
+      // the original headers were sent, and thus nbytes should not include the
+      // size of the original standard headers. Rather, for trailers, nbytes
+      // only needs to include the body length (i.e., DATA frame payload
+      // length), and the length of these current trailer headers calculated in
+      // num_header_bytes.
+      this->read_vio.nbytes = this->data_length + num_header_bytes;
+      Http2StreamDebug("nbytes: %" PRId64 ", ndone: %" PRId64 ", 
num_header_bytes: %d, data_length: %" PRId64,
+                       this->read_vio.nbytes, this->read_vio.ndone, 
num_header_bytes, this->data_length);
       if (this->is_outbound_connection()) {
+        // This is a response header.
+        // We don't set ndone because the VC_EVENT_EOS will
+        // first flush the remaining content to consumers,
+        // after which the TUNNEL_EVENT_DONE will be fired
+        // and the header handler will be set up.
+        // The header handler will read the buffer, and not
+        // get its content from the VIO
+        // This can break if the implementation
+        // changes.
         this->signal_read_event(VC_EVENT_EOS);
       } else {
+        // Request headers.
+        this->read_vio.ndone = this->read_vio.nbytes;
         this->signal_read_event(VC_EVENT_READ_COMPLETE);
       }
     } else {
diff --git a/tests/Pipfile b/tests/Pipfile
index 51f69e8ff2..ebdd37f189 100644
--- a/tests/Pipfile
+++ b/tests/Pipfile
@@ -49,5 +49,9 @@ jsonschema = "*"
 python-jose = "*"
 pyyaml ="*"
 
+# For the grpc tests.
+grpcio = "*"
+grpcio-tools = "*"
+
 [requires]
 python_version = "3"
diff --git a/tests/gold_tests/h2/grpc/grpc.test.py 
b/tests/gold_tests/h2/grpc/grpc.test.py
new file mode 100644
index 0000000000..b7fc962aa3
--- /dev/null
+++ b/tests/gold_tests/h2/grpc/grpc.test.py
@@ -0,0 +1,143 @@
+"""Test basic gRPC traffic."""
+
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+import os
+from ports import get_port
+import sys
+
+
+class TestGrpc():
+    """Test basic gRPC traffic."""
+
+    def __init__(self, description: str):
+        """Configure a TestRun for gRPC traffic.
+
+        :param description: The description for the test runs.
+        """
+        self._description = description
+
+    def _configure_dns(self, tr: 'TestRun') -> 'Process':
+        """Configure a locally running MicroDNS server.
+
+        :param tr: The TestRun with which to associate the MicroDNS server.
+        :return: The MicroDNS server process.
+        """
+        self._dns = tr.MakeDNServer("dns", default=['127.0.0.1'])
+        return self._dns
+
+    def _configure_traffic_server(self, tr: 'TestRun', dns_port: int, 
server_port: int) -> 'Process':
+        """Configure the traffic server process.
+
+        :param tr: The TestRun with which to associate the traffic server.
+        :param dns_port: The MicroDNS server port that traffic server should 
connect to.
+        :param server_port: The gRPC server port that traffic server should 
connect to.
+        :return: The traffic server process.
+        """
+        self._ts = tr.MakeATSProcess("ts", enable_tls=True, enable_cache=False)
+
+        self._ts.addDefaultSSLFiles()
+        self._ts.Disk.ssl_multicert_config.AddLine("dest_ip=* 
ssl_cert_name=server.pem ssl_key_name=server.key")
+
+        self._ts.Disk.remap_config.AddLine(f"map / 
https://example.com:{server_port}/";)
+
+        self._ts.Disk.records_config.update(
+            {
+                "proxy.config.ssl.server.cert.path": self._ts.Variables.SSLDir,
+                "proxy.config.ssl.server.private_key.path": 
self._ts.Variables.SSLDir,
+                'proxy.config.ssl.client.alpn_protocols': 'h2,http/1.1',
+                'proxy.config.http.server_session_sharing.pool': 'thread',
+                'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE',
+                'proxy.config.dns.nameservers': f"127.0.0.1:{dns_port}",
+                'proxy.config.dns.resolv_conf': "NULL",
+                "proxy.config.diags.debug.enabled": 1,
+                "proxy.config.diags.debug.tags": "http",
+            })
+        return self._ts
+
+    def _configure_grpc_server(self, tr: 'TestRun') -> 'Process':
+        """Start the gRPC server.
+
+        :param tr: The TestRun with which to associate the gRPC server.
+        :return: The gRPC server process.
+        """
+        tr.Setup.Copy('grpc_server.py')
+        self._server = tr.Processes.Process('server')
+
+        server_pem = os.path.join(Test.Variables.AtsTestToolsDir, "ssl", 
"server.pem")
+        server_key = os.path.join(Test.Variables.AtsTestToolsDir, "ssl", 
"server.key")
+        self._server.Setup.Copy(server_pem)
+        self._server.Setup.Copy(server_key)
+
+        port = get_port(self._server, 'port')
+        command = (f'{sys.executable} {tr.RunDirectory}/grpc_server.py {port} '
+                   'server.pem server.key')
+        self._server.Command = command
+        self._server.ReturnCode = 0
+        return self._server
+
+    def _configure_grpc_client(self, tr: 'TestRun', proxy_port: int) -> None:
+        """Start the gRPC client.
+
+        :param tr: The TestRun with which to associate the gRPC client.
+        :param proxy_port: The proxy_port to which to connect.
+        """
+        tr.Setup.Copy('grpc_client.py')
+        ts_cert = os.path.join(self._ts.Variables.SSLDir, 'server.pem')
+        # The cert is for example.com, so we must use that domain.
+        hostname = 'example.com'
+        command = (f'{sys.executable} {tr.RunDirectory}/grpc_client.py '
+                   f'{hostname} {proxy_port} {ts_cert}')
+        tr.Processes.Default.Command = command
+        tr.Processes.Default.ReturnCode = 0
+
+    def _compile_protobuf_files(self) -> None:
+        """Compile the protobuf files."""
+        tr = Test.AddTestRun(f'{self._description}: compile the protobuf 
files.')
+        tr.Setup.Copy('simple.proto')
+        command = (
+            f'{sys.executable} -m grpc_tools.protoc -I{tr.RunDirectory} '
+            f'--python_out={tr.RunDirectory} 
--grpc_python_out={tr.RunDirectory} simple.proto')
+        tr.Processes.Default.Command = command
+        pb2_file = os.path.join(tr.RunDirectory, 'simple_pb2.py')
+        tr.Disk.File(pb2_file, id='pb2', exists=True)
+
+        pb2_grpc_file = os.path.join(tr.RunDirectory, 'simple_pb2_grpc.py')
+        tr.Disk.File(pb2_grpc_file, id='pb2_grpc', exists=True)
+
+    def _run_test_traffic(self) -> None:
+        """Configure the TestRun for the client and servers."""
+        tr = Test.AddTestRun(f'{self._description}: run the gRPC traffic.')
+
+        dns = self._configure_dns(tr)
+        server = self._configure_grpc_server(tr)
+        ts = self._configure_traffic_server(tr, dns.Variables.Port, 
server.Variables.port)
+
+        tr.Processes.Default.StartBefore(dns)
+        tr.Processes.Default.StartBefore(server)
+        tr.Processes.Default.StartBefore(ts)
+
+        self._configure_grpc_client(tr, ts.Variables.ssl_port)
+
+    def run(self) -> None:
+        """Configure the various test runs for the gRPC test."""
+        self._compile_protobuf_files()
+        self._run_test_traffic()
+
+
+test = TestGrpc("Test basic gRPC traffic")
+test.run()
diff --git a/tests/gold_tests/h2/grpc/grpc_client.py 
b/tests/gold_tests/h2/grpc/grpc_client.py
new file mode 100644
index 0000000000..db5990bee6
--- /dev/null
+++ b/tests/gold_tests/h2/grpc/grpc_client.py
@@ -0,0 +1,74 @@
+"""A gRPC client."""
+
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+import argparse
+import grpc
+import os
+import sys
+
+import simple_pb2
+import simple_pb2_grpc
+
+
+def run_grpc_client(hostname: str, proxy_port: int, proxy_cert: bytes) -> int:
+    """Run the gRPC client.
+
+    :param hostname: The hostname to which to connect.
+    :param proxy_port: The ATS port to which to connect.
+    :param proxy_cert: The public TLS certificate to verify ATS against.
+    :return: The exit code.
+    """
+    credentials = grpc.ssl_channel_credentials(root_certificates=proxy_cert)
+    channel_options = (('grpc.ssl_target_name_override', hostname),)
+    destination_endpoint = f'127.0.0.1:{proxy_port}'
+    channel = grpc.secure_channel(destination_endpoint, credentials, 
options=channel_options)
+    print(f'Connecting to: {destination_endpoint}')
+    stub = simple_pb2_grpc.SimpleStub(channel)
+
+    message = simple_pb2.SimpleRequest(message="Client request message")
+    response = stub.SimpleMethod(message)
+    print(f'Response received from server: {response.message}')
+    return 0
+
+
+def parse_args() -> argparse.Namespace:
+    """Parse command line arguments."""
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument('hostname', help='The hostname to which to connect.')
+
+    parser.add_argument('proxy_port', type=int, help='The ATS port to which to 
connect.')
+    parser.add_argument('proxy_cert', type=argparse.FileType('rb'), help='The 
public TLS certificate to use.')
+    return parser.parse_args()
+
+
+def main() -> int:
+    """Run the main entry point for the gRPC client.
+
+    :return: The exit code.
+    """
+    args = parse_args()
+
+    try:
+        return run_grpc_client(args.hostname, args.proxy_port, 
args.proxy_cert.read())
+    except grpc.RpcError as e:
+        print(f'RPC failed with code {e.code()}: {e.details()}')
+        return 1
+
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/tests/gold_tests/h2/grpc/grpc_server.py 
b/tests/gold_tests/h2/grpc/grpc_server.py
new file mode 100644
index 0000000000..109b303cc3
--- /dev/null
+++ b/tests/gold_tests/h2/grpc/grpc_server.py
@@ -0,0 +1,81 @@
+"""A gRPC server."""
+
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+import argparse
+from concurrent import futures
+import grpc
+import sys
+import time
+
+import simple_pb2
+import simple_pb2_grpc
+
+
+class SimpleServicer(simple_pb2_grpc.SimpleServicer):
+    """A gRPC servicer."""
+
+    def SimpleMethod(self, request, context):
+        """An example gRPC method."""
+        print(f'Request received from client: {request.message}')
+        response = simple_pb2.SimpleResponse(message=f"Echo: 
{request.message}")
+        return response
+
+
+def run_grpc_server(port: int, server_cert: str, server_key: str) -> int:
+    """Run the gRPC server.
+
+    :param port: The port on which to listen.
+    :param server_cert: The public TLS certificate to use.
+    :param server_key: The private TLS key to use.
+    :return: The exit code.
+    """
+    credentials = grpc.ssl_server_credentials([(server_key, server_cert)])
+    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
+    simple_pb2_grpc.add_SimpleServicer_to_server(SimpleServicer(), server)
+    server_endpoint = f'127.0.0.1:{port}'
+    server.add_secure_port(server_endpoint, credentials)
+    print(f'Listening on: {server_endpoint}')
+    server.start()
+    try:
+        server.wait_for_termination()
+    except KeyboardInterrupt:
+        print("Keyboard interrupt received, exiting.")
+        return 0
+    return 0
+
+
+def parse_args() -> argparse.Namespace:
+    """Parse command line arguments."""
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument('port', type=int, help='The port on which to listen.')
+    parser.add_argument('server_crt', type=argparse.FileType('rb'), help="The 
public TLS certificate to use.")
+    parser.add_argument('server_key', type=argparse.FileType('rb'), help="The 
private TLS key to use.")
+    return parser.parse_args()
+
+
+def main() -> int:
+    """Run the main entry point for the gRPC server.
+
+    :return: The exit code.
+    """
+    args = parse_args()
+    return run_grpc_server(args.port, args.server_crt.read(), 
args.server_key.read())
+
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/tests/gold_tests/h2/grpc/simple.proto 
b/tests/gold_tests/h2/grpc/simple.proto
new file mode 100644
index 0000000000..d8f2aa106b
--- /dev/null
+++ b/tests/gold_tests/h2/grpc/simple.proto
@@ -0,0 +1,38 @@
+/** @file
+
+  The gRPC protocol buffer definition for the gRPC autest.
+
+  @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.
+ */
+
+syntax = "proto3";
+
+package simple;
+
+service Simple {
+    rpc SimpleMethod(SimpleRequest) returns (SimpleResponse) {}
+}
+
+message SimpleRequest {
+    string message = 1;
+}
+
+message SimpleResponse {
+    string message = 1;
+}

Reply via email to