Skip to content

Instantly share code, notes, and snippets.

@ddallala
Created May 23, 2013 22:54
Show Gist options
  • Select an option

  • Save ddallala/5640107 to your computer and use it in GitHub Desktop.

Select an option

Save ddallala/5640107 to your computer and use it in GitHub Desktop.
Simple Local Web Proxy - can replace URLs at the request
/*
** Peteris Krumins ([email protected])
** http://www.catonmat.net -- good coders code, great reuse
**
** A simple proxy server written in node.js.
**
*/
var fs = require('fs');
var config = {
add_proxy_header: false, //activate addition of X-Forwarded-For header for better logging on real server side
allow_ip_list: './config/allow_ip_list',
black_list: './config/black_list',
host_filters: './config/hostfilters.js',
listen: [{
ip: '127.0.0.1',
port: 9090
}, //all ipv4 interfaces
{
ip: '::1',
port: 9090
}
] //all ipv6 interfaces
/*,listen_ssl:[{
ip:'0.0.0.0',//all *secure* ipv4 interfaces
port:443,
key:fs.readFileSync('/path/to/ssl.key'),
cert:fs.readFileSync('/path/to/ssl.crt'),
ca:[fs.readFileSync('/path/to/ca.pem'),
fs.readFileSync('/path/to/sub-ca.pem')]
},{
ip:'::',//all *secure* ipv6 interfaces
port:443,
key:fs.readFileSync('/path/to/ssl.key'),
cert:fs.readFileSync('/path/to/ssl.crt'),
ca:[fs.readFileSync('/path/to/ca.pem'),
fs.readFileSync('/path/to/sub-ca.pem')]
}
]*/
};
exports.config = config;
/*
** Peteris Krumins ([email protected])
** http://www.catonmat.net -- good coders code, great reuse
**
** A simple proxy server written in node.js.
**
** @modified: ddallala
**
*/
var http = require('http'),
https = require('https');
util = require('util');
fs = require('fs'),
config = require('./config').config,
hostfilters = {};
var url_replacements = [
["http://code.jquery.com/jquery-1.9.1.min.js","http://127.0.0.1/test.js"]
];
//support functions
function replace_url(request){
for(var i=0;i<url_replacements.length;i++) {
var tuple = url_replacements[i];
var reg_ex =new RegExp(tuple[0]);
if(reg_ex.test(request.url)){
util.log("replacing: " + request.url + " ==> " + tuple[1]);
request.url = tuple[1];
request.headers.host = tuple[1].replace('http://','').replace('https://','').split(/[/?#]/)[0];
return request;
}
}
return request;
}
//decode host and port info from header
function decode_host(host) {
out = {};
host = host.split(':');
out.host = host[0];
out.port = host[1] || 80;
return out;
}
//encode host field
function encode_host(host) {
return host.host + ((host.port == 80) ? "" : ":" + host.port);
}
//add a X-Forwarded-For header ?
config.add_proxy_header = (config.add_proxy_header !== undefined && config.add_proxy_header == true);
//header decoding
function handle_proxy_route(host, token) {
//extract target host and port
action = decode_host(host);
action.action = "proxyto"; //default action
return action;
}
function prevent_loop(request, response) {
if (request.headers.proxy == "node.jtlebi") { //if request is already tooted => loop
util.log("Loop detected");
response.writeHead(500);
response.write("Proxy loop !");
response.end();
return false;
} else { //append a tattoo to it
request.headers.proxy = "node.jtlebi";
return request;
}
}
function clean_local(request){
util.log(request.url + "==>");
request.url = request.url.replace(/^http:\/\/localhost/,"");
request.url = request.url.replace(/^http:\/\/127.0.0.1/,"");
util.log(request.url);
return request;
}
function action_notfound(response, msg) {
response.writeHead(404);
response.write(msg);
response.end();
}
function action_proxy(response, request, host) {
util.log("Proxying to " + host);
//detect HTTP version
var legacy_http = request.httpVersionMajor == 1 && request.httpVersionMinor < 1 || request.httpVersionMajor < 1;
//launch new request + insert proxy specific header
var headers = request.headers;
if (config.add_proxy_header) {
if (headers['X-Forwarded-For'] !== undefined) {
headers['X-Forwarded-For'] = request.connection.remoteAddress + ", " + headers['X-Forwarded-For'];
} else {
headers['X-Forwarded-For'] = request.connection.remoteAddress;
}
}
var proxy = http.createClient(action.port, action.host);
var proxy_request = proxy.request(request.method, request.url, request.headers);
//deal with errors, timeout, con refused, ...
proxy.on('error', function (err) {
util.log(err.toString() + " on request to " + host);
return action_notfound(response, "Requested resource (" + request.url + ") is not accessible on host \"" + host + "\"");
});
//proxies to FORWARD answer to real client
proxy_request.addListener('response', function (proxy_response) {
if (legacy_http && proxy_response.headers['transfer-encoding'] != undefined) {
console.log("legacy HTTP: " + request.httpVersion);
//filter headers
var headers = proxy_response.headers;
delete proxy_response.headers['transfer-encoding'];
var buffer = "";
//buffer answer
proxy_response.addListener('data', function (chunk) {
buffer += chunk;
});
proxy_response.addListener('end', function () {
headers['Content-length'] = buffer.length; //cancel transfer encoding "chunked"
response.writeHead(proxy_response.statusCode, headers);
response.write(buffer, 'binary');
response.end();
});
} else {
//send headers as received
response.writeHead(proxy_response.statusCode, proxy_response.headers);
//easy data forward
proxy_response.addListener('data', function (chunk) {
response.write(chunk, 'binary');
});
proxy_response.addListener('end', function () {
response.end();
});
}
});
//proxies to SEND request to real server
request.addListener('data', function (chunk) {
proxy_request.write(chunk, 'binary');
});
request.addListener('end', function () {
proxy_request.end();
});
}
//special security logging function
function security_log(request, response, msg) {
var ip = request.connection.remoteAddress;
msg = "**SECURITY VIOLATION**, " + ip + "," + (request.method || "!NO METHOD!") + " " + (request.headers.host || "!NO HOST!") + "=>" + (request.url || "!NO URL!") + "," + msg;
console.log(msg);
}
//security filter
// true if OK
// false to return immediatlely
function security_filter(request, response) {
//HTTP 1.1 protocol violation: no host, no method, no url
if (request.headers.host === undefined ||
request.method === undefined ||
request.url === undefined) {
security_log(request, response, "Either host, method or url is poorly defined");
return false;
}
return true;
}
//actual server loop
function server_cb(request, response) {
console.log("----------------------------------------------------");
//the *very* first action here is to handle security conditions
//all related actions including logging are done by specialized functions
//to ensure compartimentation
if (!security_filter(request, response)) return;
var ip = request.connection.remoteAddress;
//loop filter
request = prevent_loop(request, response);
if (!request) {
return;
}
util.log(ip + ": " + request.method + " " + request.headers.host + "=>" + request.url);
// replace urls before sending out
request = replace_url(request);
//calc new host info
var action = handle_proxy_route(request.headers.host, true);
host = encode_host(action);
// clean request to remove 127.0.0.1 and localhost
request = clean_local(request);
//handle action
action_proxy(response, request, host);
}
//last chance error handler
//it catch the exception preventing the application from crashing.
//I recommend to comment it in a development environment as it
//"Hides" very interesting bits of debugging informations.
process.on('uncaughtException', function (err) {
console.log('LAST ERROR: Caught exception: ' + err);
util.log(err.stack);
});
//http
config.listen.forEach(function (listen) {
util.log("Starting reverse proxy server on port '" + listen.ip + ':' + listen.port);
http.createServer(server_cb).listen(listen.port, listen.ip);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment