| # Copyright 2015 gRPC authors. |
| # |
| # 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. |
| |
| require 'spec_helper' |
| |
| include GRPC::Core |
| |
| shared_examples 'basic GRPC message delivery is OK' do |
| include GRPC::Core |
| |
| context 'the test channel' do |
| it 'should have a target' do |
| expect(@ch.target).to be_a(String) |
| end |
| end |
| |
| it 'unary calls work' do |
| run_services_on_server(@server, services: [EchoService]) do |
| call = @stub.an_rpc(EchoMsg.new, return_op: true) |
| expect(call.execute).to be_a(EchoMsg) |
| end |
| end |
| |
| it 'unary calls work when enabling compression' do |
| run_services_on_server(@server, services: [EchoService]) do |
| long_request_str = '0' * 2000 |
| md = { 'grpc-internal-encoding-request' => 'gzip' } |
| call = @stub.an_rpc(EchoMsg.new(msg: long_request_str), |
| return_op: true, |
| metadata: md) |
| response = call.execute |
| expect(response).to be_a(EchoMsg) |
| expect(response.msg).to eq(long_request_str) |
| end |
| end |
| |
| def client_cancel_test(cancel_proc, expected_code, |
| expected_details) |
| call = @stub.an_rpc(EchoMsg.new, return_op: true) |
| run_services_on_server(@server, services: [EchoService]) do |
| # start the call, but don't send a message yet |
| call.start_call |
| # cancel the call |
| cancel_proc.call(call) |
| # check the client's status |
| failed = false |
| begin |
| call.execute |
| rescue GRPC::BadStatus => e |
| failed = true |
| expect(e.code).to be expected_code |
| expect(e.details).to eq expected_details |
| end |
| expect(failed).to be(true) |
| end |
| end |
| |
| it 'clients can cancel a call on the server' do |
| expected_code = StatusCodes::CANCELLED |
| expected_details = 'CANCELLED' |
| cancel_proc = proc(&:cancel) |
| client_cancel_test(cancel_proc, expected_code, expected_details) |
| end |
| |
| it 'cancel_with_status unknown status' do |
| code = StatusCodes::UNKNOWN |
| details = 'test unknown reason' |
| cancel_proc = proc { |call| call.cancel_with_status(code, details) } |
| client_cancel_test(cancel_proc, code, details) |
| end |
| |
| it 'cancel_with_status unknown status' do |
| code = StatusCodes::FAILED_PRECONDITION |
| details = 'test failed precondition reason' |
| cancel_proc = proc { |call| call.cancel_with_status(code, details) } |
| client_cancel_test(cancel_proc, code, details) |
| end |
| end |
| |
| shared_examples 'GRPC metadata delivery works OK' do |
| describe 'from client => server' do |
| before(:example) do |
| n = 7 # arbitrary number of metadata |
| diff_keys_fn = proc { |i| [format('k%d', i), format('v%d', i)] } |
| diff_keys = Hash[n.times.collect { |x| diff_keys_fn.call x }] |
| null_vals_fn = proc { |i| [format('k%d', i), format('v\0%d', i)] } |
| null_vals = Hash[n.times.collect { |x| null_vals_fn.call x }] |
| same_keys_fn = proc { |i| [format('k%d', i), [format('v%d', i)] * n] } |
| same_keys = Hash[n.times.collect { |x| same_keys_fn.call x }] |
| symbol_key = { a_key: 'a val' } |
| @valid_metadata = [diff_keys, same_keys, null_vals, symbol_key] |
| @bad_keys = [] |
| @bad_keys << { Object.new => 'a value' } |
| @bad_keys << { 1 => 'a value' } |
| end |
| |
| it 'raises an exception if a metadata key is invalid' do |
| @bad_keys.each do |md| |
| # NOTE: no need to run a server in this test b/c the failure |
| # happens while validating metadata to send. |
| failed = false |
| begin |
| @stub.an_rpc(EchoMsg.new, metadata: md) |
| rescue TypeError => e |
| failed = true |
| expect(e.message).to eq('grpc_rb_md_ary_fill_hash_cb: bad type for key parameter') |
| end |
| expect(failed).to be(true) |
| end |
| end |
| |
| it 'sends all the metadata pairs when keys and values are valid' do |
| service = EchoService.new |
| run_services_on_server(@server, services: [service]) do |
| @valid_metadata.each_with_index do |md, i| |
| expect(@stub.an_rpc(EchoMsg.new, metadata: md)).to be_a(EchoMsg) |
| # confirm the server can receive the client metadata |
| # finish the call |
| expect(service.received_md.length).to eq(i + 1) |
| md.each do |k, v| |
| expect(service.received_md[i][k.to_s]).to eq(v) |
| end |
| end |
| end |
| end |
| end |
| |
| describe 'from server => client' do |
| before(:example) do |
| n = 7 # arbitrary number of metadata |
| diff_keys_fn = proc { |i| [format('k%d', i), format('v%d', i)] } |
| diff_keys = Hash[n.times.collect { |x| diff_keys_fn.call x }] |
| null_vals_fn = proc { |i| [format('k%d', i), format('v\0%d', i)] } |
| null_vals = Hash[n.times.collect { |x| null_vals_fn.call x }] |
| same_keys_fn = proc { |i| [format('k%d', i), [format('v%d', i)] * n] } |
| same_keys = Hash[n.times.collect { |x| same_keys_fn.call x }] |
| symbol_key = { a_key: 'a val' } |
| @valid_metadata = [diff_keys, same_keys, null_vals, symbol_key] |
| @bad_keys = [] |
| @bad_keys << { Object.new => 'a value' } |
| @bad_keys << { 1 => 'a value' } |
| end |
| |
| it 'raises an exception if a metadata key is invalid' do |
| service = EchoService.new |
| run_services_on_server(@server, services: [service]) do |
| @bad_keys.each do |md| |
| proceed = Queue.new |
| server_exception = nil |
| service.on_call_started = proc do |call| |
| call.send_initial_metadata(md) |
| rescue TypeError => e |
| server_exception = e |
| ensure |
| proceed.push(1) |
| end |
| client_exception = nil |
| client_call = @stub.an_rpc(EchoMsg.new, return_op: true) |
| thr = Thread.new do |
| client_call.execute |
| rescue GRPC::BadStatus => e |
| client_exception = e |
| end |
| proceed.pop |
| # TODO(apolcyn): we shouldn't need this cancel here. It's |
| # only currently needed b/c the server does not seem to properly |
| # terminate the RPC if it fails to send initial metadata. That |
| # should be fixed, in which case this cancellation can be removed. |
| client_call.cancel |
| thr.join |
| p client_exception |
| expect(client_exception.nil?).to be(false) |
| expect(server_exception.nil?).to be(false) |
| expect(server_exception.message).to eq( |
| 'grpc_rb_md_ary_fill_hash_cb: bad type for key parameter') |
| end |
| end |
| end |
| |
| it 'sends an empty hash if no metadata is added' do |
| run_services_on_server(@server, services: [EchoService]) do |
| call = @stub.an_rpc(EchoMsg.new, return_op: true) |
| expect(call.execute).to be_a(EchoMsg) |
| expect(call.metadata).to eq({}) |
| end |
| end |
| |
| it 'sends all the pairs when keys and values are valid' do |
| service = EchoService.new |
| run_services_on_server(@server, services: [service]) do |
| @valid_metadata.each do |md| |
| service.on_call_started = proc do |call| |
| call.send_initial_metadata(md) |
| end |
| call = @stub.an_rpc(EchoMsg.new, return_op: true) |
| call.execute |
| replace_symbols = Hash[md.each_pair.collect { |x, y| [x.to_s, y] }] |
| expect(call.metadata).to eq(replace_symbols) |
| end |
| end |
| end |
| end |
| end |
| |
| describe 'the http client/server' do |
| before(:example) do |
| server_host = '0.0.0.0:0' |
| @server = new_rpc_server_for_testing |
| server_port = @server.add_http2_port(server_host, :this_port_is_insecure) |
| @ch = Channel.new("0.0.0.0:#{server_port}", nil, :this_channel_is_insecure) |
| @stub = EchoStub.new( |
| "0.0.0.0:#{server_port}", nil, channel_override: @ch) |
| end |
| |
| it_behaves_like 'basic GRPC message delivery is OK' do |
| end |
| |
| it_behaves_like 'GRPC metadata delivery works OK' do |
| end |
| end |
| |
| describe 'the secure http client/server' do |
| def load_test_certs |
| test_root = File.join(File.dirname(__FILE__), 'testdata') |
| files = ['ca.pem', 'server1.key', 'server1.pem'] |
| files.map { |f| File.open(File.join(test_root, f)).read } |
| end |
| |
| before(:example) do |
| certs = load_test_certs |
| server_host = '0.0.0.0:0' |
| server_creds = GRPC::Core::ServerCredentials.new( |
| nil, [{ private_key: certs[1], cert_chain: certs[2] }], false) |
| @server = new_rpc_server_for_testing |
| server_port = @server.add_http2_port(server_host, server_creds) |
| args = { Channel::SSL_TARGET => 'foo.test.google.fr' } |
| @ch = Channel.new( |
| "0.0.0.0:#{server_port}", args, |
| GRPC::Core::ChannelCredentials.new(certs[0], nil, nil)) |
| @stub = EchoStub.new( |
| "0.0.0.0:#{server_port}", nil, channel_override: @ch) |
| end |
| |
| it_behaves_like 'basic GRPC message delivery is OK' do |
| end |
| |
| it_behaves_like 'GRPC metadata delivery works OK' do |
| end |
| |
| it 'modifies metadata with CallCredentials' do |
| # create call creds |
| auth_proc = proc { { 'k1' => 'v1' } } |
| call_creds = GRPC::Core::CallCredentials.new(auth_proc) |
| # create arbitrary custom metadata |
| custom_md = { 'k2' => 'v2' } |
| # perform an RPC |
| echo_service = EchoService.new |
| run_services_on_server(@server, services: [echo_service]) do |
| expect(@stub.an_rpc(EchoMsg.new, |
| credentials: call_creds, |
| metadata: custom_md)).to be_a(EchoMsg) |
| end |
| # call creds metadata should be merged with custom MD |
| expect(echo_service.received_md.length).to eq(1) |
| expected_md = { 'k1' => 'v1', 'k2' => 'v2' } |
| expected_md.each do |k, v| |
| expect(echo_service.received_md[0][k]).to eq(v) |
| end |
| end |
| |
| it 'plugin procs survive GC after their CallCredentials wrapper is collected' do |
| # Regression test for use-after-free in grpc_metadata_credentials_plugin. |
| # When CallCredentials are composed via a.compose(b), the resulting |
| # wrapper marks b but NOT a. If a's Ruby wrapper is GC'd, the C-level |
| # composite still holds a's plugin. Without the pin_plugin_proc fix, |
| # a's proc would be collected, and invoking the callback would crash |
| # with a segfault (rb_id_table_lookup on freed memory). |
| first_call_count = 0 |
| |
| first_creds = GRPC::Core::CallCredentials.new(proc { |
| first_call_count += 1 |
| { 'first-key' => 'first-value' } |
| }) |
| second_creds = GRPC::Core::CallCredentials.new(proc { |
| { 'second-key' => 'second-value' } |
| }) |
| composed = first_creds.compose(second_creds) |
| |
| # Drop references. composed.mark includes second_creds but NOT first_creds, |
| # so first_creds' Ruby wrapper becomes GC-eligible while its C plugin |
| # survives inside the composite. |
| first_creds = nil # rubocop:disable Lint/UselessAssignment |
| second_creds = nil # rubocop:disable Lint/UselessAssignment |
| |
| # Force GC to collect the first_creds wrapper. Without the fix, the proc |
| # from first_creds would also be collected here. |
| GC.start(full_mark: true, immediate_sweep: true) |
| |
| # Make an RPC with the composed credentials — both plugin callbacks fire. |
| # Without the fix, accessing the freed proc would crash the process. |
| echo_service = EchoService.new |
| run_services_on_server(@server, services: [echo_service]) do |
| expect(@stub.an_rpc(EchoMsg.new, |
| credentials: composed)).to be_a(EchoMsg) |
| end |
| expect(first_call_count).to eq(1) |
| expect(echo_service.received_md[0]['first-key']).to eq('first-value') |
| expect(echo_service.received_md[0]['second-key']).to eq('second-value') |
| end |
| |
| it 'modifies large metadata with CallCredentials' do |
| val_array = %w( |
| '00000000000000000000000000000000000000000000000000000000000000', |
| '11111111111111111111111111111111111111111111111111111111111111', |
| ) |
| # create call creds |
| auth_proc = proc do |
| { |
| k2: val_array, |
| k3: '0000000000000000000000000000000000000000000000000000000000', |
| keeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeey4: 'v4' |
| } |
| end |
| call_creds = GRPC::Core::CallCredentials.new(auth_proc) |
| # create arbitrary custom metadata |
| custom_md = { k1: 'v1' } |
| # perform an RPC |
| echo_service = EchoService.new |
| run_services_on_server(@server, services: [echo_service]) do |
| expect(@stub.an_rpc(EchoMsg.new, |
| credentials: call_creds, |
| metadata: custom_md)).to be_a(EchoMsg) |
| end |
| # call creds metadata should be merged with custom MD |
| expect(echo_service.received_md.length).to eq(1) |
| expected_md = { |
| k1: 'v1', |
| k2: val_array, |
| k3: '0000000000000000000000000000000000000000000000000000000000', |
| keeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeey4: 'v4' |
| } |
| expected_md.each do |k, v| |
| expect(echo_service.received_md[0][k.to_s]).to eq(v) |
| end |
| end |
| end |