HTTP和TCP代理原理及实现,主要是理解

Web 代理是一种存在于网络中间的实体,提供各式各样的功能。现代网络系统中,Web 代理无处不在。我之前有关 HTTP 的博文中,多次提到了代理对 HTTP 请求及响应的影响。今天这篇文章,我打算谈谈 HTTP 代理本身的一些原理,以及如何用 Node.js 快速实现代理。

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 代理原理特别简单:

HTTP 客户端向代理发送请求报文,代理服务器需要正确地处理请求和连接(例如正确处理 Connection: keep-alive),同时向服务器发送请求,并将收到的响应转发给客户端。

下面这张图片来自于《HTTP 权威指南》,直观地展示了上述行为:

web_proxy

假如我通过代理访问 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 客户端通过 CONNECT 方法请求隧道代理创建一条到达任意目的服务器和端口的 TCP 连接,并对客户端和服务器之间的后继数据进行盲转发。

下面这张图片同样来自于《HTTP 权威指南》,直观地展示了上述行为:

web_tunnel

假如我通过代理访问 A 网站,浏览器首先通过 CONNECT 请求,让代理创建一条到 A 网站的 TCP 连接;一旦 TCP 连接建好,代理无脑转发后续流量即可。所以这种代理,理论上适用于任意基于 TCP 的应用层协议,HTTPS 网站使用的 TLS 协议当然也可以。这也是这种代理为什么被称为隧道的原因。对于 HTTPS 来说,客户端透过代理直接跟服务端进行 TLS 握手协商密钥,所以依然是安全的,下图中的抓包信息显示了这种场景:

wireshark_connect

可以看到,浏览器与代理进行 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 功能,可以清楚地看到浏览器和代理之间的数据传递:

wireshark_connect_detail

可以看到,浏览器建立到服务端 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 安全机制。注:已经写完了,点这里查看。

原文链接:HTTP 代理原理及实现(一) | JerryQu 的小站

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/597548.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++完成Query执行sql语句的接口封装和测试

1、在LXMysql.h 创建Query执行函数 //封装 执行sql语句 if sqllen 0 strlen获取字符长度bool Query(const char*sql,unsigned long sqllen0); 2、在LXMysql.cpp编写函数 bool LXMysql::Query(const char* sql, unsigned long sqllen){if (!mysql)//如果mysql没有初始化好{c…

使用qtquick调用python程序

一. 内容简介 使用qtquick调用python程序 二. 软件环境 2.1vsCode 2.2Anaconda version: conda 22.9.0 2.3pytorch 安装pytorch(http://t.csdnimg.cn/GVP23) 2.4QT 5.14.1 新版QT6.4,&#xff0c;6.5在线安装经常失败&#xff0c;而5.9版本又无法编译64位程序&#xf…

UE4 4.21使用编辑器蓝图EditorBlueprint方法

在UE4 4.21中&#xff0c;编辑器蓝图&#xff08;Editor Blueprint&#xff09;是一个强大的工具&#xff0c;允许开发者扩展和自定义Unreal编辑器的功能。通过编辑器蓝图&#xff0c;我们可以创建自定义的工具和功能&#xff0c;以优化开发流程。 本教程将指导您如何在UE4 4.…

【LeetCode-剑指offer】--19.验证回文串II

19.验证回文串II 方法&#xff1a;双指针 首先考虑如果不允许删除字符&#xff0c;如何判断一个字符串是否是回文串。常见的做法是使用双指针。定义左右指针&#xff0c;初始时分别指向字符串的第一个字符和最后一个字符&#xff0c;每次判断左右指针指向的字符是否相同&#…

技术学习|CDA level I 多维数据透视分析

对基于多源表的结构数据进行商业智能分析&#xff0c;可以帮助决策者从多个不同业务角度对业务行为结果进行观测&#xff0c;进而帮助决策者全面、精确地定位业务问题&#xff0c;实现商业洞察的相关内容。通过商业智能分析产出的分析成果被统称为商业智能报表&#xff0c;简称…

ubuntu创建pytorch-gpu的docker环境

文章目录 安装docker创建镜像创建容器 合作推广&#xff0c;分享一个人工智能学习网站。计划系统性学习的同学可以了解下&#xff0c;点击助力博主脱贫( •̀ ω •́ )✧ 使用docker的好处就是可以将你的环境和别人的分开&#xff0c;特别是共用的情况下。本文介绍了ubuntu环境…

4.快速实现增删改查,模糊查询功能

打开springboot项目&#xff0c;在com.example下建包common,在common下新建Result.java 4.1封装统一的返回数据结构 1.在Result.java中编写如下代码&#xff1a; private static final String *SUCCESS*"0"; private static final String *ERROR*"-1"; p…

智能穿戴时代 | 米客方德SD NAND的崭新优势

SD NAND在智能穿戴上的优势 SD NAND是一种可以直接焊接在智能穿戴设备主板上的存储芯片&#xff0c;其小型化设计有助于紧凑设备尺寸&#xff0c;同时提供可靠的嵌入式存储解决方案。 这种集成设计减少了空间占用&#xff0c;同时确保设备在高度活动的环境中更为稳定。SD NAND…

[足式机器人]Part2 Dr. CAN学习笔记-动态系统建模与分析 Ch02-3流体系统建模

本文仅供学习使用 本文参考&#xff1a; B站&#xff1a;DR_CAN Dr. CAN学习笔记-动态系统建模与分析 Ch02-12课程介绍电路系统建模、基尔霍夫定律 流量 flow rate q q q m 3 / s m^3/s m3/s 体积 volume V V V m 3 m^3 m3 高度 heigh h h h m m m 压强 pressure p p p …

Jvm垃圾收集器系列之Parallel Scavenge收集器(个人见解仅供参考)

问&#xff1a;什么是Parallel Scavenge&#xff1f; 答&#xff1a;Parallel Scavenge是Java HotSpot虚拟机中的一种垃圾收集器&#xff0c;它主要用于提高应用程序的吞吐量。 问&#xff1a;Parallel Scavenge的主要目标是什么&#xff1f; 答&#xff1a;Parallel Scavenge的…

Azure Machine Learning - 人脸识别任务概述与技术实战

Azure AI 人脸服务提供了可检测、识别和分析图像中的人脸的 AI 算法。 人脸识别软件在许多不同情形中都十分重要&#xff0c;例如识别、无接触访问控制和实现隐私的人脸模糊。你可以通过客户端库 SDK&#xff0c;或者直接调用 REST API 使用人脸服务。 目录 一、人脸识别服务场…

【Android】如何设置应用程序启动Activity(应用启动时显示的界面)

前言 在Android中&#xff0c;AndroidManifest.xml文件可以通过修改来设置应用启动时显示的界面&#xff0c;即启动Activity。 操作步骤 打开AndroidManifest.xml文件。 在文件中找到想要设置为启动Activity的<activity>元素。该元素通常在<application>元素内部…

非小米电脑安装小米电脑管家

博客地址 雪饼 (xue6ing.cn) 前言 小米电脑管家是一款专为小米笔记本用户设计的电脑管理软件。它提供了一系列强大的功能&#xff0c;包括驱动管理、硬件信息、设备互联等。其内置了小米的HyperConnect跨端智联功能。 如果是小米手机用户的话非常建议你安装&#xff01;因为…

EasyExcel写出包含多个sheet页的Excel

https://blog.csdn.net/qq_38751895/article/details/131852740

虚拟机添加显示屏

1、关闭虚拟机&#xff0c;虚拟机在为关机的情况下&#xff0c;虚拟机设置->显示器->监视器 都是灰色的&#xff0c;不能设置&#xff1b; 2、虚拟机设置->显示器->监视器 “监视器数量” 设置为2 “拉伸模式” 不要勾选 点确定 3、点击 查看->循环使用多个…

增删改查语句实现了解不同的函数与特殊字符unionunion all区别

目录 一、crud&#xff08;增删改查&#xff09; 1.1、查询 概念&#xff1a; 语法&#xff1a; 分组 筛选 排序 分页 1.2、增加 概念&#xff1a; 语法&#xff1a; 方式一&#xff1a;insert into 表名 values (值&#xff0c;值....) 方式二&#xff1a;&#x…

福建科立讯通信 指挥调度管理平台 多处文件上传漏洞复现

0x01 产品简介 福建科立讯通信指挥调度管理平台是一个专门针对通信行业的管理平台。该产品旨在提供高效的指挥调度和管理解决方案,以帮助通信运营商或相关机构实现更好的运营效率和服务质量。该平台提供强大的指挥调度功能,可以实时监控和管理通信网络设备、维护人员和工作任…

JVM工作原理与实战(六):类的生命周期-连接阶段

专栏导航 JVM工作原理与实战 RabbitMQ入门指南 从零开始了解大数据 目录 专栏导航 前言 一、类的生命周期 1.加载&#xff08;Loading&#xff09; 2.连接&#xff08;Linking&#xff09; 3.初始化&#xff08;Initialization&#xff09; 4.使用&#xff08;Using&…

【CSS】设置0.5px的边框宽度

直接写border: 0.5px solid red; 这样在移动端可能会出现问题&#xff0c;下面说下解决办法&#xff1a; 直接上代码&#xff1a; <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-C…

STM32 内部 EEPROM 读写

STM32 的某些系列 MCU 自带 EEPROM。笔者使用的 STM32L151RET6 自带 16 KB 的 EEPROM&#xff0c;可以用来存储自定义的数据。在芯片选型时&#xff0c;自带 EEPROM 也可以作为一个考量点&#xff0c;省去了在外接 EEPROM 的烦恼。 下面简单介绍下 STM32 内部 EEPROM 的读写流…