Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
axetroy opened this issue Jan 1, 2022 · 0 comments
Open

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

axetroy opened this issue Jan 1, 2022 · 0 comments

Comments

@axetroy
Copy link
Owner

axetroy commented Jan 1, 2022

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

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

如何反向代理一个站点?

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

  • 正向代理: (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

@axetroy axetroy changed the title 如果制作一个反向代理服务器以及处理细节 如何制作一个反向代理服务器以及处理细节 Jan 1, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant