PHPのプロキシサーバーを説明する

昨日書いた[PHPでプロキシサーバーを作る](http://qiita.com/tadsan/items/f76e3c62af82c98efcfc)について、ちょっとだけ説明します。

## PHPタグと`namespace`

```php
<?php

namespace zonuexe\ZoProxy;
```

みなさまご存じ、PHPファイルでは `<?php … ?>` タグの中にスクリプトを書くことができます。

名前空間の[`namespace`](http://php.net/manual/ja/language.namespaces.definition.php)キーワードはファイルの冒頭に書く必要がある([`declare`](http://php.net/manual/ja/control-structures.declare.php)を除く)ので、ここまでは基本的にワンセットです。名前空間の名前の付けかたは… [PSR-4](http://www.php-fig.org/psr/psr-4/)を参考にしてください。

## `use`(クラスのインポート/エイリアス)

```php
use GuzzleHttp\Client as HttpClient;
use GuzzleHttp\Psr7;
```

`use`については[PHP: 名前空間の使用法: エイリアス/インポート - Manual](http://php.net/manual/ja/language.namespaces.importing.php)を読んでください。

この宣言により、以後このクラス内では`\GuzzleHttp\Client`を`HttpClient`として、`\GuzzleHttp\Psr7`を`Psr7`として使用することができます。この`use`キーワードは**エイリアス**または**インポート**と呼ばれますが、その用語の印象に反してこの時点では**まだクラスはロードされません**。飽くまで、*名前をつけるだけ*です。

## Guzzle (HTTPクライアント)

今回のコードではクライアントとして[Guzzle](https://github.com/guzzle/guzzle)を利用しました。PHPのHTTPクライアント実装としてはGuzzle以外にもいろいろありますが、[PSR-7: HTTP message interfaces](http://www.php-fig.org/psr/psr-7/)準拠のものを選びました。あとは私がGuzzle6が割と好みだからですね。

PSR-7準拠のHTTPクラスとHTTPクライアントライブラリを利用すれば、今回のコードはほとんど変更なしで動く気がします。HTTPクラスとしては[guzzle/psr7: PSR-7 HTTP message library](https://github.com/guzzle/psr7)を利用します。インターフェイスとしてPSR-7ならば別のライブラリを使っても全然大丈夫ですが、Guzzleの依存関係として勝手にインストールされるので別のライブラリを入れる積極的理由もなさげ。

シェルで以下のようなコマンドを叩くとインストールできます。Composerを使ったことがないひとは[ComposerでPHPの依存関係を管理する](http://qiita.com/tadsan/items/86099d44d12f1103b0a0)とかを読んでください。

```sh
composer require guzzlehttp/guzzle:~6.3
```

## Composerのクラスローダーを`require`

```php
require __DIR__ . '/../vendor/autoload.php';
```

Composerの初期化ファイルを読み込みます。Composerでインストールしたディレクトリを指定してください。`include`でも`require_once`でも`include_once`でも、この場合は好みでいいです。

Composerでインストールさたれファイルはこれだけでロードする準備が整った状態になりますので、**個別のファイルを読み込む必要はありません**。`include_path`を明示的に通す必要もありません。むしろ、絶対読み込まないでください。

## 対応表を作る

```php
// ここはいい感じにやってね
$host_table = [
    'hoge.example.com' => [
        'host' => 'localhost',
        'port' => 3939,
        'scheme' => 'http',
    ],
    'foo.example.com' => [
        'host' => 'foo.example.jp',
        'scheme' => 'https',
    ],
];
```

「この名前でリクエストされたら、この名前に変換するよ」の対応表です。前回を記事を書いたときに気付いたのですが、これではあるユースケースに対応できないので一考の余地がありますね。ただ、私の要件としてはこれで十分機能するので、このままでいきます。 (拡張性への課題については読者への宿題とします)

## `Request`オブジェクトを作る

```php
$request = Psr7\ServerRequest::fromGlobals();
```

`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES`などの[スーパーグローバル変数](http://php.net/manual/ja/language.variables.superglobals.php)をもとにPSR-7の`Request`オブジェクトを組み立てます。つまり、このサーバーへのリクエストからPSR-7に変換することができます。

`\Guzzle\Psr7\ServerRequest::fromGlobals()`はPSR-7の仕様ではなくguzzle/psr7の機能です。(ただし、ほかの多くのHTTPクラス実装も同様の機能を持ってます)

## URIオブジェクトを作る

```php
$new_uri = $request->getUri()->withPort(80);
```

URIオブジェクトは`Request::getUri()`で取り出すことができます。URIでは`http://localhost:3939/`のように、`:3939`の書式で[ポート番号](https://ja.wikipedia.org/wiki/%E3%83%9D%E3%83%BC%E3%83%88%E7%95%AA%E5%8F%B7)を指定できます。一般にHTTPのデフォルトポートは`80`なので、guzzle/psr7はデフォルトポートをURLから取り除きます。

ここまでこの記事を書いていまさら気付いたのですが、これは良くない前提を置いてますね…? 気になるひとは[psr7/Uri.php](https://github.com/guzzle/psr7/blob/master/src/Uri.php)を読んでみてください。 *(誰も気付いてくれないと悲しい)*

## 対応表から取り出すキーを作る

```php
$key = $new_uri->getHost();
$port = $new_uri->getPort();
if ($port !== null && !Psr7\Uri::isDefaultPort($port)) {
    $key .= ":{$port}";
}
```

さきほど作った対応表から取り出すためのキーです。つまり、`"hoge.example.com"`とか`"localhost:3939"`のような文字列に変換されるのです。

## 対応表に沿って、くいくいっとURIを改竄する

```php
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リクエストする

```php
$client = new HttpClient;
$response = $client->send($request->withUri($new_uri), [
    'http_errors' => false,
]);
```

さて、[Quickstart — Guzzle Documentation](http://docs.guzzlephp.org/en/stable/quickstart.html)にある通り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](http://docs.guzzlephp.org/en/stable/request-options.html)を見てくださいね。

## レスポンスを返す

```php
foreach ($response->getHeaders() as $key => $values) {
    foreach ($values as $value) {
        header("{$key}:{$value}");
    }
}

echo $response->getBody();
```

CGIとかなら、普通はヘッダインジェクションを警戒したいところですが… [`header()`](http://php.net/manual/ja/function.header.php)はそのあたりのチェックはちゃんとしてくれるはずなので。たぶん。

## まとめ

~~いかがでしたか？ 今回はPHPでPSR-7を使ってproxy鯖を実装する方法を調べてみました。みなさまのお役に立てば幸いです。~~ <sub>PSR-7をほとんど定義通りそのまま使っただけなので、今回新たに調べたことは特にない。うさんくさいblogとかキュレーションメディアの常套句を一回使ってみるのが夢だった。</sub>