gRPC for PHPでgRPCサーバへSSL接続する方法と、調べる過程で5つのOSSにコントリビュートした話
こんにちは🐔
Showcase Gigの林 (howyi) です。
社内のプロダクトで、Goで作られたgrpcのサーバへPHPのクライアントで接続する、といった処理がありました。
このPHPのgRPCクライアント部分をSSL対応する方法と、そのときにハマったエラーなどを紹介します。
gRPC for PHPでgRPCサーバへSSL接続する方法
まず結論となる、方法から紹介します。
公式の認証ガイド にある通り、protoから生成したClientにcredentialsとして Grpc\ChannelCredentials::createSsl() を渡すことで対応できます。
// 証明書を指定しない場合
$client = new helloworld\GreeterClient('myservice.example.com', [
'credentials' => Grpc\ChannelCredentials::createSsl(),
]);
// 証明書を指定する場合
$client = new helloworld\GreeterClient('myservice.example.com', [
'credentials' => Grpc\ChannelCredentials::createSsl(file_get_contents('roots.pem')),
]);
証明書を指定しない場合、PhanやPHPStan等の静的解析ツールが引数不足のエラーを出すことがありますが、パッケージのアップデートもしくは除外設定を行うことで回避してください。
そのほかのオプションについては 公式のAPIドキュメント を参照してください。
対応時にハマった所
ドキュメントを探す
まず対応するにあたって 公式の認証ガイド を見たのですが、ここにSSL対応についての説明がPHPのみありませんでした。
※現在はあります
最初のコード
いったんドキュメントは諦め、コード内のメソッド等をみたところ、ほかの言語と同様に createSsl() というメソッドが用意されていました。
おそらくこれだと思い、書いてみて引数などの項目を確認してみます。
どうやら引数にはデフォルト値として空文字列が指定されており、この状態だと証明書の指定なしで動かせるため、こちらで進めてみます。
$client = new helloworld\GreeterClient('myservice.example.com', [
'credentials' => Grpc\ChannelCredentials::createSsl(),
]);
※gRPCはPHPエクステンションとして提供されているため、Composerで入る諸ライブラリと違い定義元のコードにジャンプということができません
PHPStanによる静的解析で弾かれる
実行前に上記コードを PHPStan で静的解析した所、以下のエラーが出てきます。
Static method Grpc\ChannelCredentials::createSsl() invoked with 0 parameters, 1-3 required.
PhpStormではすべての引数はデフォルト値が入るoptionalと記載していますが、PHPStanは1番目の引数はrequireだと言っています。
少し調べてみるとcreateSslの第一引数は途中からOptionalになり、PHPStanがそれに対応できていないようです。
いったんはデフォルト値を入れることで対応してみます。
$client = new helloworld\GreeterClient('myservice.example.com', [
'credentials' => Grpc\ChannelCredentials::createSsl('', '', ''),
]);
証明書が存在しないエラーが出る
静的解析も通ったので、実際に実行してみた所、下記のエラーが発生しました。
Failed to create secure client channel
どうやら空文字列を証明書として解釈してエラーとなっているみたいです。
デフォルト値として設定されているのに入れるとこのエラーが出るのは明らかにおかしいと思ったため、ここでgRPCのソースコードを見てみます。
Grpc\ChannelCredentials::createSsl() のコードを見てみる
gRPCのPHPライブラリはPHPエクステンションとして提供されているので、Cで書かれています。
/**
* Create SSL credentials.
* @param string $pem_root_certs = "" PEM encoding of the server root certificates (optional)
* @param string $private_key = "" PEM encoding of the client's
* private key (optional)
* @param string $cert_chain = "" PEM encoding of the client's
* certificate chain (optional)
* @return ChannelCredentials The new SSL credentials object
*/
PHP_METHOD(ChannelCredentials, createSsl) {
char *pem_root_certs = NULL;
grpc_ssl_pem_key_cert_pair pem_key_cert_pair;
php_grpc_int root_certs_length = 0;
php_grpc_int private_key_length = 0;
php_grpc_int cert_chain_length = 0;
pem_key_cert_pair.private_key = pem_key_cert_pair.cert_chain = NULL;
grpc_set_ssl_roots_override_callback(get_ssl_roots_override);
/* "|s!s!s!" == 3 optional nullable strings */
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "|s!s!s!",
&pem_root_certs, &root_certs_length,
&pem_key_cert_pair.private_key,
&private_key_length,
&pem_key_cert_pair.cert_chain,
&cert_chain_length) == FAILURE) {
zend_throw_exception(spl_ce_InvalidArgumentException,
"createSsl expects 3 optional strings", 1 TSRMLS_CC);
return;
}
~~~~~~~
まず引数の $pem_root_certs, $private_key, $cert_chain に注目してみると、最初に NULLをセットしており、zend_parse_parameters にわたすことでPHPから来た引数をセットしています。
zend_parse_parameters とは、PHPの引数をCの変数とマッピングする関数であり、英数字と記号で各引数の型を指定します。
ここで zend_parse_parameters の記法を見てみます。
|以降の引数は省略可能
s 文字列
! NULL許容
以上のことから、今回指定している |s!s!s! は 「3つの引数は string & nullable & optional(default null)」となっていることがわかります。
よって、 \Grpc\ChannelCredentials::createSsl()は\Grpc\ChannelCredentials::createSsl(null, null, null)と同じ挙動 となっているようです。
つまり、PHPStanだけでなく、PhpStormの型定義とgRPCのPHPDocも間違っていたということになります。
静的解析はいったん除外し、PhpStormの提供する型定義にも従わず、以下のコードで正常に動くことが確認できました。
$client = new helloworld\GreeterClient('myservice.example.com', [
/* @phpstan-ignore-next-line */
'credentials' => Grpc\ChannelCredentials::createSsl(),
]);
修正PRを投げる
自分はこの調査でそこそこの時間を無駄に消耗してしまったのですが、今後同じようなことをしたい人が同じ所で困らないように各リポジトリに修正PRを投げます。
※投稿時点ではすべてマージ済みですが、gRPCとPhpStormについてはまだリリースされていない状態です
gRPCの修正
大元となるコードの修正です。
ドキュメントの修正
ドキュメントがPHPだけ存在しなかったため grpc.io へ向けて追記のPRを出しました。
静的解析の修正
PHPStan, Phanの修正です。
PHPStanはPHPエクステンションの定義をPhanから手動でコピーしているため、どちらのリポジトリにもPRを出します。
PhpStormの修正
PhpStormは、PHPエクステンションの定義をJetBrains/phpstorm-stubsというリポジトリで管理しています。
まとめ
数行直してSSL対応終わりとなるかと思いきや、処理を追っていくにつれgRPC本家や様々なOSSへコミットまですることになりました。
各プロダクトのPHPエクステンションへの対応方法などを知ることができて良い機会になったと思っています。
みなさんもOSS利用で困ったりハマったりしたときは、次の人が同じく困らないようOSSへコントリビュートしてみるのもいかがでしょうか。
宣伝
Showcase Gigでは多種多様な技術に挑戦する仲間を募集しています!
ぜひチェックしてみてください ✨