Node.jsによるコンテンツ変換用リバースプロキシ

2020/02/27更新

目次

基本的なリバースプロキシ

Node.jsでリバースプロキシを作るには、http-proxyモジュールを使うと非常に簡単にできる。

const httpProxy = require('http-proxy');

// 対象のホスト(オリジンサーバー)
const TARGET_HOST = 'http://localhost';
// リクエストを受け付けるポート
const PROXY_PORT = 8080;

const proxy = httpProxy.createProxyServer({target: TARGET_HOST});

proxy.listen(PROXY_PORT);

これで、http://localhost:8080/へアクセスすると、http://localhost/からの応答をプロキシして返すことができる。

リクエストを処理する

HTTPヘッダの操作

オリジンサーバーへリクエストを渡す前にHTTPヘッダを操作したりする必要がある場合は、proxyReqイベントハンドラ内で処理する。

proxy.on('proxyReq', (proxyReq, req, res, options) => {
    // ヘッダの追加・修正
    proxyReq.setHeader('Host', 'localhost');
    // ヘッダの除去
    proxyReq.removeHeader('Accept-Encoding');
});

レスポンスを処理する

HTMLの置換

前述の基本的な例では、http://localhost/からの応答をただ単に右から左へと受け流すだけである。そこで、レスポンスのHTMLに一定の変換を施して返すような処理を追加してみる。

proxy.on('proxyRes', (proxyRes, req, res) => {
    // 変換対象でないファイルは何も手を加えない
    if (!/\.html$/.test(req.url)) {
        return;
    }
    // 本来の出力メソッドを退避しておく
    const _write = res.write;
    const _end = res.end;
    // 対象サーバーからのレスポンスを取得する
    let body = Buffer.from('');
    res.write = (data) => {
        body = Buffer.concat([body, data]);
    };
    // 変換処理を施して返す
    res.end = () => {
        // 全ての<p>タグを赤色にする
        const output = body.toString().replace(/<p>/g, '<p style="color: #ff0000">'); // ◆
        // 変換後のバイト数でContent-Lengthヘッダを更新する
        const contentLength = Buffer.byteLength(output);
        res.setHeader('Content-Length', contentLength);
        // 変換したデータを返す
        _write.call(res, output);
        _end.call(res);
    };
});

これで、例えばhttp://localhost:8080/index.htmlにアクセスすると、http://localhost/index.html<p>タグが全て赤色になって表示される。上記コードの◆の部分を好きな処理に置き換えれば、レスポンスを自由に加工して返すことができる。

圧縮されたレスポンスへの対応

Accept-Encoding: gzipなどのヘッダを付けてオリジンサーバーにリクエストすると、レスポンスが圧縮されている場合がある。その場合は、レスポンスを処理する前にまず解凍する必要がある。Accept-Encodingヘッダを取り除いてリクエストすれば、圧縮されることはなくなる。

const zlib = require('zlib');
…(略)…
proxy.on('proxyRes', (proxyRes, req, res) => {
    …(略)…
    res.end = () => {
        // Content-Encodingヘッダを見て、もしgzip圧縮されていれば解凍する
        if (proxyRes.headers['content-encoding'] == 'gzip' && body.length > 0) {
            body = zlib.gunzipSync(body);
        }
        …(略)…
    };
});