non vorrei lavorare

ブログ名の通りです。javascript three.js mruby rust OCaml golang julialang blender

node.jsでHTTPプロキシ経由でhttpsアクセスするには

次男が生まれました!

こんにちはkjunichiです。先月末に次男が生まれました。長男と同様に立ち会い出産で臨んだのですが、緊急で帝王切開となりバタバタしてました。自分が仮死状態で生まれたこともあり、手術室へ移動して閉めだされてから次男に会うまでは心配でした。今は母子ともに元気に退院して来てます。

 

プロキシを通す必要がある環境下でnode.jsでhttpsアクセスするには

普通にhttpsモジュールは使えないようなので、調べた結果のメモ。

そもそも、HTTPプロキシ経由でhttpsアクセスするには

CONNECT ssl.example.com:443 HTTP/1.1
Host: 127.0.0.1

とHTTPプロキシにこれからhttpsで通信する旨を通知する必要がある。

その後、HTTPプロキシから200の応答をもらうことで、 「以降の通信を暗号化」して行うことになる。

以降の通信を暗号化が問題だった。

PHPでは、ソケット(ストリーム)を引数に

stream_socket_enable_crypto($sock, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);

なる便利な関数が用意されているのだった。

じゃぁ、node.jsでは?

という事で、初めは、githubのnode.jsのhttpsモジュールのソースから読み出し、 接続済みのソケットをゴニョゴニョすれば力技で行けそうな気配だったが。。。

starttlsなるモジュールがあった!

npm install starttls

最初にGoogleで発見したのはgistのコードの断片だったが、その関数が starttlsなる名前であり、node.js + starttls で再度検索したらnpmに登録されていた次第。

サンプルコード

HTTP1.1でKeep Aliveな通信にしてしまっているので、 HTTPのパース処理等やっつけで、サンプルとして最低限成り立つように 試みてはいます。。

// Https access with Http Proxy Server
// プロキシサーバ経由でhttps通信する

var net = require('net');
var url = require('url');
var startTls = require('starttls').startTls;


var HTTP_PROXY_HOST = "localhost";
var HTTP_PROXY_PORT = "8080";

var targetUrl = "https://www.google.co.jp/";

var parsedUrl = url.parse(targetUrl);
var targetHost = parsedUrl.host;
var port = 443;

var conn = net.createConnection(HTTP_PROXY_PORT,HTTP_PROXY_HOST);


conn.on('error',function(error) {
    console.log("error! : "+error);
});

conn.on('connect',function() {
    console.log("connected.");
    // CRLFCRLFが必要
    conn.write("CONNECT "+targetHost+":"+port+" HTTP/1.0\r\nHost:"+targetHost+"\r\n\r\n",function(){
        var isUpgrade = false;
        conn.on('data',function(data) {
            //
            console.log("recieve data.");
            console.log(data.toString());

            if(!isUpgrade) {
            var securePair = startTls(conn,function(){
                console.log("starttls: done");
                securePair.cleartext.write("GET " + targetUrl + " HTTP/1.1\r\nHost:"+targetHost+"\r\n\r\n",function() {
                    console.log("GET https");
                });

                // バッファを蓄えておく配列
                var bufs = []; 
                // 受け取ったバッファの合計サイズ
                bufs.totalLength = 0; 

                securePair.cleartext.on('data',function(chunk) {
                    console.log("recieve data.");

                    bufs.push(chunk);
                    bufs.totalLength += chunk.length;

                    // 現在のバッファを元にHTTPレスポンスを解析して判定
                    if(parse(bufs)) {       
                        console.log("Parse done: "+ Buffer.concat(bufs, bufs.totalLength).toString());
                        conn.end();
                    }
                });

                //conn.end();
                isUpgrade=true;
            });
            }

        });

        console.log("SSL! CONN: ");

        conn.on('end',function() {
            console.log("end");
        });
    });
});

//
// 以下はhttpsをプロキシ経由でアクセスする事とは本質的でない部分
//

function getHttpResHeaders(data) {
    var pos = data.toString().indexOf("\r\n\r\n");
    if(pos>0) {
        console.log("HTTP Hedars: "+ data.slice(0,pos-1));
        return data.slice(0,pos-1);
    }
    return "";
}

function getLength(headers) {
    var targetKey = "Content-Length:";
    var pos = headers.toString().indexOf(targetKey);
    pos = pos + targetKey.length;
    var tmp = headers.slice(pos).toString();
    //console.log("tmp = "+tmp);
    pos = tmp.indexOf("\r\n");
    if(pos>0) {
        return tmp.slice(0,pos-1);
    }
    return tmp;
}

// HTTPレスポンスの解析
// HTTP1.1でKeep AliveだとonEndで判定できないから頑張る
function parse(bufs) {
    var data = Buffer.concat(bufs, bufs.totalLength);
    var httpHeaders=getHttpResHeaders(data);
    if(httpHeaders.length>0) {
        //console.log("HTTP Response Header!");
        // Content-Lengthから受信予定のサイズを取得する。
        var length = getLength(httpHeaders);
        //console.log("Content-Length : " + length);

        // Content-Length分取得済みか?
        var body=data.toString().slice(data.toString().indexOf("\r\n\r\n"));
        if(body.length >= length) {
            // Body部取得完了
            //console.log("body = "+body);
            return true;
        }
    }

    return false;
}

大変参考になったページ

関連するかもなブログ記事

9年前の記事

7年前の記事