четверг, 29 сентября 2016 г.

Node.js Cluster

But how to increase the number of process with Node so that you can have a good scaling system?
In the .NET world you can find something similar is ASP.NET (hosted on IIS) and it's called "web garden", in Node instead it's called Cluster. Basically there are more than one active process and a "manager".
In that scenario you can use one process for each core of you computer, so your Node application can scale better with you hardware.
Basically it's like running 'node app.js' for each core you have, and another process to manage them all.
First step, install some packages:
npm install cluster --save
The goal of this example is to create one process for each core, so the first thing to do is to read the number of cores installed on your laptop:
var cluster = require('cluster');

if (cluster.isMaster) {
  var numCPUs = require('os').cpus().length;
  for (var i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  Object.keys(cluster.workers).forEach(function(id) {
    console.log(cluster.workers[id].process.pid);
  });
}
cluster.isMaster is necessary to be sure that you are forking it just one time.
Now if you run the app you should have one process for each core, plus the master
In my case I've 8 core, so 9 process because of master.
The next step is to add a webserver, so
npm install http --save
and put your logic for each fork:
var cluster = require('cluster');
var http = require('http');

if (cluster.isMaster) {
  var numCPUs = require('os').cpus().length;
  for (var i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  Object.keys(cluster.workers).forEach(function(id) {
    console.log(cluster.workers[id].process.pid);
  });
} else{

  // Create HTTP server.
  http.Server(function(req, res) {
    res.writeHead(200);
    res.end("This answer comes from the process " + process.pid);

  }).listen(8080);
}
Now calling the webserver you can see which process is answering your request:
image
Because the code is too simple, probably you'll get the same 'pid' for each request from your browser. The easier way to test it is to lock the thread (yes, I said that) so the "balancer" can switch the request to another process demonstrating the cluster.
In Node there isn't something like Thread.Sleep, so the best way to lock a thread is create something that keeps it busy, something like an infinite loop :smirk:
var cluster = require('cluster');
var http = require('http');

if (cluster.isMaster) {
  var numCPUs = require('os').cpus().length;
  for (var i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  Object.keys(cluster.workers).forEach(function(id) {
    console.log(cluster.workers[id].process.pid);
  });
} else{

  // Create HTTP server.
  http.Server(function(req, res) {
    res.writeHead(200);
    res.end("This answer comes from the process " + process.pid);

    //that's just for example
    while(true){

    }

  }).listen(8080);
}
If you want to manage all the processes and to log some events, it could be helpful to track some events for each process and to send a message from the "worker" to the "master" or to check when a process dies.
To do that it's necessary to use message event on the worker, so here's the code:
var cluster = require('cluster');
var http = require('http');

if (cluster.isMaster) {

  console.log("Master pid: " + process.pid);

  var numberOfRequests = 0;

  var numCPUs = require('os').cpus().length;
  for (var i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  Object.keys(cluster.workers).forEach(function(id) {
    console.log('creating process with id = ' + cluster.workers[id].process.pid);

    //getting message
    cluster.workers[id].on('message', function messageHandler(msg) {
      if (msg.cmd && msg.cmd == 'notifyRequest') {
        numberOfRequests += 1;
      }

      console.log("Getting message from process : ", msg.procId);
    });

    //Getting worker online
    cluster.workers[id].on('online', function online()
    {
      console.log("Worker pid: " + cluster.workers[id].process.pid + " is online");
    });

    //printing the listening port
    cluster.workers[id].on('listening', function online(address)
    {
      console.log("Listening on port + " , address.port);
    });

    //Catching errors
    cluster.workers[id].on('exit', function(code, signal) {
      if( signal ) {
        console.log("worker was killed by signal: "+signal);
      } else if( code !== 0 ) {
        console.log("worker exited with error code: "+code);
      } else {
        console.log("worker success!");
      }
    });
  });

  //Printing number of requests
  setInterval(function(){
    console.log("Handled " + numberOfRequests + " requests");
  }, 3000);

} else {

  // Create HTTP server.
  http.Server(function(req, res) {
    res.writeHead(200);
    res.end("This answer comes from the process " + process.pid);

    console.log("Message sent from http server");

    // Notify master about the request
    process.send({ cmd: 'notifyRequest', procId : process.pid });


  }).listen(8080);
}

Пример.

Файл cluster-boot.js

// Данный сценарий выполнит код из файла app.js с помощью нескольких worker.
// Число worker опеределено в константе WORKER_COUNT.

// Мастер-процесс будет отвечать на сигнал SIGHUP,
// который будет перезапускать worker'ы и перезагружать app.

var workerCount = process.env.WORKER_COUNT || 2
    , cluster = require('cluster');

// Определяем что каждый worker должен сделать.
// В данном случае каждый worker должен выполнить код из файла app.js.
// Подразумевается, что код в app.js представляет собой простой HTTP-сервер.
cluster.setupMaster({exec: 'app.js'});

/////////////////////////////////////////////////////////////
// Создание новых worker
/////////////////////////////////////////////////////////////

// Значение, определяющее разрешно ли создавать новые worker.
var stop = false;

// Создать первоначальный набор worker.
forkNewWorkers();

// Функция для создания необходимого числа новых worker в случае,
// если ранее не было решено их все остановить.
function forkNewWorkers () {
    if (!stop) {
        for (var i = numWorkers(); i < workerCount; i++) {
            cluster.fork();
        }
    }
}

// Функция для определения числа активных в данный момент worker.
function numWorkers () {
    return Object.keys(cluster.workers).length;
}

// Каждый worker может отключиться из-за того, что его процесс был убит или
// из-за того, что мы прошлись по массиву workersToStop и рестартовали каждый worker, перечисленный в нем.
// В любом случае при отключении worker мы создаем взамен него новые worker'ы для выполнения работ.
cluster.on('disconnect', forkNewWorkers);

// Теперь каждый worker начинает слушать порт.
// Как только worker будет готов к своей работе мы посылаем сигнал для перезапуска следующего worker.
cluster.on('listening', stopNextWorker);

//////////////////////////////////////////////////////////////
// Уничтожение worker
//////////////////////////////////////////////////////////////

// Список worker находящихся в очереди на перезапуск.
var workersToStop = [];

// Сообщить следующему worker, находящемуся в очереди на перезапуск, отключиться.
// Это позволит процессу завершить свою работу за 60 секунд перед отправкой сигнала SIGTERM.
function stopNextWorker () {
    var i = workersToStop.pop()
        , worker = cluster.workers[i];
    if (worker) {stopWorker(worker);}
}

// Остановить работу всех worker за раз.
function stopAllWorkers () {
    stop = true;
    console.log('stop all workers');
    for (var id in cluster.workers) {
        stopWorker(cluster.workers[id]);
    }
}

// Функция для остановки конкретного wroker.
// Делаем задержку в 60 секунд после отключения worker перед отправкой сигнала SIGTERM.
function stopWorker (worker) {
  console.log('stopping', worker.process.pid);
  worker.disconnect();
  var killTimer = setTimeout(function () {
    worker.kill();
  }, 60000);
  // Убеждаемся, что мы не будем подвешивать мастер-процесс при добавлении этого setTimeout
  killTimer.unref();
}

/////////////////////////////////////////////////////////////
// Прослушивания передачи сигналов в мастер-процесс из сторонних программ
/////////////////////////////////////////////////////////////

// Если сигнал HUP послан в мастер-процесс, то последовательно перезапустить все worker.
process.on('SIGHUP', function () {
    console.log('restarting all workers');
    workersToStop = Object.keys(cluster.workers);
    stopNextWorker();
});

// Если сигнал TERM послан в мастер-процесс, то убить все worker за раз.
process.on('SIGTERM', stopAllWorkers);

/////////////////////////////////////////////////////////////
// Вывод сообщения об успешном запуске программы
/////////////////////////////////////////////////////////////

// Вывод в консоль сообщения о том, что программа запущена успешно
console.log('app master', process.pid, 'booted');

вторник, 27 сентября 2016 г.

Better Node.js Stack Traces

Error: BAD
    ────────────────────────────
    at Object.<anonymous> (D:\Trace\lib.js:215:7)  
    ───────────────────────────
    213 » }
    214 » */
    215 » throw new Error('BAD');
    ------------^
    216 » 
    217 » 

    at Module.Module._compile [as _compile] (module.js:556:32)
    at Object.Module._extensions..js [as .js] (module.js:565:10)
    at Module.Module.load [as load] (module.js:473:32)
    at tryModuleLoad (module.js:432:12)
    at Function.Module._load [as _load] (module.js:424:3)
    at Module.Module.require [as require] (module.js:483:17)
    at require (internal/module.js:20:19)
    ───────────────────────────
    at Object.<anonymous> (D:\Trace\test.js:9:9)
    ───────────────────────────
     7 » 
     8 » 
     9 » var lib = require('./lib.js');
    -------------^
    10 » 
    11 » 

    at Module.Module._compile [as _compile] (module.js:556:32)

Код файла с ошибкой app.js

require('./stacktrace.js')();

require('fs').readFile('./test.js', function () {
    throw new Error('BAD');
});

Код файла stacktrace.js

/*
─────────────────────
/path/to/your/code.js
─────────────────────
189 »
190 » function bar (x) {
191 »   throw new Error('x: ' + x);
---------------^
192 » }
193 »
194 » foo(bar);
*/

var LINES_BEFORE = 2
    , LINES_AFTER = 3
    , MAX_COLUMNS = 80
    , DEFAULT_INDENT = 4
    , GUTTER_CONTENT = ' » '
    , ENDASH_CHAR = '-'
    , UP_ARROW_CHAR = '^'
    , ELLIPSIS_CHAR = '…'
    , EMDASH_CHAR = '─'
    , ERR_FILE_NOT_EXIST = 'ENOENT'
    , DEFAULT_LIBRARY_REGEX = /node_modules/
    , OUTPUT_PREFIX = repeatCharacter(' ', DEFAULT_INDENT); // ==> repeatCharacter()

module.exports = function () {
    Error.prepareStackTrace = function (error, frames) {
        var lines = [];
        try {
            lines.push(error.toString());
        } catch (error) {
            try {
                lines.push('<error: ' + error + '>');
            } catch (e) {
                lines.push('<error>');
            }
        }
        frames.forEach(function (frame) {
            var line;
            try {
                line = formatFrame(frame); // ==> formatFrame()
            } catch (error) {
                try {
                    line = '<error: ' + error + '>';
                } catch (e) {
                    line = '<error>';
                }
            }
            lines.push(line);
        });
        return lines.join('\n');
    };
};

function formatFrame (frame) {
    var line = ''
        , underline = ''
        , betterLine = ''
        , addPrefix = true
        , fileLocationAndContext = getFileLocationAndContextFrom(frame)  // ==> getFileLocationAndContextFrom()
        , fileLocation = fileLocationAndContext.fileLocation
        , context = fileLocationAndContext.context
        , functionName = frame.getFunctionName()
        , methodName = frame.getMethodName()
        , isConstructor = frame.isConstructor()
        , isMethodCall = !(frame.isToplevel() || isConstructor);
    if (isMethodCall) {
        line += frame.getTypeName() + '.';
        if (functionName) {
            line += functionName;
            if (methodName && (methodName != functionName)) {
                line += ' [as ' + methodName + ']';
            }
        } else {
            line += methodName || '<anonymous>';
        }
    } else if (isConstructor) {
        line += 'new ' + (functionName || '<anonymous>');
    } else if (functionName) {
        line += functionName;
    } else {
        line += fileLocation;
        addPrefix = false;
    }
    if (addPrefix) {
        line += ' (' + fileLocation + ')';
    }
    line = 'at ' + line;
    if (context) {
        underline = OUTPUT_PREFIX + line.replace(/./g, EMDASH_CHAR);
        betterLine = line.replace(/./g, EMDASH_CHAR) + '\n' + OUTPUT_PREFIX + [line, underline, context].join('\n');
        return OUTPUT_PREFIX + betterLine + '\n';
    } else {
        return OUTPUT_PREFIX + line;
    }
}

function getFileLocationAndContextFrom (frame) {
    var fileLocation = ''
        , context = null
        , fileName
        , lineNumber
        , columnNumber;
    if (frame.isNative()) {
        fileLocation = 'native';
    } else if (frame.isEval()) {
        fileLocation = 'eval at ' + frame.getEvalOrigin();
    } else {
        fileName = frame.getFileName();
        if (fileName) {
            fileLocation += fileName;
            lineNumber = frame.getLineNumber();
            if (lineNumber != null) {
                fileLocation += ':' + lineNumber;
                columnNumber = frame.getColumnNumber();
                if (columnNumber) {
                    fileLocation += ':' + columnNumber;
                }
                try {
                    if (shouldCollapseDirectory(fileName)) { // ==> shouldCollapseDirectory()
                        context = null;
                    } else {
                        context = formatContext(fileName, lineNumber, columnNumber); // ==> formatContext()
                    }
                } catch(error) {
                    if (error.code === ERR_FILE_NOT_EXIST) {
                        context = null;
                    } else {
                        context = error;
                    }
                }
            }
        }
    }
    if (!fileLocation) {
        fileLocation = 'unknown source';
    }
    return {
          fileLocation: fileLocation
        , context: context
    };
}

function shouldCollapseDirectory (fileName) {
    return DEFAULT_LIBRARY_REGEX.test(fileName);
}

function formatContext (fileName, lineNumber, columnNumber) {
    var code;
    try {
        code = require('fs').readFileSync(fileName).toString();
    } catch(error) {
        if (error.code === ERR_FILE_NOT_EXIST) {
            throw error;
        }
        return OUTPUT_PREFIX + error.toString();
    }

    // Figure out the lines of context before and after
    var lines = code.split('\n')
        , preLines = lines.slice(lineNumber - LINES_BEFORE - 1, lineNumber)
        , postLines = lines.slice(lineNumber, lineNumber + LINES_AFTER);

    // Collect formatted versions of all the lines
    var formattedLines = []
        , maxLineNumber = lineNumber + LINES_AFTER
        , currentLineNumber = lineNumber - LINES_BEFORE;

    function renderLines (lines) {
        while (lines.length) {
            formattedLines.push(
                formatCodeLine(currentLineNumber, lines.shift(), maxLineNumber) // ==> formatCodeLine()
            );
            currentLineNumber++;
        }
    }

    renderLines(preLines);

    formattedLines.push(formatCodeArrow(currentLineNumber - 1, columnNumber, maxLineNumber)); // ==> formatCodeArrow()

    renderLines(postLines);

    return OUTPUT_PREFIX + formattedLines.join('\n' + OUTPUT_PREFIX);
}

function formatCodeLine (lineNumber, line, maxLineNumber) {
    var pad = maxLineNumber.toString().length - lineNumber.toString().length
        , padding = '';
    while (pad-- > 0) {
        padding += ' ';
    }
    if (line.length > MAX_COLUMNS) {
        line = line.slice(0, MAX_COLUMNS - 1) + ELLIPSIS_CHAR;
    }
    return padding + lineNumber + GUTTER_CONTENT + line;
}

function formatCodeArrow (lineNumber, columnNumber, maxLineNumber) {
    var length = (GUTTER_CONTENT + maxLineNumber).length + columnNumber;
    return repeatCharacter(ENDASH_CHAR, length).slice(0, length - 1) + UP_ARROW_CHAR; // ==> repeatCharacter()
}

function repeatCharacter (character, length) {
    return new Array(length).join(character) + character;
}

понедельник, 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();

});

JavaScript Stack Trace Libraries

github.com/hcodes/show-js-error/
xpl.github.io/useless/

Detail