日期:2014-05-16  浏览次数:20403 次

nodejs代替goagent
转载http://www.cnblogs.com/hackwaly/archive/2012/06/23/2559474.html


以前一直在用goagent  fq,不过受限于gae,goagent有两个比较明显的缺点:一是不支持socks,二是访问https网站需要安装证书。
趁着端午节的空闲时间,琢磨着找一个国外的免费的nodejs的AppEngine,然后写一个代理部署到上面,也就可以 fq 了
我选择的是http://nodejitsu.com,现在还处于private beta,开放注册,可以免费试用。除了它我目前还没有找到免费的可用的并且支持net模块的appEngine了。

要注意两点:
1,申请稍微麻烦一点,需要npm安装一个jitsu的客户端进行验证,这也是防止不会用的人瞎申请浪费。
2,目前nodejitsu每个app只允许绑定一个80端口,这个端口不支持tcp(它的宣传里说支持任意多个端口,但坑我了一个下午才在它的github里找到说明,是以后会支持),原因是nodejitsu使用http协议里的"Host" header来实现公网到app之间的路由。

由于nodejitsu不支持tcp端口的监听,所以得走一个http的tunnel才行,这就要求我们实现一个服务器端和一个客户端,由客户端提供socks代理端口,而代理请求则通过httptunnel交给服务器端去处理。
socks5服务器的实现代码我就不贴了,大家可以参考https://github.com/gvangool/node-socks
里面建立连接的时候是直接建立连接的,所以在本地跑的话,没有 fq效果,只能作为代理使用,我们需要把建立连接的部分替换成建立http tunnel的连接。

下面是代码:
客户端建立httptunnel连接部分

function toBase64(str){
    return new Buffer(str).toString('base64');
}
function httpRequest(host, port){
    var opts = {
        host: 'your-proxy.your-subdomain.jit.su',
        port: 80,
        agent: false,
        // 这里需要转义以避开gfw的url检测.
        path: '/' + querystring.escape(toBase64(JSON.stringify({
            host: host,
            port: port
        }))),
        method: 'POST'
    };
    return http.request(opts);
}

/**
 * 建立代理连接.
 * @param socket socks5端口连接到的socket
 * @param chunk socks5代理请求的包, 用于socks5协议响应.
 * @param host 请求连接的host
 * @param port 请求连接的port
 */
function proxy(socket, chunk, host, port){
    var httpReq = httpRequest(host, port);
    // 维持长连接.
    httpReq.setSocketKeepAlive(1024);
    // 发一个无用的字符使request发出第一个数据包, 以便服务器接受并建立连接.
    httpReq.write('\n');
    httpReq.on('response', function (httpRes){
        // socks5协议部分,
        var resp = new Buffer(chunk.length);
        chunk.copy(resp);
        // rewrite response header
        resp[0] = SOCKS_VERSION;
        resp[1] = 0x00;
        resp[2] = 0x00;
        // 告诉socks5客户端连接建议完毕.
        socket.write(resp);

        // 忽略那个无用的"\n".
        httpRes.once('data', function (chunk){
            if (chunk.toString() === '\n') {
                httpRes.pipe(socket);
                console.log('TUNNEL SUCCESS');
            } else {
                console.log('TUNNEL FAIL');
            }
        });
        httpRes.on('close', function (){
            socket.end();
        });
    });
    socket.setKeepAlive(1024);
    socket.pipe(httpReq);
    socket.on('close', function (){
        httpReq.abort();
    });
}



服务器端

var http = require('http');
var net = require('net');
var querystring = require('querystring');

function fromBase64(str){
    return new Buffer(str, 'base64').toString();
}
http.createServer(function (req, res){
    var url = req.url;
    // 这里解码path为host和port
    var params = JSON.parse(fromBase64(querystring.unescape(url.slice(1))));
    var host = params.host;
    var port = params.port;
    console.log('CONNECT: ' + host + ':' + port);
    var buff = [];
    function addChunk(chunk){
        buff.push(chunk);
    }
    // 忽略第一个用来强制http发包的'\n'
    req.once('data', function (chunk){
        if (chunk.toString() === '\n') {
            req.on('data', addChunk);
        }
    });
    var socket = net.connect(port, host, function (){
        req.removeListener('data', addChunk);
        res.writeHead(200);
        // 貌似nodejs为了优化, writeHead并不会立即响应给客户端.
        // 所以这里需要额外发一个没用的字符.
        res.write('\n');
        for (var i=0; i<buff.length; i++) {
            socket.write(buff[i]);
        }
        req.pipe(socket);
        socket.pipe(res);
        console.log('CONNECTED: ' + host + ':' + port);
    });
}).listen(80);



之所以没有提供完整的代码也是为了防止nodejitsu的压力猛增,从而关闭免费试用。希望大家谅解,其实若是有心则很容易就可以搞定了。