対象読者
この記事は、php -Sを利用しているPHP開発者や、php-src を読んでビルトインサーバーの仕組みと改善方法を学びたい人を対象としています。
C 言語や Zend Engine の詳しい知識は前提にせず、TLS、PHP Stream API、embed SAPI を題材に、既存機能の調査、PoC による実現可能性の確認、責務分離の考え方を扱います。
PoC (Proof of Concept) のコード
src/http_server_tls.c が今回の目的のために作成したコードです。
embed SAPI と Stream API の PoC から学ぶ php -S の実装と改善
PHPで小さなアプリケーションを確認するとき、ビルトインサーバーは便利です。
ターミナルからphp -Sを起動すれば、ApacheやNginxを用意しなくても、PHPスクリプトや静的ファイルをブラウザーから確認できます。
PHP公式マニュアルでも、ビルトインサーバーはアプリケーション開発、テスト、管理された環境でのデモを支援する機能とされています。一方で、本格的なWebサーバーではなく、公開ネットワークで使うものではないとも明記されています。
それでも、開発用サーバーだからこそ困ることがあります。
php -SはHTTPSで起動できません。
今回は、PHPビルトインサーバーを本番サーバーに変えるのではなく、PHPがすでに持っている機能を使って、TLS対応の基礎部分を作れるのかを調べました。
その過程で、PHP Stream API、embed SAPI、Debianのlibphp-embed、そしてFrankenPHPへつながる学習材料が見えてきました。
localhost では動くのに、スマートフォンでは動かない
HTTPSが必要になる理由として分かりやすいのが、ブラウザーのSecure Contextです。
現代のブラウザーでは、端末や個人情報へ強くアクセスするWeb APIの多くが、安全なコンテキストでしか利用できません。
代表例には、次のようなものがあります。
- Service WorkerとPWA
- カメラやマイク
- WebAuthnとパスキー
- Clipboard API
- 位置情報
ただし、ローカル開発には例外があります。
http://localhost、http://127.0.0.1、http://*.localhostは、HTTPSでなくても「潜在的に信頼できるオリジン」として扱われます。そのため、同じPCのブラウザーで確認している間は、HTTPでもService Workerなどを利用できることがあります。
問題になるのは、スマートフォンや別のPCから確認するときです。
開発用PCでビルトインサーバーをLANへ公開し、スマートフォンからhttp://192.168.x.x:8000のようなアドレスへアクセスしても、そのアドレスはスマートフォン自身のlocalhostではありません。
PHPビルトインサーバーは外部インターフェースで待ち受けることもできますが、HTTP接続のままです。
カメラを使ったQRコード読み取りや、PWAの実機確認をしたい場合、ここでlocalhostの例外が使えなくなります。
PHP には TLS 機能がすでにある
PHPビルトインサーバーがHTTPSに対応していないと聞くと、PHPにサーバー側のTLS機能がないように思えます。
しかし、PHP Stream APIはファイルだけを扱う仕組みではありません。
TCPやTLSなどの通信も、同じStream APIの枠組みから扱えます。
TLS用のStream Contextには、ローカル証明書を指定するlocal_certや、秘密鍵を別ファイルとして指定するlocal_pkなどの設定があります。ssl://やtls://トランスポートに対して、証明書や検証方法を設定できる仕組みが、PHPにはすでに用意されています。
つまり、今回の問いは次のように変わります。
「PHPにTLS機能を追加できるか」ではありません。
「PHPがすでに持っているTLS機能を、ビルトインサーバーの実装から利用できるか」です。
この言い換えは重要です。
OpenSSLを直接操作する新しい暗号化コードを作る必要はありません。すでに存在するPHP Stream APIを使える構造を考えればよいからです。
PHP はコマンドだけではなく、ライブラリでもある
PHPは通常、コマンドやWebサーバーから起動します。
CLIならphpコマンド、WebアプリケーションならPHP-FPMを思い浮かべる人が多いでしょう。
これらの実行環境とPHPの実行エンジンをつなぐ入口がSAPIです。
SAPIはServer Application Programming Interfaceの略です。
代表的なSAPIには、次のようなものがあります。
| SAPI | PHPを起動する環境 |
|---|---|
| CLI |
phpコマンド |
| FPM | FastCGIサーバー |
| CLI Server | php -S |
| embed | Cなどのホストプログラム |
embed SAPIでは、PHPが単独でプロセスの中心になるのではありません。
Cプログラムなどがホストになり、その中へPHPランタイムをライブラリとして組み込みます。
PHP拡張と比較すると、呼び出す方向が逆です。
PHP拡張は、PHPからCを呼びます。
embed SAPIは、CからPHPを呼びます。
php-srcには、embed SAPIの初期化、PHP内部関数の呼び出し、PHPスクリプトの実行方法を説明するREADMEがあります。
ただし、通常のPHPマニュアルを読んでいるだけでは、なかなか見つけにくい資料です。
READMEはphp-srcのソースツリー内にあります。
PHPをライブラリとして使えることを知らなければ、そもそも「embed SAPI」という言葉で検索することもありません。
Debian では libphp-embed が必要
Debianでembed SAPIを利用するには、通常のphp-cliに加えてlibphp-embedが必要です。Cプログラムをビルドする場合は、PHPのヘッダーやphp-configを含むphp-devも導入します。
php-srcにはsapi/embed/README.mdとして使い方が用意されています。ただし、PHPマニュアル本体ではなくソースツリー内にあるため、embed SAPIという名称を知らないと見つけにくい資料です。
embed SAPI と Stream API で TLS 通信を試す
今回作成したPoC (Proof of Concept)では、embed SAPI を使って PHP ランタイムを初期化し、その中から PHP Stream API を呼び出しました。
処理の流れは次のようになります。
- ホストとなるCプログラムを起動する
- embed SAPIでPHPランタイムを初期化する
- TLS用のStream Contextを作る
- 証明書と秘密鍵を設定する
- TLS接続を待ち受ける
- 接続を受け付ける
- 復号されたHTTPリクエストを読む
- HTTPレスポンスをTLS経由で返す
PoCでは、Stream Contextに証明書と秘密鍵を設定し、tls://127.0.0.1:8443で接続を待ち受けます。受信したデータをPHP Stream APIで読み、固定のHTTP/1.1レスポンスを返します。
TLSハンドシェイクや暗号化処理はOpenSSL APIを直接操作せず、PHP Stream APIへ任せています。
PoCの目的は完成品ではなく、TLS通信がPHPの既存機能で成立することの確認です。
FrankenPHP も PHP をプロセスへ組み込んでいる
embed SAPI は、昔の実験的な仕組みとして残っているだけではありません。
現在の実用例として挙げられるのが FrankenPHP です。
FrankenPHP のビルド手順では、PHP を --enable-embed と ZTS を有効にして構築し、共有ライブラリとしてGo側のプログラムへリンクします。
ただし、「FrankenPHP は embed SAPI をそのまま使っている」と説明するのは正確ではありません。
FrankenPHP は PHP インタープリターを CGO 経由で Go へ直接組み込み、C 側に独自の SAPI 実装を持っています。
Go側はリクエストの振り分けやスレッドプールを管理し、C 側は PHP SAPI のライフサイクル、PHP スクリプトの実行、スーパーグローバルの構築などを担当します。
つまり、FrankenPHP は次の二段階を実装しています。
-
libphpを使って PHP ランタイムをプロセスへ組み込む - Web サーバーと PHP を接続する独自SAPIを実装する
この事例から分かるのは、PHP を別プロセスの PHP-FPM として動かすだけでなく、Web サーバーと同じプロセスへ組み込む設計も現役で使われていることです。
embed SAPI は完成したWebサーバーではありません。
PHP を別の実行環境へ組み込むための入口です。
参考
まとめ
今回の PoC では、embed SAPI から PHP Stream API を利用し、TLS 接続を受け付けて HTTP レスポンスを返せることを確認しました。
この結果から分かるのは、php -S が HTTPS に対応していない理由は、PHP に TLS 機能がないからではないということです。課題は、既存のビルトインサーバーが、その機能を利用しやすい構造になっていない点にあります。
TLS 対応を進めるには、通信、HTTP 解析、I/O 待機、PHP 実行の責務を分け、それぞれを独立して変更できる境界が必要です。
実現可能性は確認できました。次に必要なのは、維持可能な変更単位へ分解することです。