prestissimoでハマったcurlの問題メモ

Posted by Hiraku on 2016-04-30

composerを高速化するプラグイン prestissimo をメンテしていく中でハマった問題の中には、curlの挙動によるものがいくつかありました。

細かすぎて伝わらないやつです。もう消えてしまったソースコードもあるけど、なにかの役に立つかもしれないしメモを残しておきます。

PHP5.5以降とそれ以前でCURLOPT_PROGRESSFUNCTIONのプロトタイプが違う

Missing argument 5 for Hirak\Prestissimo\CurlRemoteFilesystem::progress() ・ Issue #18 ・ hirak/prestissimo

ダウンロードの進捗を取ってプログレスバーを表示するのに使う、CURLOPT_PROGRESSFUNCTIONというオプションがあります。ここで設定した関数に渡される引数がPHP5.5で変わっていて、そのままだとPHP5.4以前ではエラーになっていました。

PHP5.4のサポートを捨てるわけにも行かず、仕方ないのでPHP_VERSION_IDでバージョン別の可変長引数を取ることにしました。汚い。

<?php
function callback() {
    if (PHP_VERSION_ID >= 50500) {
        list($ch, $downBytesMax, $downBytes, $upBytesMax, $upBytes) = func_get_args();
    } else {
        list($downBytesMax, $downBytes, $upBytesMax, $upBytes) = func_get_args();
    }
    //...
}

//...
curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, 'callback');

prestissimo-0.2ではComposer\Util\RemoteFilesystemの置き換え自体を諦めることにしたので、このソースコードは消滅しています。

ちなみに、PHP自体でバージョン判定を行うとき、PHP_VERSIONを使うと`version_compare()`を使う必要が出てきますが、PHP_VERSION_IDを使えば関数を使う必要が無いので最速だと思います。

CURLOPT_USERPWDがリセットできない

prestissimoでは接続を再利用するために、極力curl_init()をせず、一度作ったcurlリソースを使い回すようにしています。 しかし使いまわされた2回目の通信は、同じオプションとは限りません。 curl_reset()が導入されるのはPHP5.5からです。composerプラグインではcomposerのターゲットになっているPHP5.3でも動くようにする必要があったため、毎回curl_setoptを全件やり直すことで、curl_reset()相当のことをやろうと思いました。

しかし、一部のオプションはfalseやnullをセットしても無かったことにできませんでした。PHPのバージョンによっては改善されているのですが、、。

この中で問題になったのがCURLOPT_USERPWDです。composerはBASIC認証がかかったリポジトリに対応しているので、場合によってはこれをセットする必要がありました。

curl_setopt($ch, CURLOPT_USERPWD, "$user:$pass");

nullを渡してもリセットできないことがあり、このオプションの利用は諦めることにしました。BASIC認証を表現するときはこのオプションで書くのが一番美しいんですが。。残念。

Authorizationヘッダを手動で作ることで対処しました。

curl_setopt($ch, CURLOPT_HTTPHEADER, array(
    'Authorization: Basic ' . base64_encode("$this->username:$this->password");
));

NSS版のcurlは楕円曲線暗号を使ってくれない

libcurlは暗号化のライブラリを差し替えられるようになっています。何となくopensslと組み合わせるイメージがありましたが、そうとも限らないようです。

例えば手元のMacに入っているcurlはこんな感じでした。opensslではなくSecureTransportが使われています。

$ curl -V
curl 7.43.0 (x86_64-apple-darwin15.0) libcurl/7.43.0 SecureTransport zlib/1.2.5
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtsp smb smbs smtp smtps telnet tftp 
Features: AsynchDNS IPv6 Largefile GSS-API Kerberos SPNEGO NTLM NTLM_WB SSL libz UnixSockets

CentOSでcurlをインストールするとNSSが組み合わさった状態になっていました。

$ curl -V
curl 7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.19.1 Basic ECC zlib/1.2.3 libidn/1.18 libssh2/1.4.2
Protocols: tftp ftp telnet dict ldap ldaps http file https ftps scp sftp 
Features: GSS-Negotiate IDN IPv6 Largefile NTLM SSL libz

このNSSなcurlは、ECC系の暗号化スイートを標準で無効化しているようです。特許問題が気になるのでしょうか?

何が問題になったかというと、これも拙作のpackagist.jpが使っているCloudFlareのhttpsと相性が悪いのです。そのままだと対応暗号スイートがなくて接続できない。

$ curl -v https://packagist.jp/packages.json
* About to connect() to packagist.jp port 443 (#0)
*   Trying 2400:cb00:2048:1::681c:1f64... connected
* Connected to packagist.jp (2400:cb00:2048:1::681c:1f64) port 443 (#0)
* Initializing NSS with certpath: sql:/etc/pki/nssdb
*   CAfile: /etc/pki/tls/certs/ca-bundle.crt
  CApath: none
* NSS error -12286
* Closing connection #0
* SSL connect error
curl: (35) SSL connect error

ただ、明示的に暗号スイートを使わせるようにすれば、接続できるようです。

$ curl -v --ciphers "ecdhe_ecdsa_aes_256_sha,ecdhe_ecdsa_aes_128_sha" https://packagist.jp/packages.json
* About to connect() to packagist.jp port 443 (#0)
*   Trying 2400:cb00:2048:1::681c:1e64... connected
* Connected to packagist.jp (2400:cb00:2048:1::681c:1e64) port 443 (#0)
* Initializing NSS with certpath: sql:/etc/pki/nssdb
*   CAfile: /etc/pki/tls/certs/ca-bundle.crt
  CApath: none
* SSL connection using TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
* Server certificate:
*       subject: CN=sni96584.cloudflaressl.com,OU=PositiveSSL Multi-Domain,OU=Domain Control Validated
*       start date:  3月 06 00:00:00 2016 GMT
*       expire date:  9月 11 23:59:59 2016 GMT
*       common name: sni96584.cloudflaressl.com
*       issuer: CN=COMODO ECC Domain Validation Secure Server CA 2,O=COMODO CA Limited,L=Salford,ST=Greater Manchester,C=GB
...

これも仕方ないので、暗号スイートを全部列挙することで対処しました。対応していないものを列挙すると動かなくなるので、何か非互換の問題が起きないか心配でしたが、今のところ報告は上がってきていません。

enable ECC cipher suites by hirak ・ Pull Request #69 ・ hirak/prestissimo

curl_setopt($ch, CURLOPT_SSL_CIPHER_LIST, implode(',', $ciphers));

マニュアルに書いてないのですが、CURLOPT_SSL_CIPHER_LISTの列挙方式は','区切りの文字列です。":"でも区切れるとか書いてあるマニュアルもありましたが、少なくともNSS版のcurlでは','区切り以外認識しませんでした。

ひとまず、以上です。あとは私がやらかしたポカばっかりですね。

keyword: prestissimo

PHPの最新記事