diff --git a/include/proxy/http/HttpSM.h b/include/proxy/http/HttpSM.h index ae14d4d11df..6f28523d0b0 100644 --- a/include/proxy/http/HttpSM.h +++ b/include/proxy/http/HttpSM.h @@ -446,6 +446,7 @@ class HttpSM : public Continuation, public PluginUserArgs 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 ffb811cd718..72515f1fb68 100644 --- a/include/proxy/http2/Http2Stream.h +++ b/include/proxy/http2/Http2Stream.h @@ -82,7 +82,7 @@ class Http2Stream : public ProxyTransaction 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 @@ class Http2Stream : public ProxyTransaction History _history; Milestones(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 9d737edcc12..c8e9a1ada95 100644 --- a/include/tscore/ink_string++.h +++ b/include/tscore/ink_string++.h @@ -32,6 +32,7 @@ #pragma once #include +#include #include /*********************************************************************** diff --git a/src/proxy/hdrs/HdrToken.cc b/src/proxy/hdrs/HdrToken.cc index 2b7a30b7fb6..104350feabe 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 21d446c4f17..b3b2aa4ca12 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 3384c62863d..9080263001f 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 9f716cdc70c..0dd8e53d941 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 fac543b6bfa..d958151fc4b 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 @@ -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 51f69e8ff2a..ebdd37f1898 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 00000000000..b7fc962aa37 --- /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 00000000000..db5990bee60 --- /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 00000000000..109b303cc38 --- /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 00000000000..d8f2aa106bb --- /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; +}