xref: /aosp_15_r20/external/grpc-grpc/src/ruby/end2end/call_credentials_timeout_test.rb (revision cc02d7e222339f7a4f6ba5f422e6413f4bd931f2)
1#!/usr/bin/env ruby
2#
3# Copyright 2016 gRPC authors.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17this_dir = File.expand_path(File.dirname(__FILE__))
18protos_lib_dir = File.join(this_dir, 'lib')
19grpc_lib_dir = File.join(File.dirname(this_dir), 'lib')
20$LOAD_PATH.unshift(grpc_lib_dir) unless $LOAD_PATH.include?(grpc_lib_dir)
21$LOAD_PATH.unshift(protos_lib_dir) unless $LOAD_PATH.include?(protos_lib_dir)
22$LOAD_PATH.unshift(this_dir) unless $LOAD_PATH.include?(this_dir)
23
24require 'grpc'
25require 'end2end_common'
26
27def create_channel_creds
28  test_root = File.join(File.dirname(__FILE__), '..', 'spec', 'testdata')
29  files = ['ca.pem', 'client.key', 'client.pem']
30  creds = files.map { |f| File.open(File.join(test_root, f)).read }
31  GRPC::Core::ChannelCredentials.new(creds[0], creds[1], creds[2])
32end
33
34def client_cert
35  test_root = File.join(File.dirname(__FILE__), '..', 'spec', 'testdata')
36  cert = File.open(File.join(test_root, 'client.pem')).read
37  fail unless cert.is_a?(String)
38  cert
39end
40
41def create_server_creds
42  test_root = File.join(File.dirname(__FILE__), '..', 'spec', 'testdata')
43  GRPC.logger.info("test root: #{test_root}")
44  files = ['ca.pem', 'server1.key', 'server1.pem']
45  creds = files.map { |f| File.open(File.join(test_root, f)).read }
46  GRPC::Core::ServerCredentials.new(
47    creds[0],
48    [{ private_key: creds[1], cert_chain: creds[2] }],
49    true) # force client auth
50end
51
52def check_rpcs_still_possible(stub)
53  # Expect three more RPCs to succeed. Use a retry loop because the server
54  # thread pool might still be busy processing the previous round of RPCs.
55  3.times do
56    timeout_seconds = 30
57    deadline = Time.now + timeout_seconds
58    success = false
59    while Time.now < deadline
60      STDERR.puts 'now perform another RPC and expect OK...'
61      begin
62        stub.echo(Echo::EchoRequest.new(request: 'hello'), deadline: Time.now + 10)
63        success = true
64        break
65      rescue GRPC::BadStatus => e
66        STDERR.puts "RPC received status: #{e}. Try again..."
67      end
68    end
69    unless success
70      fail "failed to complete a successful RPC within #{timeout_seconds} seconds"
71    end
72  end
73end
74
75# rubocop:disable Metrics/MethodLength
76def main
77  server_runner = ServerRunner.new(EchoServerImpl)
78  server_runner.server_creds = create_server_creds
79  server_port = server_runner.run
80  channel_args = {
81    GRPC::Core::Channel::SSL_TARGET => 'foo.test.google.fr'
82  }
83  token_fetch_attempts = MutableValue.new(0)
84  token_fetch_attempts_mu = Mutex.new
85  jwt_aud_uri_extraction_success_count = MutableValue.new(0)
86  jwt_aud_uri_extraction_success_count_mu = Mutex.new
87  expected_jwt_aud_uri = 'https://foo.test.google.fr/echo.EchoServer'
88  jwt_aud_uri_failure_values = []
89  times_out_first_time_auth_proc = proc do |args|
90    # We check the value of jwt_aud_uri not necessarily as a test for
91    # the correctness of jwt_aud_uri w.r.t. its expected semantics, but
92    # more for as an indirect way to check for memory corruption.
93    jwt_aud_uri_extraction_success_count_mu.synchronize do
94      if args[:jwt_aud_uri] == expected_jwt_aud_uri
95        jwt_aud_uri_extraction_success_count.value += 1
96      else
97        jwt_aud_uri_failure_values << args[:jwt_aud_uri]
98      end
99    end
100    token_fetch_attempts_mu.synchronize do
101      old_val = token_fetch_attempts.value
102      token_fetch_attempts.value += 1
103      if old_val.zero?
104        STDERR.puts 'call creds plugin sleeping for 4 seconds'
105        sleep 4
106        STDERR.puts 'call creds plugin done with 4 second sleep'
107        raise 'test exception thrown purposely from call creds plugin'
108      end
109    end
110    { 'authorization' => 'fake_val' }.merge(args)
111  end
112  channel_creds = create_channel_creds.compose(
113    GRPC::Core::CallCredentials.new(times_out_first_time_auth_proc))
114  stub = Echo::EchoServer::Stub.new("localhost:#{server_port}",
115                                    channel_creds,
116                                    channel_args: channel_args)
117  STDERR.puts 'perform a first few RPCs to try to get things into a bad state...'
118  threads = []
119  got_at_least_one_failure = MutableValue.new(false)
120  2000.times do
121    threads << Thread.new do
122      begin
123        # 2 seconds is chosen as deadline here because it is less than the 4 second
124        # sleep that the first call creds user callback does. The idea here is that
125        # a lot of RPCs will be made concurrently all with 2 second deadlines, and they
126        # will all queue up onto the call creds user callback thread, and will all
127        # have to wait for the first 4 second sleep to finish. When the deadlines
128        # of the associated calls fire ~2 seconds in, some of their C-core data
129        # will have ownership dropped, and they will hit the user-after-free in
130        # https://github.com/grpc/grpc/issues/19195 if this isn't handled correctly.
131        stub.echo(Echo::EchoRequest.new(request: 'hello'), deadline: Time.now + 2)
132      rescue GRPC::BadStatus
133        got_at_least_one_failure.value = true
134        # We don't care if these RPCs succeed or fail. The purpose of these
135        # RPCs is just to try to induce a specific use-after-free bug, and to get
136        # the call credentials callback thread into a bad state.
137      end
138    end
139  end
140  threads.each(&:join)
141  unless got_at_least_one_failure.value
142    fail 'expected at least one of the initial RPCs to fail'
143  end
144  check_rpcs_still_possible(stub)
145  jwt_aud_uri_extraction_success_count_mu.synchronize do
146    if jwt_aud_uri_extraction_success_count.value < 4
147      fail "Expected auth metadata plugin callback to be ran with the jwt_aud_uri
148parameter matching its expected value at least 4 times (at least 1 out of the 2000
149initial expected-to-timeout RPCs should have caused this by now, and all three of the
150successful RPCs should have caused this). This test isn't doing what it's meant to do."
151    end
152    unless jwt_aud_uri_failure_values.empty?
153      fail "Expected to get jwt_aud_uri:#{expected_jwt_aud_uri} passed to call creds
154user callback every time that it was invoked, but it did not match the expected value
155in #{jwt_aud_uri_failure_values.size} invocations. This suggests that either:
156a) the expected jwt_aud_uri value is incorrect
157b) there is some corruption of the jwt_aud_uri argument
158Here are are the values of the jwt_aud_uri parameter that were passed to the call
159creds user callback that did not match #{expected_jwt_aud_uri}:
160#{jwt_aud_uri_failure_values}"
161    end
162  end
163  server_runner.stop
164end
165
166main
167