Updates: 写了个 axios interceptor axios-https-proxy 可以解决 HTTPS 请求 HTTP 代理的问题。

这两天在做 googleapis 的相关开发。

googleapis 是一个前后端同构库,请求部分基于 axios 实现。

因为众所周知的原因,我们的电脑要翻墙才能访问 www.googleapis.com 的服务器。

我这边是采用 5h4d0w50ck5 翻的墙,在命令行下大部分情况可以配置 HTTP_PROXYHTTPS_PROXY 环境变量实现通过 HTTP 协议代理访问墙外的内容。

axios 也同样支持了这种配置方式。

然而,axios 仅仅支持 HTTP 代理协议中直接代理这一种方式,不支持隧道代理的方式。

同时,axios 无视了环境变量中的协议头,默认代理服务器的协议与目标地址协议相同。

也就是说:如果我们请求 http 地址,axios 就使用 http 协议连接 HTTP_PROXY 中配置的服务器来访问目标地址;如果我们请求 https 地址,axios 就使用 https 协议连接 HTTPS_PROXY 中配置的服务器来访问目标地址。两种情况下都是采用直接代理方式与代理服务器沟通。

同样地,在 axios 中配置 proxy 参数也是无法配置代理服务器协议的,协议以请求的地址为准。

但是,我们 5h4d0w50ck5 提供的 HTTP 代理服务器仅支持 HTTP 协议,不支持 HTTPS 协议。

所以说,当我们透过 5h4d0w50ck5 提供的 HTTP 代理服务器访问 HTTPS 地址时,axios 会采用 HTTPS 协议访问 5h4d0w50ck5 提供的 HTTP 代理服务器,从而访问失败。

相关 issue:https://github.com/axios/axios/issues/925

为了解决这个问题,我首先尝试了一种不标准的方法:使用直接代理方式连接 HTTP 服务器访问 HTTPS 目标。这种方法非常不安全,因为将 HTTPS 传输的加密内容暴露在了 HTTP 的未加密环境下。从而使可信内容变得不可信。

我修改了 axios 源码中对于 isHttps 的判断逻辑,使用 HTTP 协议连接 HTTPS 代理。

结果是,拿不到响应内容,在终端下用 nc 模拟,仍然拿不到内容。

➜ nc -v 127.0.0.1 1087
found 0 associations
found 1 connections:
     1:	flags=82<CONNECTED,PREFERRED>
    outif lo0
    src 127.0.0.1 port 49499
    dst 127.0.0.1 port 1087
    rank info not available
    TCP aux info available

Connection to 127.0.0.1 port 1087 [tcp/cplscrambler-in] succeeded!
GET https://www.google.com/ HTTP/1.1

HTTP/1.1 200 Connection established

说明 5h4d0w50ck5 提供的 HTTP 代理服务器不支持这种不标准不安全的访问模式。

所以说,我们无法使用直接代理的方式达到目标,只能转而使用隧道代理的方式。

这个时候我们就要请出 node 界 HTTP 客户端老大哥 request,看看它是怎么实现隧道代理的。

于是我们就发现了它的一个支撑库 tunnel-agentrequest 是通过 tunnel-agent生成 HTTP Agent 的方式实现隧道代理的。

幸运的是,axios 也支持配置 HTTP Agent,它有两个配置参数 httpAgenthttpsAgent,分别配置不同协议下请求的 HTTP Agent。所以我们就可以把 tunnel-agent 借过来用在 axios 上了。

首先我们是这样配置的:

const axios = require('axios')
const { httpsOverHttp, httpOverHttp } = require('tunnel-agent')

const TUNNEL_OPTIONS = { proxy: { port: 1087 } }
axios.defaults.proxy = false // 强制禁用环境变量中的代理配置
axios.defaults.httpAgent = httpOverHttp(TUNNEL_OPTIONS)
axios.defaults.httpsAgent = httpsOverHttp(TUNNEL_OPTIONS)

然后我们发现请求报错

TypeError: Agent option must be an Agent-like object, undefined, or false.
    at new ClientRequest (_http_client.js:104:11)

查 node 源码之后我们发现,原因是我们传入的 agent 没有 addRequest 方法。

但是我们打印了 tunnelAgent.addRequest,它的确是存在的。

这里的问题出现在 axios 中的 utils.merge 函数:它不会自动合并默认参数中挂在原型上的成员。

相关 issue:https://github.com/axios/axios/issues/1077

tunnel-agent 源码后发现,我们的 tunnelAgent.addRequest 恰好是挂在 TunnelAgent 构造函数原型上的,所以没有被合并到真正的请求配置中来。

所以我们不能采用配置默认参数的方式配置我们的 Agent,转而使用 interceptors 的方式配置。

const axios = require('axios')
const { httpsOverHttp, httpOverHttp } = require('tunnel-agent')

const TUNNEL_OPTIONS = { proxy: { port: 1087 } }

axios.interceptors.request.use(function (config) {
  config.proxy = false // 强制禁用环境变量中的代理配置
  config.httpAgent = httpOverHttp(TUNNEL_OPTIONS)
  config.httpsAgent = httpsOverHttp(TUNNEL_OPTIONS)
  return config
})

这种方式使得 proxy 与 agent 配置参数无法被业务更改,但是的确是一种可行的方法。

把它存成了 5h4d0w50ck5.js 文件,当需要翻墙时 执行

$ node --require ./5h4d0w50ck5 app.js

即可使用 axios 访问墙外的内容。