понедельник, 19 сентября 2016 г.

Создание TCP, HTTP и HTTPS прокси на Node.js

HTTP и HTTPS прокси на Node.js

ejz.ru/63/node-js-http-https-proxy

С HTTP все очень просто.

Файл http-proxy.js

var http = require('http')
    , url = require('url');

var server = http.createServer(function(request, response) {

    console.log(request.url);

    var fromURL = url.parse(request.url);

    var options = {
          port: fromURL.port
        , hostname: fromURL.hostname
        , method: request.method
        , path: fromURL.path
        , headers: request.headers
    };

    var proxyRequest = http.request(options);

    proxyRequest.on('response', function (proxyResponse) {
        response.writeHead(proxyResponse.statusCode, proxyResponse.headers); // имеет смысл перенести вниз после всех слушателей событий
        proxyResponse.on('data', function (chunk) {
            response.write(chunk, 'binary');
        });
        proxyResponse.on('end', function() {
            response.end();
        });
        proxyResponse.on('error', function (err) {
            console.log('Error with client ', err);
        });
    });

    request.on('data', function (chunk) {
        proxyRequest.write(chunk, 'binary')
    });
    request.on('end', function () {
        proxyRequest.end()
    });
    request.on('error', function (err) {
        console.log('Problem with request ', err);
    });

});

server.listen(8080);

Тестируем...

node http-proxy.js

http://google.com

http_proxy='127.0.0.1:8080' wget -q -O - 'http://google.com/' | grep -i meta
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<meta name="description" content="Google" />

Отлично! С HTTP запросами разобрались.

С HTTPS протоколом прокси работает через метод CONNECT.
Это означает, что после CONNECT метода прокси соединяется с запрошенным хостом и начинает туннелировать через себя весь трафик между клиентом и хостом (в том числе и обмен сертификатами).
На уровне кода это выглядит как обработка события 'connect' с последующим созданием туннелирующего сокета.

Файл https-proxy.js

var http = require('http')
    , net = require('net')
    , url = require('url');

var server = http.createServer(function (request, response) {

    console.log(request.url);

    var fromURL = url.parse(request.url);

    var options = {
          port: fromURL.port
        , hostname: fromURL.hostname
        , method: request.method
        , path: fromURL.path
        , headers: request.headers
    };

    var proxyRequest = http.request(options);

    proxyRequest.on('response', function (proxyResponse) {
        response.writeHead(proxyResponse.statusCode, proxyResponse.headers); // имеет смысл перенести вниз после всех слушателей событий
        proxyResponse.on('data', function (chunk) {
            response.write(chunk, 'binary');
        });
        proxyResponse.on('end', function () {
            response.end();
        });
    });

    request.on('data', function (chunk) {
        proxyRequest.write(chunk, 'binary');
    });
    request.on('end', function () {
        proxyRequest.end();
    });

});

server.on('connect', function (request, socketRequest, head) {

    console.log(request.url);

    var fromURL = url.parse('http://' + request.url);

    var socket = net.connect(fromURL.port, fromURL.hostname, function() {
        // Сказать клиенту, что соединение установлено
        socket.write(head);
        socketRequest.write("HTTP/" + request.httpVersion + " 200 Connection established\r\n\r\n");
    })

    // Туннелирование к хосту
    socket.on('data', function (chunk) {socketRequest.write(chunk);});
    socket.on('end', function () {socketRequest.end();});
    socket.on('error', function () {
        // Сказать клиенту, что произошла ошибка
        socketRequest.write("HTTP/" + request.httpVersion + " 500 Connection error\r\n\r\n");
        socketRequest.end();
    })

    // Туннелирование к клиенту
    socketRequest.on('data', function (chunk) {socket.write(chunk);});
    socketRequest.on('end', function () {socket.end();});
    socketRequest.on('error', function () {socket.end();});

})

server.listen(8080);

Проверим отработку...

node https-proxy.js

http://ipdb.at/
ipdb.at:443

http_proxy='127.0.0.1:8080' https_proxy='127.0.0.1:8080' wget -q -O - 'http://ipdb.at/' | grep -i 'your ip'
<div id="status-bar">Your IP is <span>10.20.0.1</span></div>

Первым ответом был 302 редирект.
Далее, обратите внимание, при CONNECT методе прокси сервер не знает полностью запрашиваемый урл.
Урл уже передается по шифрованному туннелю.

Давайте усложним задачу.
Представим, что наш прокси-сервис должен перенаправлять запросы на другой прокси (например, для балансировки трафика).
Трафик идет раздельно: HTTP идет по HTTP, HTTPS идет CONNECT'ом.
На пользовательский request вещается 'connect' обработчик.
Таким образом можно при успешном соединении на шлюз можно трафик из сокета шлюза кидать на сокет запроса.

Файл proxy-to-proxy.js

var assert = require('assert')
    , gateway = 'proxy://login:passwd@10.20.30.40:3128/' // Прокси для редиректа
    , http = require('http')
    , net = require('net')
    , url = require('url');

if (process.env.gateway) {gateway = process.env.gateway;}

var server = http.createServer(function (request, response) {

    console.log(request.url);

    var fromGateway = url.parse(gateway);

    var options = {
          port: parseInt(fromGateway.port)
        , hostname: fromGateway.hostname
        , method: request.method
        , path: request.url
        , headers: request.headers || {}
    };

    if (fromGateway.auth) {
        options.headers['Proxy-Authorization'] = 'Basic ' + new Buffer(fromGateway.auth).toString('base64');
    }

    var gatewayRequest = http.request(options);

    gatewayRequest.on('error', function (err) {
        console.log('[error] ' + err);
        response.end();
    });

    gatewayRequest.on('response', function (gatewayResponse) {
        if (gatewayResponse.statusCode === 407) {
            console.log('[error] AUTH REQUIRED');
            process.exit();
        }
        response.writeHead(gatewayResponse.statusCode, gatewayResponse.headers); // имеет смысл перенести вниз после всех слушателей событий
        gatewayResponse.on('data', function (chunk) {
            response.write(chunk, 'binary');
        })
        gatewayResponse.on('end', function () {
            response.end();
        });
    });

    request.on('data', function (chunk) {
        gatewayRequest.write(chunk, 'binary');
    })
    request.on('end', function () {
        gatewayRequest.end();
    });

    gatewayRequest.end();

});

server.on('connect', function (request, socketRequest, head) {

    console.log(request.url);

    var fromURL = url.parse('http://' + request.url)
        , fromGateway = url.parse(gateway);

    var options = {
          port: fromGateway.port
        , hostname: fromGateway.hostname
        , method: 'CONNECT'
        , path: fromURL.hostname + ':' + (fromURL.port || 80)
        , headers: request.headers || {}
    }

    if (fromGateway.auth) {
        options.headers['Proxy-Authorization'] = 'Basic ' + new Buffer(fromGateway.auth).toString('base64');
    }

    var gatewayRequest = http.request(options);

    gatewayRequest.on('error', function (err) {
        console.log('[error] ' + err);
        process.exit();
    });

    gatewayRequest.on('connect', function (res, socket, head) {

        assert.equal(res.statusCode, 200);
        assert.equal(head.length, 0);

        socketRequest.write("HTTP/" + request.httpVersion + " 200 Connection established\r\n\r\n"); // имеет смысл перенести вниз после всех слушателей событий

        // Туннелирование к хосту
        socket.on('data', function (chunk) {socketRequest.write(chunk, 'binary');});
        socket.on('end', function () {socketRequest.end();});
        socket.on('error', function () {
            // Сказать клиенту, что произошла ошибка
            socketRequest.write("HTTP/" + request.httpVersion + " 500 Connection error\r\n\r\n");
            socketRequest.end();
        })

        // Туннелирование к клиенту
        socketRequest.on('data', function (chunk) {socket.write(chunk, 'binary');});
        socketRequest.on('end', function () {socket.end();});
        socketRequest.on('error', function () {socket.end();});

    }).end();

});

server.listen(8080, '127.0.0.1');

Проверка работоспособности...

node proxy-to-proxy.js

http://ipdb.at/
ipdb.at:443

http_proxy='127.0.0.1:8080' https_proxy='127.0.0.1:8080' wget -q -O - 'http://ipdb.at/' | grep -i 'your ip'
<div id="status-bar">Your IP is <span>10.20.30.40</span></div>

Теперь создадим TCP proxy server: client —> proxy -> remote

Файл tcp-proxy.js

var net = require('net');

var REMOTE_ADDR = "192.168.1.25"
    , REMOTE_PORT = 6512;

var server = net.createServer(function (socket) {

    socket.on('data', function (message) {

        console.log('  ** START **');
        console.log('<< From client to proxy ', message.toString());

        var serviceSocket = new net.Socket();

        serviceSocket.connect(REMOTE_PORT, REMOTE_ADDR, function () {
            console.log('>> From proxy to remote', message.toString());
            serviceSocket.write(message);
        });

        serviceSocket.on('data', function (data) {
            console.log('<< From remote to proxy', data.toString());
            socket.write(data);
            console.log('>> From proxy to client', data.toString());
        });

    });

});

server.listen(8080);

console.log('TCP server accepting connection on port: 8080');

Теперь пример прокси-сервера для Express.js, который изменяет ответ перед отправкой в браузер

Файл express-proxy.js

var http = require('http')
    , express = require('express');

var app = express();

app.get('/*', function (clientRequest, clientResponse) {

    var options = {
          hostname: 'google.com'
        , port: 80
        , path: clientRequest.url
        , method: 'GET'
    };

    var googleRequest = http.request(options, function (googleResponse) {

        var body = '';

        if (String(googleResponse.headers['content-type']).indexOf('text/html') !== -1) {
            googleResponse.on('data', function (chunk) {
                body += chunk;
            });
            googleResponse.on('end', function () {
                // Внесение изменений в HTML-код полученного файла перед его отправкой в браузер
                body = body.replace(/google.com/gi, host + ':' + port);
                body = body.replace(/<\/body>/, '<script src="http://localhost:3000/new-script.js" type="text/javascript"></script></body>');
                clientResponse.writeHead(googleResponse.statusCode, googleResponse.headers);
                clientResponse.end(body);
            });
        } else {
            googleResponse.pipe(clientResponse, {end: true});
        }

    });

    googleRequest.end();

});

Комментариев нет:

Отправить комментарий