如何制作一个反向代理服务器以及处理细节
Opened this issue · 0 comments
新的一年,新的气象,好久没有更新过博客了。
在这里祝大家新年快乐,大吉大利。
如何反向代理一个站点?
代理,大家都不陌生,大体上分为正向代理和反向代理。常见的有
- 正向代理: (HTTP/Socket5/v2ray/shadowSocket)
- 反向代理: (Nginx/Apache)
本文就讲解,如何制作一个反向代理工具,并且代理任意站点。
市面上已经有那么多代理工具,为什么还要折腾一个?
起因
在开发时,我们可能会有这样的一些需求:
- 如何调试/在线修改站点?(非 SPA)
像 Chrome 浏览器的开发者工具里面就有 override 功能,可以修改服务器返回的资源(HTML/CSS/JS),并映射到本地目录。
但它有一个缺陷:如果修改的站点带有端口,就无法映射到本地,因为 :
是一个非法字符,无法在文件系统中创建,或者你可以试试创建一个目录 localhost:8080
ref: https://stackoverflow.com/questions/70337046/chrome-local-overrides-with-port-number
- 调试 微信 等一些对 HTTPS 有要求的平台?
对于一些平台的上线,有要求使用 HTTPS,但你的站点又没有,在开发阶段,可以暂时通过代理的方式解决。
- 如何调试微信这类的 Web?
不止微信,还有支付宝等内嵌的 H5 页面如何调试?简单的修改和打印信息,根本就不需要发布,通过代理即可解决。
- 如何向同事或其他人展示不存在的页面?
比如我要给同事展示某网站,需要科学上网,但是他/她却没有,这时候就可以在本机通过代理的方式呈现出来。
就出于以上几点,我觉得,撸一个反向代理工具。
技术选型
采用 Golang 进行开发,除了标准库里面支持反向代理之外,还因为它非常简单的交叉编译。
这是网上随便找的代码
import (
"log"
"net/http"
"net/http/httputil"
"net/url"
)
// NewProxy takes target host and creates a reverse proxy
// NewProxy 拿到 targetHost 后,创建一个反向代理
func NewProxy(targetHost string) (*httputil.ReverseProxy, error) {
url, err := url.Parse(targetHost)
if err != nil {
return nil, err
}
return httputil.NewSingleHostReverseProxy(url), nil
}
// ProxyRequestHandler handles the http request using proxy
// ProxyRequestHandler 使用 proxy 处理请求
func ProxyRequestHandler(proxy *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
proxy.ServeHTTP(w, r)
}
}
func main() {
// initialize a reverse proxy and pass the actual backend server url here
// 初始化反向代理并传入真正后端服务的地址
proxy, err := NewProxy("http://my-api-server.com")
if err != nil {
panic(err)
}
// handle all requests to your server using the proxy
// 使用 proxy 处理所有请求到你的服务
http.HandleFunc("/", ProxyRequestHandler(proxy))
log.Fatal(http.ListenAndServe(":8080", nil))
}
这么简单,这不就完成了吗?
不,事情没有那么简单,如果仅代理接口,那么一般都不会有什么问题,但要代理站点,就有一大堆的问题要处理。
-
HTTP 状态码 301
301 是永久重定向,如果浏览器收到 301,那么下次再请求这个地址,就不会再次发送请求,这对于一个代理来说,这并不好。
所以我们需要将 301 改成 302 临时重定向。
-
HTTP header Location
Location 表示重定向的地址,可以是绝对路径,可以是相对路径。如果我们代理了某个站点,就要重写 Location
例如代理了 https://github.com
如果某个请求返回了 Location: https://github.com
那么我们就需要对其进行修改
- Location: https://github.com + Location: <我的代理地址>
-
文件内容替换
有些站点的资源/链接等直接使用一个完整的 URL 而不是相对路径,例如
<a href="https://github.com">Click</a>
那么我们要对其替换
- <a href="https://github.com">Click</a> + <a href="<我的代理地址>">Click</a>
这不仅仅是要替换 HTML,还有 CSS/Javascript/XML/JSON 等文件
-
处理 Content-Encoding
在替换文件之前,首先是解压,我们接收到的响应,都是经过压缩之后的,那么就无法替换。
大多数的站点,都会经过压缩后返回。根据规范,服务器可能会采用以下几种压缩格式:
- gzip: 大多数服务器采用的压缩格式
- compress: 已弃用
- deflate
- identity: 无压缩
- br
这几类压缩算法都是公开的,并且社区已有现成的库。
只需要 解压 -> 替换 -> 压缩 -> 返回响应
甚至可以省略压缩这一步以提高性能。
-
处理 Cookies
有些 Cookie 指定了域名,被代理之后域名就不正确,所以我们需要对其进行重写。
- Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Domain=github.com + Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Domain=<我的代理地址>
有些 Cookie 指定了 Secure,必须要在 HTTPS 下才可用,但代理服务器如果是 HTTP 的话,也要替换
- Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure=true + Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;
-
处理 HTML 的 Content-Security-Policy
CSP 是一个安全策略,简单来说,只允许站点信任域名的资源。
代理之后域名发送变化,如果不替换就无法使用
- <meta http-equiv="Content-Security-Policy" content="default-src 'self' https://github.com"> + <meta http-equiv="Content-Security-Policy" content="default-src 'self' <我的代理地址>">
以及 HTTP 返回的头部信息
- Content-Security-Policy: default-src 'self' *.mailsite.com; img-src * + Content-Security-Policy: default-src 'self' <我的代理地址>; img-src *
-
处理 HTML 的 integrity
integrity 属性用于校验资源的完整性,浏览器在下载资源后再进行 HASH 校验,如果校验不通过,则说明内容已被篡改,浏览器就不会加载。
<link crossorigin="anonymous" media="all" integrity="sha512-MCJFYfbQoT4EXC6aWx5Wghs8FC/jslHEeN2iWXphliccmede2dQlhIBTAUCBq9Yu5poltu4askungzvyCsycGg==" rel="stylesheet" href="https://github.githubassets.com/assets/tab-size-fix-30224561f6d0a13e045c2e9a5b1e5682.css" />
这里偷个懒,直接把 integrity 属性移除
- <link crossorigin="anonymous" media="all" integrity="sha512-MCJFYfbQoT4EXC6aWx5Wghs8FC/jslHEeN2iWXphliccmede2dQlhIBTAUCBq9Yu5poltu4askungzvyCsycGg==" rel="stylesheet" href="https://github.githubassets.com/assets/tab-size-fix-30224561f6d0a13e045c2e9a5b1e5682.css" /> + <link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/tab-size-fix-30224561f6d0a13e045c2e9a5b1e5682.css" />
-
处理页面中的其他链接
在很多网站中,他们引入自家的 CDN,比如百度搜索出来的图片
https://t8.baidu.com/it/u=2652343384,1723246354&fm=218&app=126&f=JPEG?w=121&h=75&s=182A5D32DCBB7D8A06F8DCC6030070A2
显然替换成
http://t8.<我的代理地址>/xxxxx
就不对,并且这类请求依赖与 Cookie,否则请求失败。要处理这类,我们也要对其进行代理
代理之后的地址变成
http://<我的代理地址>/?forward_url=<原地址>
处理前:
处理后:
最困难的部分
以上几个处理,大多都是替换 URL,无非就是把目标服务器地址,替换成代理服务器地址,例如代理 Github
github.com -> localhost
这里我想到几种解决方案:
-
能否直接替换字符串,简单粗暴?
答: 不行
否则就会出现这样的情况
api.github.com
->api.localhost
-
能否使用正则表达式替换?
答: 不行
例如
/http?s:/\/\/google\.com/
照样可以匹配https://google.com.hk
最后变成
http://localhost.hk
-
把内容解析成 AST 再替换节点内容
这属于高级一点的玩法,替换是最准确的,但同时也是最费时费力,消耗性能的。
而且一点有语法错误,无法解析的情况,就很难处理(比如总有些站点,写的 HTML 都不符合规范,甚至闭合标签都没有)
-
最终方案
最终方案就是提取文本中的 URL,然后比对域名,域名匹配的才替换。
所以这又回到大难题: 如果从一堆文本中提取 URL?
我写了一大串的正则表达式去匹配,但你永远想不到,有些网站的 URL 是长什么样子
比如有这样的
https://avatars.githubusercontent.com/u/9758711?s=40&v=4
,有一个特殊字符;
你很难判定,这是不是一个完整的 URL
最终也只是做大匹配绝大多数的地址
项目地址
最后到这里已经讲完大部分的细节。
经过我的测试,反向代理 Google/Facebook/Github/百度 等几个主流网站都没有问题。更不用说自己开发的站点。
希望能帮助到大家,有 BUG 欢迎反馈,顺便给个小 ✨✨。