node.js で reverse proxy を書く

ちょっと書いてみたくなったので書いた。実用性とか気にしちゃいけない。

$ node reverse_proxy.js d.hatena.ne.jp
reverse proxy to d.hatena.ne.jp http://localhost:8000

これで localhost:8000 → d.hatena.ne.jp の reverse proxy になります。

やってることは単純で、

  • http server を起動
  • client から来た request の Host を差し替えて upstream に投げる
  • upstream から受け取った response を client に返す

だけです。例によってイベント駆動なので、http body を扱うには addListener する形ですね。

本来は body の中身 (HTML内の URL とか) を書き換えるようなものが欲しかったのですが、utf8 以外の文字コードを扱うのが面倒なのでここまで。
# utf8 以外は chunk が文字列じゃなくて、byte の入った Object として扱う必要があります

var sys     = require('sys'),
    http    = require('http'),
    port    = 8000,
    upstreamHost = process.argv[2];

var main = function() {
    http.createServer(handle_request).listen(port);
    sys.puts("reverse proxy to " + upstreamHost + " http://localhost:" + port );
};

var handle_request = function (client_request, client_response) {
    var upstream  = http.createClient(80, upstreamHost);
    var localhost = client_request.headers.host;
    client_request.headers.host = upstreamHost;

    var upstream_request = upstream.request(
        client_request.method,
        client_request.url,
        client_request.headers
    );
    client_request.addListener("data", function(chunk) {
        upstream_request.write(chunk)
    });
    client_request.addListener("end", function() {
        upstream_request.end();
    });

    upstream_request.addListener("response", function (upstream_response) {
        proxy_pass_reverse(upstream_response, localhost);
        client_response.writeHead(
            upstream_response.statusCode,
            upstream_response.headers
        );
        var size = 0;
        upstream_response.addListener("data", function(chunk) {
            client_response.write(chunk);
            size += chunk.length;
        });
        upstream_response.addListener("end", function() {
            sys.puts(client_request.method + " " + client_request.url + " " + upstream_response.statusCode + " " + size);
            client_response.end();
        });
    });
};

// Location ヘッダの書き換え
var proxy_pass_reverse = function(upstream_response, localhost) {
    var location = upstream_response.headers["location"];
    if (location) {
        upstream_response.headers.location
            = location.replace( upstreamHost, localhost );
        sys.puts("new location: " + upstream_response.headers.location);
    }
};

main();