NodeJS实现HTTP/HTTPS代理
May/6th 2011

    身在天朝,难免会用到代理的时候。 比如在学校内网用代理免费上外网,在墙内用代理上404网站等。

    现在使用的代理大部分为HTTP和Socket代理。 Socket代理更底层,需要本地解析域名,而HTTP代理则是基于HTTP协议之上的,不需要本地解析域名。下面我讲讲HTTP(S)代理的设计思路以及NodeJS代码实现。

HTTP协议

    HTTP协议简单说来就是浏览器把一串字符串发送到目标服务器,然后把目标服务器返回回来的一串字符串显示给用户。

    浏览器发送的这串字符主要分为两个部分,一部分是头,里面包含目标服务器域名,当前请求的文件路径等信息。另一部分是正文,一般的GET请求没有正文。

    服务器返回来的字符串也分为头和正文。

HTTP代理原理

    HTTP代理需要做的事情就是接收浏览器发来的请求字符串,再从请求字符串的头部分找出浏览器请求的目标主机,然后直接把这串请求字符串发给目标主机,再把目标主机返回的数据发给浏览器。 “什么?就这么简单?” “呃。。是啊,但这还没完。。”

    现代浏览器一般都是默认采用HTTP/1.1版本,并且默认会发送Connection: keep-alive请求。 这些信息是写在请求的头部的,意思是通知目标服务器采用keep-alive技术继续处理后续的请求。 但是我们做的代理程序要想支持keep-alive是比较麻烦的。所以干脆就把这个篡改成Connection: close。 这样就可以保证浏览器请求的每个文件都会单独发送一个HTTP请求。

下面是NodeJS代码实现

 

var net = require('net');
var local_port = 8893;

//在本地创建一个server监听本地local_port端口
net.createServer(function (client)
{
    
    //首先监听浏览器的数据发送事件,直到收到的数据包含完整的http请求头
    var buffer = new Buffer(0);
    client.on('data',function(data)
    {
        buffer = buffer_add(buffer,data);
        if (buffer_find_body(buffer== -1return;
        var req = parse_request(buffer);
        if (req === falsereturn;
        client.removeAllListeners('data');
        relay_connection(req);
    });

    //从http请求头部取得请求信息后,继续监听浏览器发送数据,同时连接目标服务器,并把目标服务器的数据传给浏览器
    function relay_connection(req)
    {
        console.log(req.method+' '+req.host+':'+req.port);
        
        //如果请求不是CONNECT方法(GET, POST),那么替换掉头部的一些东西
        if (req.method != 'CONNECT')
        {
            //先从buffer中取出头部
            var _body_pos = buffer_find_body(buffer);
            if (_body_pos < 0_body_pos = buffer.length;
            var header = buffer.slice(0,_body_pos).toString('utf8');
            //替换connection头
            header = header.replace(/(proxy\-)?connection\:.+\r\n/ig,'')
                    .replace(/Keep\-Alive\:.+\r\n/i,'')
                    .replace("\r\n",'\r\nConnection: close\r\n');
            //替换网址格式(去掉域名部分)
            if (req.httpVersion == '1.1')
            {
                var url = req.path.replace(/http\:\/\/[^\/]+/,'');
                if (url.path != urlheader = header.replace(req.path,url);
            }
            buffer = buffer_add(new Buffer(header,'utf8'),buffer.slice(_body_pos));
        }
        
        //建立到目标服务器的连接
        var server = net.createConnection(req.port,req.host);
        //交换服务器与浏览器的数据
        client.on("data", function(data){ server.write(data); });
        server.on("data", function(data){ client.write(data); });

        if (req.method == 'CONNECT')
            client.write(new Buffer("HTTP/1.1 200 Connection established\r\nConnection: close\r\n\r\n"));
        else
            server.write(buffer);
    }
}).listen(local_port);

console.log('Proxy server running at localhost:'+local_port);


//处理各种错误
process.on('uncaughtException', function(err)
{
    console.log("\nError!!!!");
    console.log(err);
});



/**
* 从请求头部取得请求详细信息
* 如果是 CONNECT 方法,那么会返回 { method,host,port,httpVersion}
* 如果是 GET/POST 方法,那么返回 { metod,host,port,path,httpVersion}
*/
function parse_request(buffer)
{
    var s = buffer.toString('utf8');
    var method = s.split('\n')[0].match(/^([A-Z]+)\s/)[1];
    if (method == 'CONNECT')
    {
        var arr = s.match(/^([A-Z]+)\s([^\:\s]+)\:(\d+)\sHTTP\/(\d\.\d)/);
        if (arr && arr[1] && arr[2] && arr[3] && arr[4])
            return { method: arr[1], host:arr[2], port:arr[3],httpVersion:arr[4] };
    }
    else
    {
        var arr = s.match(/^([A-Z]+)\s([^\s]+)\sHTTP\/(\d\.\d)/);
        if (arr && arr[1] && arr[2] && arr[3])
        {
            var host = s.match(/Host\:\s+([^\n\s\r]+)/)[1];
            if (host)
            {
                var _p = host.split(':',2);
                return { method: arr[1], host:_p[0], port:_p[1]?_p[1]:80, path: arr[2],httpVersion:arr[3] };
            }
        }
    }
    return false;
}




/**
* 两个buffer对象加起来
*/
function buffer_add(buf1,buf2)
{
    var re = new Buffer(buf1.length + buf2.length);
    buf1.copy(re);
    buf2.copy(re,buf1.length);
    return re;
}

/**
* 从缓存中找到头部结束标记("\r\n\r\n")的位置
*/
function buffer_find_body(b)
{
    for(var i=0,len=b.length-3;i<len;i++)
    {
        if (b[i] == 0x0d && b[i+1] == 0x0a && b[i+2] == 0x0d && b[i+3] == 0x0a)
        {
            return i+4;
        }
    }
    return -1;
}

 

另外,可以用 "nohup node some.js > /dev/null &" 命令让nodejs程序在后台运行。



30718 read 32 comment(s)
#1
宇博   2011年05月06号 17:16       回复
支持你。nodeJS确实很好,发现你也用上了
#2
longbill   2011年05月06号 17:17       回复
@宇博 哈哈哈,已经用很久了。
#3
诺菲尼   2011年05月16号 14:30       回复
水平有限,代码不怎么懂
#4
防晒霜排行榜   2011年06月19号 16:18       回复
看不懂。。。。。。。。。。。
#5
内蒙古人事考试   2011年11月03号 17:39       回复
你太专业了,我还不太懂
#6
wen   2012年02月16号 12:54       回复
龙哥 你好 关注你很长时间了 当我刚看到你写的nodejs的时候 我当时没有什么感觉 但是 最近几天我刚开始关注nodejs 于是就一下子想到了你的博客 但是我初步测试了一下你的nodejs代理 但是没有成功 还是不能上facebook之类的网站 求解!!!
#7
wen   2012年02月16号 12:56       回复
这个网站是你自己做的么?这个网站渐变加载显示的效果怎么弄的啊?感觉挺有意思的 是jquery吗??
#8
longbill   2012年02月16号 13:04       回复
@wen 这个只是代理服务器,中间没有加密的。所以就算你把这个代理程序放到国外的服务器,也可能被墙。
#9
longbill   2012年02月16号 13:04       回复
@wen 这个是判断浏览器滚动到哪个位子,然后再加载图片。
#10
wen   2012年02月17号 14:52       回复
@longbill 够牛 我就觉得这个加载的效果挺棒的 能告诉我哪里有类似的源码下载吗? 
#11
James   2012年11月12号 17:38       回复
你好,尝试了你的nodejs代理程序,让人惊艳。
有些地方不太明白,为什么要交换服务器和浏览器的数据?如果只把服务器的数据写道浏览器,似乎也一切正常。
#12
longbill   2012年11月12号 17:41       回复
@James 当然要交换数据啦。不然上传文件怎么办?
#13
longbill   2012年11月12号 17:43       回复
@James 代理程序需要尽快解析浏览器发起的http请求,然后连接到目标服务器。然后后面浏览器可能还会继续发送数据(post)
#14
James   2012年11月12号 17:45       回复
@longbill 
哦,原来如此,
实际上net.createServer中的client就是本地的request对象吧?没有看到nodejs的文档里有client对象
#15
longbill   2012年11月12号 17:47       回复
@James client只是一个socket对象的名字而已。我记得client指的是浏览器发起的socket连接。
#16
James   2012年11月12号 18:02       回复
@longbill 
非常感谢!有什么联系方式吗?nodejs方面还要多多请教!
#17
James   2012年12月02号 21:07       回复
@longbill 
替换网址格式(去掉域名部分) 这部分有个url.path != url,请解释一下,谢谢!
#18
longbill   2012年12月02号 21:13       回复
@James 请求第一行 GET / HTTP/1.1,但是有时候代理服务器得到的是 GET http://xx.com/ HTTP/1.1,有时候会有问题,所以去掉了前面的域名部分。
#19
James   2012年12月02号 21:15       回复
哦,问题是url.path是从哪里来的?
#20
longbill   2012年12月02号 21:16       回复
@James /**
* 从请求头部取得请求详细信息
* 如果是 CONNECT 方法,那么会返回 { method,host,port,httpVersion}
* 如果是 GET/POST 方法,那么返回 { metod,host,port,path,httpVersion}
*/
function parse_request(buffer)
#21
James   2012年12月02号 21:19       回复
@longbill 
晕啊,请注意一下您的代码
 var url = req.path.replace(/http\:\/\/[^\/]+/,'');
                if (url.path != url) header = header.replace(req.path,url);
刚刚定义了url变量啊,这个url的path是怎么来的?
#22
James   2012年12月02号 21:28       回复
@longbill ?不屑回答吗?为什么我觉得这个url.path应该是req.path?解释一下吧,多谢!
#23
longbill   2012年12月02号 21:29       回复
@James 那貌似我写错了。应该是req.path
#24
James   2012年12月02号 21:34       回复
@longbill 哈哈,同学,你可是把我耍到了。不过也好,console.log了这么久,总算是大概明白了
#25
longbill   2012年12月02号 21:36       回复
@James 我又不是故意的。这个代码是从我一个更复杂的翻墙版本代理里面提取出来的,大概测试了下,没问题就发出来了。大部分web server对这个是不敏感的。
#26
James   2012年12月02号 21:41       回复
@longbill 好啦好啦,不要这么委屈了,想不到已经有nodejs的翻墙代理了,居然比这复杂,共享一下吧,兄弟,发到我email好不好
#27
longbill   2012年12月02号 23:08       回复
@James 不好意思,暂时不外传。
#28
James   2012年12月03号 11:21       回复
@longbill 可以理解,其实我也是打算做一个类似的东西,而且也不想外传,哈哈。其实我觉得您这段代码做为代理够用了,要加的可能就是加密了,不知道您这边所谓更复杂主要是指哪些方面?指点一下吧?
#29
longbill   2012年12月03号 15:05       回复
@James 很简单啊。本地开一个代理,国外服务器开一个代理。协商好加密解密算法就ok了。
#30
nill   前年01月29日 10:49       回复
这个实现更简单 .
https://www.npmjs.com/package/simplest_node_proxy
#31
帅呆的猪蹄   前年09月09日 16:09       回复
求教大神, 这段代码我改如何使用?
添加新的评论
称呼:*
邮件:*
网站:
内容:

Copyright © Longbill 2008-2017 , Designed by EndTo , Powered by EndCMS