diff --git a/src/php/ext/grpc/call.c b/src/php/ext/grpc/call.c index 3b99de75382..7ba14a38d8e 100644 --- a/src/php/ext/grpc/call.c +++ b/src/php/ext/grpc/call.c @@ -42,13 +42,13 @@ #include #include #include "php_grpc.h" +#include "call_credentials.h" #include #include #include -#include #include #include @@ -515,11 +515,41 @@ PHP_METHOD(Call, cancel) { grpc_call_cancel(call->wrapped, NULL); } +/** + * Set the CallCredentials for this call. + * @param CallCredentials creds_obj The CallCredentials object + * @param int The error code + */ +PHP_METHOD(Call, setCredentials) { + zval *creds_obj; + + /* "O" == 1 Object */ + if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "O", &creds_obj, + grpc_ce_call_credentials) == FAILURE) { + zend_throw_exception(spl_ce_InvalidArgumentException, + "setCredentials expects 1 CallCredentials", + 1 TSRMLS_CC); + return; + } + + wrapped_grpc_call_credentials *creds = + (wrapped_grpc_call_credentials *)zend_object_store_get_object( + creds_obj TSRMLS_CC); + + wrapped_grpc_call *call = + (wrapped_grpc_call *)zend_object_store_get_object(getThis() TSRMLS_CC); + + grpc_call_error error = GRPC_CALL_ERROR; + error = grpc_call_set_credentials(call->wrapped, creds->wrapped); + RETURN_LONG(error); +} + static zend_function_entry call_methods[] = { PHP_ME(Call, __construct, NULL, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR) PHP_ME(Call, startBatch, NULL, ZEND_ACC_PUBLIC) PHP_ME(Call, getPeer, NULL, ZEND_ACC_PUBLIC) PHP_ME(Call, cancel, NULL, ZEND_ACC_PUBLIC) + PHP_ME(Call, setCredentials, NULL, ZEND_ACC_PUBLIC) PHP_FE_END}; void grpc_init_call(TSRMLS_D) { diff --git a/src/php/ext/grpc/call.h b/src/php/ext/grpc/call.h index f3ef89dc976..73efadae351 100644 --- a/src/php/ext/grpc/call.h +++ b/src/php/ext/grpc/call.h @@ -66,4 +66,8 @@ zval *grpc_php_wrap_call(grpc_call *wrapped, bool owned); * call metadata */ zval *grpc_parse_metadata_array(grpc_metadata_array *metadata_array); +/* Populates a grpc_metadata_array with the data in a PHP array object. + Returns true on success and false on failure */ +bool create_metadata_array(zval *array, grpc_metadata_array *metadata); + #endif /* NET_GRPC_PHP_GRPC_CHANNEL_H_ */ diff --git a/src/php/ext/grpc/call_credentials.c b/src/php/ext/grpc/call_credentials.c index 17f167befb0..c6f674edd9e 100644 --- a/src/php/ext/grpc/call_credentials.c +++ b/src/php/ext/grpc/call_credentials.c @@ -43,6 +43,7 @@ #include #include #include "php_grpc.h" +#include "call.h" #include #include @@ -103,7 +104,7 @@ PHP_METHOD(CallCredentials, createComposite) { zval *cred1_obj; zval *cred2_obj; - /* "OO" == 3 Objects */ + /* "OO" == 2 Objects */ if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "OO", &cred1_obj, grpc_ce_call_credentials, &cred2_obj, grpc_ce_call_credentials) == FAILURE) { @@ -125,9 +126,106 @@ PHP_METHOD(CallCredentials, createComposite) { RETURN_DESTROY_ZVAL(creds_object); } +/** + * Create a call credentials object from the plugin API + * @param function callback The callback function + * @return CallCredentials The new call credentials object + */ +PHP_METHOD(CallCredentials, createFromPlugin) { + zend_fcall_info *fci; + zend_fcall_info_cache *fci_cache; + + fci = (zend_fcall_info *)emalloc(sizeof(zend_fcall_info)); + fci_cache = (zend_fcall_info_cache *)emalloc(sizeof(zend_fcall_info_cache)); + memset(fci, 0, sizeof(zend_fcall_info)); + memset(fci_cache, 0, sizeof(zend_fcall_info_cache)); + + /* "f" == 1 function */ + if (zend_parse_parameters(ZEND_NUM_ARGS(), "f", fci, + fci_cache, + fci->params, + fci->param_count) == FAILURE) { + zend_throw_exception(spl_ce_InvalidArgumentException, + "createFromPlugin expects 1 callback", + 1 TSRMLS_CC); + return; + } + + plugin_state *state; + state = (plugin_state *)emalloc(sizeof(plugin_state)); + memset(state, 0, sizeof(plugin_state)); + + /* save the user provided PHP callback function */ + state->fci = fci; + state->fci_cache = fci_cache; + + grpc_metadata_credentials_plugin plugin; + plugin.get_metadata = plugin_get_metadata; + plugin.destroy = plugin_destroy_state; + plugin.state = (void *)state; + plugin.type = ""; + + grpc_call_credentials *creds = grpc_metadata_credentials_create_from_plugin( + plugin, NULL); + zval *creds_object = grpc_php_wrap_call_credentials(creds); + RETURN_DESTROY_ZVAL(creds_object); +} + +/* Callback function for plugin creds API */ +void plugin_get_metadata(void *ptr, grpc_auth_metadata_context context, + grpc_credentials_plugin_metadata_cb cb, + void *user_data) { + plugin_state *state = (plugin_state *)ptr; + + /* prepare to call the user callback function with info from the + * grpc_auth_metadata_context */ + zval **params[1]; + zval *arg; + zval *retval; + MAKE_STD_ZVAL(arg); + ZVAL_STRING(arg, context.service_url, 1); + params[0] = &arg; + /* TODO: Need to pass user_data as well? */ + state->fci->param_count = 1; + state->fci->params = params; + state->fci->retval_ptr_ptr = &retval; + + /* call the user callback function */ + zend_call_function(state->fci, state->fci_cache); + + if (Z_TYPE_P(retval) != IS_ARRAY) { + zend_throw_exception(spl_ce_InvalidArgumentException, + "plugin callback must return metadata array", + 1 TSRMLS_CC); + } + + grpc_metadata_array metadata; + if (!create_metadata_array(retval, &metadata)) { + zend_throw_exception(spl_ce_InvalidArgumentException, + "invalid metadata", 1 TSRMLS_CC); + grpc_metadata_array_destroy(&metadata); + } + + /* TODO: handle error */ + grpc_status_code code = GRPC_STATUS_OK; + + /* Pass control back to core */ + cb(user_data, metadata.metadata, metadata.count, code, NULL); +} + +/* Cleanup function for plugin creds API */ +void plugin_destroy_state(void *ptr) { + plugin_state *state = (plugin_state *)ptr; + efree(state->fci); + efree(state->fci_cache); + efree(state); +} + static zend_function_entry call_credentials_methods[] = { PHP_ME(CallCredentials, createComposite, NULL, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + PHP_ME(CallCredentials, createFromPlugin, NULL, + ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) PHP_FE_END}; void grpc_init_call_credentials(TSRMLS_D) { diff --git a/src/php/ext/grpc/call_credentials.h b/src/php/ext/grpc/call_credentials.h index 8f35ac68bc1..d2f6a92449b 100755 --- a/src/php/ext/grpc/call_credentials.h +++ b/src/php/ext/grpc/call_credentials.h @@ -57,6 +57,20 @@ typedef struct wrapped_grpc_call_credentials { grpc_call_credentials *wrapped; } wrapped_grpc_call_credentials; +/* Struct to hold callback function for plugin creds API */ +typedef struct plugin_state { + zend_fcall_info *fci; + zend_fcall_info_cache *fci_cache; +} plugin_state; + +/* Callback function for plugin creds API */ +void plugin_get_metadata(void *state, grpc_auth_metadata_context context, + grpc_credentials_plugin_metadata_cb cb, + void *user_data); + +/* Cleanup function for plugin creds API */ +void plugin_destroy_state(void *ptr); + /* Initializes the CallCredentials PHP class */ void grpc_init_call_credentials(TSRMLS_D); diff --git a/src/php/lib/Grpc/AbstractCall.php b/src/php/lib/Grpc/AbstractCall.php index 53849d51fca..c80cf4464e5 100644 --- a/src/php/lib/Grpc/AbstractCall.php +++ b/src/php/lib/Grpc/AbstractCall.php @@ -43,19 +43,20 @@ abstract class AbstractCall /** * Create a new Call wrapper object. * - * @param Channel $channel The channel to communicate on - * @param string $method The method to call on the - * remote server - * @param callback $deserialize A callback function to deserialize - * the response - * @param (optional) long $timeout Timeout in microseconds + * @param Channel $channel The channel to communicate on + * @param string $method The method to call on the + * remote server + * @param callback $deserialize A callback function to deserialize + * the response + * @param array $options Call options (optional) */ public function __construct(Channel $channel, $method, $deserialize, - $timeout = false) + $options = []) { - if ($timeout) { + if (isset($options['timeout']) && + is_numeric($timeout = $options['timeout'])) { $now = Timeval::now(); $delta = new Timeval($timeout); $deadline = $now->add($delta); @@ -65,6 +66,13 @@ abstract class AbstractCall $this->call = new Call($channel, $method, $deadline); $this->deserialize = $deserialize; $this->metadata = null; + if (isset($options['call_credentials_callback']) && + is_callable($call_credentials_callback = + $options['call_credentials_callback'])) { + $call_credentials = CallCredentials::createFromPlugin( + $call_credentials_callback); + $this->call->setCredentials($call_credentials); + } } /** diff --git a/src/php/lib/Grpc/BaseStub.php b/src/php/lib/Grpc/BaseStub.php index 7b2627f516d..8e9dedf73b5 100755 --- a/src/php/lib/Grpc/BaseStub.php +++ b/src/php/lib/Grpc/BaseStub.php @@ -158,25 +158,6 @@ class BaseStub return 'https://'.$this->hostname.$service_name; } - /** - * extract $timeout from $metadata. - * - * @param $metadata The metadata map - * - * @return list($metadata_copy, $timeout) - */ - private function _extract_timeout_from_metadata($metadata) - { - $timeout = false; - $metadata_copy = $metadata; - if (isset($metadata['timeout'])) { - $timeout = $metadata['timeout']; - unset($metadata_copy['timeout']); - } - - return [$metadata_copy, $timeout]; - } - /** * validate and normalize the metadata array. * @@ -220,21 +201,19 @@ class BaseStub $metadata = [], $options = []) { - list($actual_metadata, $timeout) = - $this->_extract_timeout_from_metadata($metadata); $call = new UnaryCall($this->channel, $method, $deserialize, - $timeout); + $options); $jwt_aud_uri = $this->_get_jwt_aud_uri($method); if (is_callable($this->update_metadata)) { - $actual_metadata = call_user_func($this->update_metadata, - $actual_metadata, + $metadata = call_user_func($this->update_metadata, + $metadata, $jwt_aud_uri); } - $actual_metadata = $this->_validate_and_normalize_metadata( - $actual_metadata); - $call->start($argument, $actual_metadata, $options); + $metadata = $this->_validate_and_normalize_metadata( + $metadata); + $call->start($argument, $metadata, $options); return $call; } @@ -253,23 +232,22 @@ class BaseStub */ public function _clientStreamRequest($method, callable $deserialize, - $metadata = []) + $metadata = [], + $options = []) { - list($actual_metadata, $timeout) = - $this->_extract_timeout_from_metadata($metadata); $call = new ClientStreamingCall($this->channel, $method, $deserialize, - $timeout); + $options); $jwt_aud_uri = $this->_get_jwt_aud_uri($method); if (is_callable($this->update_metadata)) { - $actual_metadata = call_user_func($this->update_metadata, - $actual_metadata, + $metadata = call_user_func($this->update_metadata, + $metadata, $jwt_aud_uri); } - $actual_metadata = $this->_validate_and_normalize_metadata( - $actual_metadata); - $call->start($actual_metadata); + $metadata = $this->_validate_and_normalize_metadata( + $metadata); + $call->start($metadata); return $call; } @@ -291,21 +269,19 @@ class BaseStub $metadata = [], $options = []) { - list($actual_metadata, $timeout) = - $this->_extract_timeout_from_metadata($metadata); $call = new ServerStreamingCall($this->channel, $method, $deserialize, - $timeout); + $options); $jwt_aud_uri = $this->_get_jwt_aud_uri($method); if (is_callable($this->update_metadata)) { - $actual_metadata = call_user_func($this->update_metadata, - $actual_metadata, + $metadata = call_user_func($this->update_metadata, + $metadata, $jwt_aud_uri); } - $actual_metadata = $this->_validate_and_normalize_metadata( - $actual_metadata); - $call->start($argument, $actual_metadata, $options); + $metadata = $this->_validate_and_normalize_metadata( + $metadata); + $call->start($argument, $metadata, $options); return $call; } @@ -321,23 +297,22 @@ class BaseStub */ public function _bidiRequest($method, callable $deserialize, - $metadata = []) + $metadata = [], + $options = []) { - list($actual_metadata, $timeout) = - $this->_extract_timeout_from_metadata($metadata); $call = new BidiStreamingCall($this->channel, $method, $deserialize, - $timeout); + $options); $jwt_aud_uri = $this->_get_jwt_aud_uri($method); if (is_callable($this->update_metadata)) { - $actual_metadata = call_user_func($this->update_metadata, - $actual_metadata, + $metadata = call_user_func($this->update_metadata, + $metadata, $jwt_aud_uri); } - $actual_metadata = $this->_validate_and_normalize_metadata( - $actual_metadata); - $call->start($actual_metadata); + $metadata = $this->_validate_and_normalize_metadata( + $metadata); + $call->start($metadata); return $call; } diff --git a/src/php/tests/generated_code/AbstractGeneratedCodeTest.php b/src/php/tests/generated_code/AbstractGeneratedCodeTest.php index 4a0bd6a1f1d..aa6906192ff 100644 --- a/src/php/tests/generated_code/AbstractGeneratedCodeTest.php +++ b/src/php/tests/generated_code/AbstractGeneratedCodeTest.php @@ -41,7 +41,6 @@ abstract class AbstractGeneratedCodeTest extends PHPUnit_Framework_TestCase * running on $GRPC_TEST_HOST. */ protected static $client; - protected static $timeout; public function testWaitForNotReady() { @@ -93,7 +92,7 @@ abstract class AbstractGeneratedCodeTest extends PHPUnit_Framework_TestCase public function testTimeout() { $div_arg = new math\DivArgs(); - $call = self::$client->Div($div_arg, ['timeout' => 100]); + $call = self::$client->Div($div_arg, [], ['timeout' => 100]); list($response, $status) = $call->wait(); $this->assertSame(\Grpc\STATUS_DEADLINE_EXCEEDED, $status->code); } diff --git a/src/php/tests/interop/interop_client.php b/src/php/tests/interop/interop_client.php index 9aab5c966c7..45aa8bfc6bf 100755 --- a/src/php/tests/interop/interop_client.php +++ b/src/php/tests/interop/interop_client.php @@ -84,7 +84,7 @@ function largeUnary($stub) * @param $fillOauthScope boolean whether to fill result with oauth scope */ function performLargeUnary($stub, $fillUsername = false, $fillOauthScope = false, - $metadata = []) + $callback = false) { $request_len = 271828; $response_len = 314159; @@ -99,7 +99,12 @@ function performLargeUnary($stub, $fillUsername = false, $fillOauthScope = false $request->setFillUsername($fillUsername); $request->setFillOauthScope($fillOauthScope); - list($result, $status) = $stub->UnaryCall($request, $metadata)->wait(); + $options = false; + if ($callback) { + $options['call_credentials_callback'] = $callback; + } + + list($result, $status) = $stub->UnaryCall($request, [], $options)->wait(); hardAssert($status->code === Grpc\STATUS_OK, 'Call did not complete successfully'); hardAssert($result !== null, 'Call returned a null response'); $payload = $result->getPayload(); @@ -186,6 +191,13 @@ function oauth2AuthToken($stub, $args) 'invalid email returned'); } +function updateAuthMetadataCallback($authUri) +{ + $auth_credentials = ApplicationDefaultCredentials::getCredentials(); + + return $auth_credentials->updateMetadata($metadata = [], $authUri); +} + /** * Run the per_rpc_creds auth test. * @@ -197,15 +209,9 @@ function perRpcCreds($stub, $args) $jsonKey = json_decode( file_get_contents(getenv(CredentialsLoader::ENV_VAR)), true); - $auth_credentials = ApplicationDefaultCredentials::getCredentials( - $args['oauth_scope'] - ); - $token = $auth_credentials->fetchAuthToken(); - $metadata = [CredentialsLoader::AUTH_METADATA_KEY => [sprintf('%s %s', - $token['token_type'], - $token['access_token'])]]; + $result = performLargeUnary($stub, $fillUsername = true, $fillOauthScope = true, - $metadata); + 'updateAuthMetadataCallback'); hardAssert($result->getUsername() == $jsonKey['client_email'], 'invalid email returned'); } @@ -363,7 +369,7 @@ function cancelAfterFirstResponse($stub) function timeoutOnSleepingServer($stub) { - $call = $stub->FullDuplexCall(['timeout' => 1000]); + $call = $stub->FullDuplexCall([], ['timeout' => 1000]); $request = new grpc\testing\StreamingOutputCallRequest(); $request->setResponseType(grpc\testing\PayloadType::COMPRESSABLE); $response_parameters = new grpc\testing\ResponseParameters(); diff --git a/src/php/tests/unit_tests/CallCredentialsTest.php b/src/php/tests/unit_tests/CallCredentialsTest.php new file mode 100644 index 00000000000..6f91b5ed964 --- /dev/null +++ b/src/php/tests/unit_tests/CallCredentialsTest.php @@ -0,0 +1,96 @@ +server = new Grpc\Server(); + $this->port = $this->server->addSecureHttp2Port('0.0.0.0:0', + $server_credentials); + $this->server->start(); + $this->host_override = 'foo.test.google.fr'; + $this->channel = new Grpc\Channel( + 'localhost:'.$this->port, + [ + 'grpc.ssl_target_name_override' => $this->host_override, + 'grpc.default_authority' => $this->host_override, + 'credentials' => $credentials, + ] + ); + } + + public function tearDown() + { + unset($this->channel); + unset($this->server); + } + + public function callbackFunc($service_url) + { + $this->assertTrue(is_string($service_url)); + + return ['k1' => ['v1'], 'k2' => ['v2']]; + } + + public function testCreateFromPlugin() + { + $deadline = Grpc\Timeval::infFuture(); + $status_text = 'xyz'; + $call = new Grpc\Call($this->channel, + '/abc/dummy_method/', + $deadline, + $this->host_override); + + $event = $call->startBatch([ + Grpc\OP_SEND_INITIAL_METADATA => [], + Grpc\OP_SEND_CLOSE_FROM_CLIENT => true, + ]); + + $this->assertTrue($event->send_metadata); + $this->assertTrue($event->send_close); + } +}