axetroy/blog

如何制作一个反向代理服务器以及处理细节

Opened this issue · 0 comments

新的一年,新的气象,好久没有更新过博客了。

在这里祝大家新年快乐,大吉大利。

如何反向代理一个站点?

代理,大家都不陌生,大体上分为正向代理和反向代理。常见的有

  • 正向代理: (HTTP/Socket5/v2ray/shadowSocket)
  • 反向代理: (Nginx/Apache)

本文就讲解,如何制作一个反向代理工具,并且代理任意站点。

市面上已经有那么多代理工具,为什么还要折腾一个?

起因

在开发时,我们可能会有这样的一些需求:

  1. 如何调试/在线修改站点?(非 SPA)

像 Chrome 浏览器的开发者工具里面就有 override 功能,可以修改服务器返回的资源(HTML/CSS/JS),并映射到本地目录。

但它有一个缺陷:如果修改的站点带有端口,就无法映射到本地,因为 : 是一个非法字符,无法在文件系统中创建,或者你可以试试创建一个目录 localhost:8080

ref: https://stackoverflow.com/questions/70337046/chrome-local-overrides-with-port-number

  1. 调试 微信 等一些对 HTTPS 有要求的平台?

对于一些平台的上线,有要求使用 HTTPS,但你的站点又没有,在开发阶段,可以暂时通过代理的方式解决。

  1. 如何调试微信这类的 Web?

不止微信,还有支付宝等内嵌的 H5 页面如何调试?简单的修改和打印信息,根本就不需要发布,通过代理即可解决。

  1. 如何向同事或其他人展示不存在的页面?

比如我要给同事展示某网站,需要科学上网,但是他/她却没有,这时候就可以在本机通过代理的方式呈现出来。

就出于以上几点,我觉得,撸一个反向代理工具。

技术选型

采用 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))
}

这么简单,这不就完成了吗?

不,事情没有那么简单,如果仅代理接口,那么一般都不会有什么问题,但要代理站点,就有一大堆的问题要处理。

  1. HTTP 状态码 301

    301 是永久重定向,如果浏览器收到 301,那么下次再请求这个地址,就不会再次发送请求,这对于一个代理来说,这并不好。

    所以我们需要将 301 改成 302 临时重定向。

  2. HTTP header Location

    Location 表示重定向的地址,可以是绝对路径,可以是相对路径。如果我们代理了某个站点,就要重写 Location

    例如代理了 https://github.com

    如果某个请求返回了 Location: https://github.com

    那么我们就需要对其进行修改

    - Location: https://github.com
    + Location: <我的代理地址>
  3. 文件内容替换

    有些站点的资源/链接等直接使用一个完整的 URL 而不是相对路径,例如

    <a href="https://github.com">Click</a>

    那么我们要对其替换

    - <a href="https://github.com">Click</a>
    + <a href="<我的代理地址>">Click</a>

    这不仅仅是要替换 HTML,还有 CSS/Javascript/XML/JSON 等文件

  4. 处理 Content-Encoding

    在替换文件之前,首先是解压,我们接收到的响应,都是经过压缩之后的,那么就无法替换。

    大多数的站点,都会经过压缩后返回。根据规范,服务器可能会采用以下几种压缩格式:

    • gzip: 大多数服务器采用的压缩格式
    • compress: 已弃用
    • deflate
    • identity: 无压缩
    • br

    这几类压缩算法都是公开的,并且社区已有现成的库。

    只需要 解压 -> 替换 -> 压缩 -> 返回响应

    甚至可以省略压缩这一步以提高性能。

  5. 处理 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;
  6. 处理 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 *
  7. 处理 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" />
  8. 处理页面中的其他链接

    在很多网站中,他们引入自家的 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=<原地址>

    处理前:

    截屏2022-01-01 20 57 37

    处理后:

    截屏2022-01-01 20 58 13

最困难的部分

以上几个处理,大多都是替换 URL,无非就是把目标服务器地址,替换成代理服务器地址,例如代理 Github

github.com -> localhost

这里我想到几种解决方案:

  1. 能否直接替换字符串,简单粗暴?

    答: 不行

    否则就会出现这样的情况 api.github.com -> api.localhost

  2. 能否使用正则表达式替换?

    答: 不行

    例如 /http?s:/\/\/google\.com/ 照样可以匹配 https://google.com.hk

    最后变成 http://localhost.hk

  3. 把内容解析成 AST 再替换节点内容

    这属于高级一点的玩法,替换是最准确的,但同时也是最费时费力,消耗性能的。

    而且一点有语法错误,无法解析的情况,就很难处理(比如总有些站点,写的 HTML 都不符合规范,甚至闭合标签都没有)

  4. 最终方案

    最终方案就是提取文本中的 URL,然后比对域名,域名匹配的才替换。

    所以这又回到大难题: 如果从一堆文本中提取 URL?

    我写了一大串的正则表达式去匹配,但你永远想不到,有些网站的 URL 是长什么样子

    比如有这样的 https://avatars.githubusercontent.com/u/9758711?s=40&amp;v=4,有一个特殊字符 ;

    你很难判定,这是不是一个完整的 URL

    最终也只是做大匹配绝大多数的地址

项目地址

最后到这里已经讲完大部分的细节。

经过我的测试,反向代理 Google/Facebook/Github/百度 等几个主流网站都没有问题。更不用说自己开发的站点。

希望能帮助到大家,有 BUG 欢迎反馈,顺便给个小 ✨✨。

https://github.com/axetroy/forward-cli