昨日書いたPHPでプロキシサーバーを作るについて、ちょっとだけ説明します。
PHPタグとnamespace
<?php
namespace zonuexe\ZoProxy;
みなさまご存じ、PHPファイルでは <?php … ?>
タグの中にスクリプトを書くことができます。
名前空間のnamespace
キーワードはファイルの冒頭に書く必要がある(declare
を除く)ので、ここまでは基本的にワンセットです。名前空間の名前の付けかたは… PSR-4を参考にしてください。
use
(クラスのインポート/エイリアス)
use GuzzleHttp\Client as HttpClient;
use GuzzleHttp\Psr7;
use
についてはPHP: 名前空間の使用法: エイリアス/インポート - Manualを読んでください。
この宣言により、以後このクラス内では\GuzzleHttp\Client
をHttpClient
として、\GuzzleHttp\Psr7
をPsr7
として使用することができます。このuse
キーワードはエイリアスまたはインポートと呼ばれますが、その用語の印象に反してこの時点ではまだクラスはロードされません。飽くまで、名前をつけるだけです。
Guzzle (HTTPクライアント)
今回のコードではクライアントとしてGuzzleを利用しました。PHPのHTTPクライアント実装としてはGuzzle以外にもいろいろありますが、PSR-7: HTTP message interfaces準拠のものを選びました。あとは私がGuzzle6が割と好みだからですね。
PSR-7準拠のHTTPクラスとHTTPクライアントライブラリを利用すれば、今回のコードはほとんど変更なしで動く気がします。HTTPクラスとしてはguzzle/psr7: PSR-7 HTTP message libraryを利用します。インターフェイスとしてPSR-7ならば別のライブラリを使っても全然大丈夫ですが、Guzzleの依存関係として勝手にインストールされるので別のライブラリを入れる積極的理由もなさげ。
シェルで以下のようなコマンドを叩くとインストールできます。Composerを使ったことがないひとはComposerでPHPの依存関係を管理するとかを読んでください。
composer require guzzlehttp/guzzle:~6.3
Composerのクラスローダーをrequire
require __DIR__ . '/../vendor/autoload.php';
Composerの初期化ファイルを読み込みます。Composerでインストールしたディレクトリを指定してください。include
でもrequire_once
でもinclude_once
でも、この場合は好みでいいです。
Composerでインストールさたれファイルはこれだけでロードする準備が整った状態になりますので、個別のファイルを読み込む必要はありません。include_path
を明示的に通す必要もありません。むしろ、絶対読み込まないでください。
対応表を作る
// ここはいい感じにやってね
$host_table = [
'hoge.example.com' => [
'host' => 'localhost',
'port' => 3939,
'scheme' => 'http',
],
'foo.example.com' => [
'host' => 'foo.example.jp',
'scheme' => 'https',
],
];
「この名前でリクエストされたら、この名前に変換するよ」の対応表です。前回を記事を書いたときに気付いたのですが、これではあるユースケースに対応できないので一考の余地がありますね。ただ、私の要件としてはこれで十分機能するので、このままでいきます。 (拡張性への課題については読者への宿題とします)
Request
オブジェクトを作る
$request = Psr7\ServerRequest::fromGlobals();
$_GET
, $_POST
, $_COOKIE
, $_FILES
などのスーパーグローバル変数をもとにPSR-7のRequest
オブジェクトを組み立てます。つまり、このサーバーへのリクエストからPSR-7に変換することができます。
\Guzzle\Psr7\ServerRequest::fromGlobals()
はPSR-7の仕様ではなくguzzle/psr7の機能です。(ただし、ほかの多くのHTTPクラス実装も同様の機能を持ってます)
URIオブジェクトを作る
$new_uri = $request->getUri()->withPort(80);
URIオブジェクトはRequest::getUri()
で取り出すことができます。URIではhttp://localhost:3939/
のように、:3939
の書式でポート番号を指定できます。一般にHTTPのデフォルトポートは80
なので、guzzle/psr7はデフォルトポートをURLから取り除きます。
ここまでこの記事を書いていまさら気付いたのですが、これは良くない前提を置いてますね…? 気になるひとはpsr7/Uri.phpを読んでみてください。 (誰も気付いてくれないと悲しい)
対応表から取り出すキーを作る
$key = $new_uri->getHost();
$port = $new_uri->getPort();
if ($port !== null && !Psr7\Uri::isDefaultPort($port)) {
$key .= ":{$port}";
}
さきほど作った対応表から取り出すためのキーです。つまり、"hoge.example.com"
とか"localhost:3939"
のような文字列に変換されるのです。
対応表に沿って、くいくいっとURIを改竄する
if (isset($host_table[$key])) {
if (isset($host_table[$key]['host'])) {
$new_uri = $new_uri->withHost($host_table[$key]['host']);
}
if (isset($host_table[$key]['port'])) {
$new_uri = $new_uri->withPort($host_table[$key]['port']);
}
if (isset($host_table[$key]['scheme'])) {
$new_uri = $new_uri->withScheme($host_table[$key]['scheme']);
}
}
http
をhttps
に変換したり、:3939
ってくっつけたり、foo.example.com
をfoo.example.jp
に変換したりできます。
HTTPリクエストする
$client = new HttpClient;
$response = $client->send($request->withUri($new_uri), [
'http_errors' => false,
]);
さて、Quickstart — Guzzle Documentationにある通りGuzzleのおてがるな利用方法としては$client->get('http://httpbin.org/get')
のようなリクエストができるのですが、$client->send($request)
のようにリクエストすることもできます。
自分がクライアントから受け取ったのもリクエストなら、こちらがほかのHTTPサーバーに送りつけるのもリクエストです。いいですね。これがPSR-7のようなHTTPオブジェクトを利用する醍醐味です。
さて、Guzzleはべんりなので、HTTPのステータスコード404
や500
などの(20x
や30x
以外の)ステータスコードが返ってきたときに例外を投げてくれます。しかし今回は完全に同じレスポンスをクライアントに渡したいので、PHPレベルでの例外にする必要はありませんね。なので'http_errors' => false
にします。そのほかのオプションはRequest Options — Guzzle Documentationを見てくださいね。
レスポンスを返す
foreach ($response->getHeaders() as $key => $values) {
foreach ($values as $value) {
header("{$key}:{$value}");
}
}
echo $response->getBody();
CGIとかなら、普通はヘッダインジェクションを警戒したいところですが… header()
はそのあたりのチェックはちゃんとしてくれるはずなので。たぶん。
まとめ
いかがでしたか? 今回はPHPでPSR-7を使ってproxy鯖を実装する方法を調べてみました。みなさまのお役に立てば幸いです。 PSR-7をほとんど定義通りそのまま使っただけなので、今回新たに調べたことは特にない。うさんくさいblogとかキュレーションメディアの常套句を一回使ってみるのが夢だった。