Web 代理是一种存在于网络中间的实体,提供各式各样的功能。现代网络系统中,Web 代理无处不在。我以前有关 HTTP 的博文中,屡次提到了代理对 HTTP 请求及响应的影响。今天这篇文章,我打算谈谈 HTTP 代理自己的一些原理,以及如何用 Node.js 快速实现代理。web
HTTP 代理存在两种形式,分别简单介绍以下:浏览器
第一种是 RFC 7230 - HTTP/1.1: Message Syntax and Routing(即修订后的 RFC 2616,HTTP/1.1 协议的第一部分)描述的普通代理。这种代理扮演的是「中间人」角色,对于链接到它的客户端来讲,它是服务端;对于要链接的服务端来讲,它是客户端。它就负责在两端之间来回传送 HTTP 报文。安全
第二种是 Tunneling TCP based protocols through Web proxy servers(经过 Web 代理服务器用隧道方式传输基于 TCP 的协议)描述的隧道代理。它经过 HTTP 协议正文部分(Body)完成通信,以 HTTP 的方式实现任意基于 TCP 的应用层协议代理。这种代理使用 HTTP 的 CONNECT 方法创建链接,但 CONNECT 最开始并非 RFC 2616 - HTTP/1.1 的一部分,直到 2014 年发布的 HTTP/1.1 修订版中,才增长了对 CONNECT 及隧道代理的描述,详见 RFC 7231 - HTTP/1.1: Semantics and Content。实际上这种代理早就被普遍实现。服务器
本文描述的第一种代理,对应《HTTP 权威指南》一书中第六章「代理」;第二种代理,对应第八章「集成点:网关、隧道及中继」中的 8.5 小节「隧道」。网络
普通代理
第一种 Web 代理原理特别简单:app
下面这张图片来自于《HTTP 权威指南》,直观地展现了上述行为:post
假如我经过代理访问 A 网站,对于 A 来讲,它会把代理当作客户端,彻底察觉不到真正客户端的存在,这实现了隐藏客户端 IP 的目的。固然代理也能够修改 HTTP 请求头部,经过 X-Forwarded-IP
这样的自定义头部告诉服务端真正的客户端 IP。但服务器没法验证这个自定义头部真的是由代理添加,仍是客户端修改了请求头,因此从 HTTP 头部字段获取 IP 时,须要格外当心。这部份内容能够参考我以前的《HTTP 请求头中的 X-Forwarded-For》这篇文章。
给浏览器显式的指定代理,须要手动修改浏览器或操做系统相关设置,或者指定 PAC(Proxy Auto-Configuration,自动配置代理)文件自动设置,还有些浏览器支持 WPAD(Web Proxy Autodiscovery Protocol,Web 代理自动发现协议)。显式指定浏览器代理这种方式通常称之为正向代理,浏览器启用正向代理后,会对 HTTP 请求报文作一些修改,来规避老旧代理服务器的一些问题,这部份内容能够参考我以前的《Http 请求头中的 Proxy-Connection》这篇文章。
还有一种状况是访问 A 网站时,实际上访问的是代理,代理收到请求报文后,再向真正提供服务的服务器发起请求,并将响应转发给浏览器。这种状况通常被称之为反向代理,它能够用来隐藏服务器 IP 及端口。通常使用反向代理后,须要经过修改 DNS 让域名解析到代理服务器 IP,这时浏览器没法察觉到真正服务器的存在,固然也就不须要修改配置了。反向代理是 Web 系统最为常见的一种部署方式,例如本博客就是使用 Nginx 的 proxy_pass
功能将浏览器请求转发到背后的 Node.js 服务。
了解完第一种代理的基本原理后,咱们用 Node.js 实现一下它。只包含核心逻辑的代码以下:
JSvar http = require('http');
var net = require('net');
var url = require('url');
function request(cReq, cRes) {
var u = url.parse(cReq.url);
var options = {
hostname : u.hostname,
port : u.port || 80,
path : u.path,
method : cReq.method,
headers : cReq.headers
};
var pReq = http.request(options, function(pRes) {
cRes.writeHead(pRes.statusCode, pRes.headers);
pRes.pipe(cRes);
}).on('error', function(e) {
cRes.end();
});
cReq.pipe(pReq);
}
http.createServer().on('request', request).listen(8888, '0.0.0.0');
以上代码运行后,会在本地 8888
端口开启 HTTP 代理服务,这个服务从请求报文中解析出请求 URL 和其余必要参数,新建到服务端的请求,并把代理收到的请求转发给新建的请求,最后再把服务端响应返回给浏览器。修改浏览器的 HTTP 代理为 127.0.0.1:8888
后再访问 HTTP 网站,代理能够正常工做。
可是,使用咱们这个代理服务后,HTTPS 网站彻底没法访问,这是为何呢?答案很简单,这个代理提供的是 HTTP 服务,根本没办法承载 HTTPS 服务。那么是否把这个代理改成 HTTPS 就能够了呢?显然也不能够,由于这种代理的本质是中间人,而 HTTPS 网站的证书认证机制是中间人劫持的克星。普通的 HTTPS 服务中,服务端不验证客户端的证书,中间人能够做为客户端与服务端成功完成 TLS 握手;可是中间人没有证书私钥,不管如何也没法伪形成服务端跟客户端创建 TLS 链接。固然若是你拥有证书私钥,代理证书对应的 HTTPS 网站固然就没问题了。
HTTP 抓包神器 Fiddler 的工做原理也是在本地开启 HTTP 代理服务,经过让浏览器流量走这个代理,从而实现显示和修改 HTTP 包的功能。若是要让 Fiddler 解密 HTTPS 包的内容,须要先将它自带的根证书导入到系统受信任的根证书列表中。一旦完成这一步,浏览器就会信任 Fiddler 后续的「伪造证书」,从而在浏览器和 Fiddler、Fiddler 和服务端之间都能成功创建 TLS 链接。而对于 Fiddler 这个节点来讲,两端的 TLS 流量都是能够解密的。
若是咱们不导入根证书,Fiddler 的 HTTP 代理还能代理 HTTPS 流量么?实践证实,不导入根证书,Fiddler 只是没法解密 HTTPS 流量,HTTPS 网站仍是能够正常访问。这是如何作到的,这些 HTTPS 流量是否安全呢?这些问题将在下一节揭晓。
隧道代理
第二种 Web 代理的原理也很简单:
下面这张图片一样来自于《HTTP 权威指南》,直观地展现了上述行为:
假如我经过代理访问 A 网站,浏览器首先经过 CONNECT 请求,让代理建立一条到 A 网站的 TCP 链接;一旦 TCP 链接建好,代理无脑转发后续流量便可。因此这种代理,理论上适用于任意基于 TCP 的应用层协议,HTTPS 网站使用的 TLS 协议固然也能够。这也是这种代理为何被称为隧道的缘由。对于 HTTPS 来讲,客户端透过代理直接跟服务端进行 TLS 握手协商密钥,因此依然是安全的,下图中的抓包信息显示了这种场景:
能够看到,浏览器与代理进行 TCP 握手以后,发起了 CONNECT 请求,报文起始行以下:
CONNECT imququ.com:443 HTTP/1.1
对于 CONNECT 请求来讲,只是用来让代理建立 TCP 链接,因此只须要提供服务器域名及端口便可,并不须要具体的资源路径。代理收到这样的请求后,须要与服务端创建 TCP 链接,并响应给浏览器这样一个 HTTP 报文:
HTTP/1.1 200 Connection Established
浏览器收到了这个响应报文,就能够认为到服务端的 TCP 链接已经打通,后续直接往这个 TCP 链接写协议数据便可。经过 Wireshark 的 Follow TCP Steam 功能,能够清楚地看到浏览器和代理之间的数据传递:
能够看到,浏览器创建到服务端 TCP 链接产生的 HTTP 往返,彻底是明文,这也是为何 CONNECT 请求只须要提供域名和端口:若是发送了完整 URL、Cookie 等信息,会被中间人尽收眼底,下降了 HTTPS 的安全性。HTTP 代理承载的 HTTPS 流量,应用数据要等到 TLS 握手成功以后经过 Application Data 协议传输,中间节点没法得知用于流量加密的 master-secret,没法解密数据。而 CONNECT 暴露的域名和端口,对于普通的 HTTPS 请求来讲,中间人同样能够拿到(IP 和端口很容易拿到,请求的域名能够经过 DNS Query 或者 TLS Client Hello 中的 Server Name Indication 拿到),因此这种方式并无增长不安全性。
了解完原理后,再用 Node.js 实现一个支持 CONNECT 的代理也很简单。核心代码以下:
JSvar http = require('http');
var net = require('net');
var url = require('url');
function connect(cReq, cSock) {
var u = url.parse('http://' + cReq.url);
var pSock = net.connect(u.port, u.hostname, function() {
cSock.write('HTTP/1.1 200 Connection Established\r\n\r\n');
pSock.pipe(cSock);
}).on('error', function(e) {
cSock.end();
});
cSock.pipe(pSock);
}
http.createServer().on('connect', connect).listen(8888, '0.0.0.0');
以上代码运行后,会在本地 8888
端口开启 HTTP 代理服务,这个服务从 CONNECT 请求报文中解析出域名和端口,建立到服务端的 TCP 链接,并和 CONNECT 请求中的 TCP 链接串起来,最后再响应一个 Connection Established 响应。修改浏览器的 HTTP 代理为 127.0.0.1:8888
后再访问 HTTPS 网站,代理能够正常工做。
最后,将两种代理的实现代码合二为一,就能够获得全功能的 Proxy 程序了,所有代码在 50 行之内(固然异常什么的基本没考虑,这是我博客代码的一向风格):
JSvar http = require('http');
var net = require('net');
var url = require('url');
function request(cReq, cRes) {
var u = url.parse(cReq.url);
var options = {
hostname : u.hostname,
port : u.port || 80,
path : u.path,
method : cReq.method,
headers : cReq.headers
};
var pReq = http.request(options, function(pRes) {
cRes.writeHead(pRes.statusCode, pRes.headers);
pRes.pipe(cRes);
}).on('error', function(e) {
cRes.end();
});
cReq.pipe(pReq);
}
function connect(cReq, cSock) {
var u = url.parse('http://' + cReq.url);
var pSock = net.connect(u.port, u.hostname, function() {
cSock.write('HTTP/1.1 200 Connection Established\r\n\r\n');
pSock.pipe(cSock);
}).on('error', function(e) {
cSock.end();
});
cSock.pipe(pSock);
}
http.createServer()
.on('request', request)
.on('connect', connect)
.listen(8888, '0.0.0.0');
须要注意的是,大部分浏览器显式配置了代理以后,只会让 HTTPS 网站走隧道代理,这是由于创建隧道须要耗费一次往返,能不用就尽可能不用。但这并不表明 HTTP 请求不能走隧道代理,咱们用 Node.js 写段程序验证下(先运行前面的代理服务):
JSvar http = require('http');
var options = {
hostname : '127.0.0.1',
port : 8888,
path : 'imququ.com:80',
method : 'CONNECT'
};
var req = http.request(options);
req.on('connect', function(res, socket) {
socket.write('GET / HTTP/1.1\r\n' +
'Host: imququ.com\r\n' +
'Connection: Close\r\n' +
'\r\n');
socket.on('data', function(chunk) {
console.log(chunk.toString());
});
socket.on('end', function() {
console.log('socket end.');
});
});
req.end();
这段代码运行完,结果以下:
HTTP/1.1 301 Moved Permanently Server: nginx Date: Thu, 19 Nov 2015 15:57:47 GMT Content-Type: text/html Content-Length: 178 Connection: close Location: https://imququ.com/ <html> <head><title>301 Moved Permanently</title></head> <body bgcolor="white"> <center><h1>301 Moved Permanently</h1></center> <hr><center>nginx</center> </body> </html> socket end.
能够看到,经过 CONNECT 让代理打开到目标服务器的 TCP 链接,用来承载 HTTP 流量也是彻底没问题的。
最后,HTTP 的认证机制能够跟代理配合使用,使得必须输入正确的用户名和密码才能使用代理,这部份内容比较简单,这里略过。在本文第二部分,我打算谈谈如何把今天实现的代理改造为 HTTPS 代理,也就是如何让浏览器与代理之间的流量走 HTTPS 安全机制。注:已经写完了,点这里查看。
本文连接:https://imququ.com/post/web-proxy.html,参与评论 »
--EOF--