PHPでSSLソケット通信とApacheのSNI


PHPを使って、プロキシ経由でSSL通信するお話。プロキシ経由にすると段階を踏んでソケット通信をしなければいけないようで、ものすごく面倒。でも会社の環境でプロキシを使っていたりして、そこから外部サーバにアクセスしたいなぁ~・・・なんて結構あるんじゃないでしょうか。

やっと出来上がったので、忘れないようにメモメモ。

参考にしたのはこちらのサイト。— PROXY経由でHTTPSにアクセスする方法 — http://pe5974.sakura.ne.jp/contents/proxy-https.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#!/usr/bin/php -q
<?php
// プロキシサーバに接続する
$sock = stream_socket_client(
  "tcp://your.proxy.com:8080"    // プロキシホスト・ポート番号
, $errno
, $errstr
, 10
, STREAM_CLIENT_CONNECT
);
if ($sock !== false) {
    // WebサイトへのSSL接続のみを作成する
    $header = "CONNECT your.website.com:443 HTTP/1.0\r\n"; // 接続したいホスト+443ポート(SSL)
    $header.= "Host: your.website.com\r\n"; // ここにもホスト名を入れる
    $header.= "User-Agent: TestClient\r\n";
    $header.= "Proxy-Connection: keep-alive\r\n"; // 不要かも...
    $header.= "\r\n";
    $res = fwrite($sock, $header, strlen($header));
    while (!feof($sock)) {
        $line .= fgets($sock);
        if (preg_match('/^http/i', $line)) break; // これがないと無限ループ
    }
    print($line);
    print("---------------------------");
    $res = stream_socket_enable_crypto($sock, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
    // プロキシ経由で、取得したいSSLページを入力する
    $header2 = "GET /index.html HTTP/1.0\r\n";
    $header2.= "Host: proxy.dummy.server\r\n";
    $header2.= "Connection: keep-alive\r\n";
    $header2.= "\r\n";
    $res = fwrite($sock, $header2, strlen($header2));
    $line2 = "";
    if ($res !== false) {
        while (!feof($sock)) {
            $line2.= fgets($sock);
        }
    }
    echo $line2;
    fclose($sock);
}
?>

実行結果は以下の通り。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[root@hogehoge ~]# ./php_socket.php
HTTP/1.0 200 Connection Established    <-- プロキシ経由のSSL接続結果(200=OK)
-----------------------------
Proxy-agent: Apache/2.2.21 (Unix) mod_ssl/2.2.21 OpenSSL/0.9.8r DAV/2

HTTP/1.1 200 OK    <-- SSLページ取得結果
Date: Tue, 10 Apr 2012 08:16:07 GMT
Server: Apache
Last-Modified: Thu, 28 Apr 2011 12:41:15 GMT
Content-Length: 231
Connection: close
Content-Type: text/html

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=EUC-JP">
<title>てすと</title>
</head>
<body>
<h1>これはてすとです</h1>
</body>
</html>

接続するためのステップとして、

  1. stream_socket_clientでプロキシへの接続を確立
  2. 1. の接続オブジェクトを使って、対象WEBサーバへのSSL接続を行う。ただし接続だけのため、メソッドは「CONNECT」
  3. 1. 2. の結果で通信を行う。すでにプロキシへの接続・SSLの接続を作ってあるのでURI部はホスト名は不要。ただしHostフィールドは必要。このときHostフィールドはプロキシサーバを差すこと。でないとApache SNIのエラーが起きる!

3. の原因がぜんぜんわからなかったんですよね・・・。

Hostname your.proxy.com provided via SNI, but your.website.com pr
ovided in HTTP request

なんていうエラーが出てしまって、「なんだこれ?」と散々悩みました。原因はリクエストヘッダに埋め込んだURIとホスト名が違うこと。単純にHostフィールドにプロキシサーバ名だのWebサイト名だのを埋め込んではいけないようで、きちんとアクセスにあったホスト名を指定しないとダメのようです。

  • 1段階目・・・特になし。
  • 2段階目・・・WEBサイトへアクセスするので、URI部・HOSTフィールドは両方ともWEBサーバを指定。
  • 3段階目・・・すでにプロキシ・WEBサイトともに接続が確立されているので、サーバは指定してはいけない

ちなみにSNIはApache2.2以降から実装されたようで、1つのIPで複数のSSL環境が作れる、という機能のようですが、詳しいことはよくわかりません(笑)。

ということでこれでSSLソケット通信ができました!