yii2内网项目,使用frp进行内网穿透,使用 https2http插件把内网服务器http流量转成https,会存在一个问题:当使用 $this->redirect(...) 或 $this->goHome() (其实用的也是前者)等重定向时,网址会变成 127.0.0.1,并且协议也从https变成了 http,结果自然是显示找不到
frp client配置类似官网例子:
[common]
server_addr = 47.xx.xxx.xx
server_port = 7900
[web_https]
type = https
custom_domains = lab.xxxxx.com
plugin = https2http
plugin_local_addr = 127.0.0.1:80
plugin_crt_path = ./xxxxx.com.pem
plugin_key_path = ./xxxxx.com.key
plugin_host_header_rewrite = 127.0.0.1
plugin_header_X-From-Where = frp
比较通过域名 https://lab.xxxxx.com:8900 访问和通过IP地址访问时 $_SERVER 超级全局变量的结果差异,主要有以下差异:
$_SERVER = Array
(
[HTTP_X_FROM_WHERE] => frp
[HTTP_X_FORWARDED_FOR] => 47.xx.xxx.xx
[HTTP_SEC_FETCH_USER] => ?1
[HTTP_SEC_FETCH_SITE] => same-origin
[HTTP_SEC_FETCH_MODE] => navigate
[HTTP_SEC_FETCH_DEST] => document
[HTTP_SEC_CH_UA_PLATFORM] => "Linux"
[HTTP_SEC_CH_UA_MOBILE] => ?0
[HTTP_SEC_CH_UA] => "Chromium";v="110", "Not A(Brand";v="24", "Google Chrome";v="110"
[HTTP_REFERER] => https://lab.xxxxx.com:8900/
[HTTP_HOST] => 127.0.0.1
[SERVER_ADDR] => 127.0.0.1
[REMOTE_ADDR] => 127.0.0.1
)
$_SERVER = Array
(
[HTTP_CONNECTION] => keep-alive
[HTTP_HOST] => 10.5.10.200
[SERVER_ADDR] => 10.5.10.200
[REMOTE_ADDR] => 10.5.21.241
)
前一个是frp代理后的结果,后一个是内网直接访问的结果,我们用下图来表示这个过程
用frp内网穿透,是某种形式的反向代理。frp server只起到转发作用,它的配置非常简单,也就指定一下“主控”端口和https监听端口。使用https2http插件时,证书是配置在frp client的(即需要把通常放在公网frp server服务器的证书放一份在frp client,这里也说明frp只适合绝对信得过的client使用该插件)。frp client 上的证书,既用于frp server转发过来的流量的解密(变成http流量转发给nginx),也用于nginx响应的流量的加密(变成https流量转发给frp server,并最终响应给用户)。
我们可以发现,对于nginx来说,它的“客户”是frp client,从IP地址来说,这里的frp client “客户”地址(REMOTE_ADDR)和这里的 server nginx地址(SERVER_ADDR)都是127.0.0.1,相当于“客户”frp client,主机Host也是 127.0.0.1。不过头部中的主机信息 HTTP_HOST 是可以被重写的,对于 http 代理是配置 host_header_rewrite,而对于我们的 https2http插件,需要在 frp client配置 plugin_host_header_rewrite来重写主机Host信息。(frp client中配置 plugin_header_X-xxxx-xxx形式的头部信息,对应了 HTTP_X_xxxx_xxx 形式的头部,只是在我们的情形中,配置 HTTP_HOST、HTTP_X_FORWARDED_HOST、HTTP_PROTO、HTTP_X_FORWARDED_PORT等都不能解决问题)
了解了frp代理的基本原理,我们需要来了解yii2中redirect是怎么进行的。
yii2 redirect 所需的URL信息可以分为两个部分:第一部分是 协议、主机、端口,这部分根据yii\web\Request::getHostInfo() 函数确定;第二部分是相对于文档根的路径,由 Url::to($url)计算得到。我们关心前面的部分,代码如下
public function getHostInfo()
{
if ($this->_hostInfo === null) {
$secure = $this->getIsSecureConnection();
$http = $secure ? 'https' : 'http';
if ($this->getSecureForwardedHeaderTrustedPart('host') !== null) {
$this->_hostInfo = $http . '://' . $this->getSecureForwardedHeaderTrustedPart('host');
} elseif ($this->headers->has('X-Forwarded-Host')) {
$this->_hostInfo = $http . '://' . trim(explode(',', $this->headers->get('X-Forwarded-Host'))[0]);
} elseif ($this->headers->has('X-Original-Host')) {
$this->_hostInfo = $http . '://' . trim(explode(',', $this->headers->get('X-Original-Host'))[0]);
} elseif ($this->headers->has('Host')) {
$this->_hostInfo = $http . '://' . $this->headers->get('Host');
} elseif (isset($_SERVER['SERVER_NAME'])) {
$this->_hostInfo = $http . '://' . $_SERVER['SERVER_NAME'];
$port = $secure ? $this->getSecurePort() : $this->getPort();
if (($port !== 80 && !$secure) || ($port !== 443 && $secure)) {
$this->_hostInfo .= ':' . $port;
}
}
}
return $this->_hostInfo;
}
代码的基本执行路径如下:
解决方案一:
frp client配置中,指定
plugin_host_header_rewrite = lab.xxxxx.com:8900
这样,frp client 转发到 nginx 时,头部信息 HTTP_HOST 会被改写成设定的值,从而 getHostInfo 执行时会走路径4 (读取头部 Host信息的紫色路径),但仅仅这样是不行的,因为我们需要协议是 https,而 getIsSecureConnection 根据超级全局变量 $_SERVER['HTTPS'] 或者头部信息Forwarded中的proto部分来决定是https还是http(转发时不存在https到http切换会比较简单,因为不需要考虑这一步)。我们一种变通方法是在 yii2 入口文件中,直接指定 $_SERVER['HTTPS']=1(当然,为了兼容内网ip地址访问,可以判断 $_SERVER['SERVER_ADDR']和$_SERVER['REMOTE_ADDR']是否同时为127.0.0.1,因为我们的自我转发中,这是特点)
if ($_SERVER['SERVER_ADDR'] === '127.0.0.1' && $_SERVER['REMOTE_ADDR'] === '127.0.0.1') { // 是内网穿透自身代理而来的请求
$_SERVER['HTTPS'] = '1'; //
}
$application->run();
解决方案二:
frp client配置中,不指定 plugin_host_header_rewrite,但额外指定
plugin_header_Forwarded = for=47.xx.xxx.xx;host=lab.xxxxx.com:8900;proto=https
然后在 yii2 的配置中,对组件 request 添加配置(下面 baseUrl后面部分)
'baseUrl' => '/admin', //-----------后端基础目录
'trustedHosts' => [ // https://www.yiiframework.com/doc/api/2.0/yii-web-request#$secureHeaders-detail
'127.0.0.1/24', // 从代理转发的,这里设置白名单,这样 $secureHeaders 中的 X-Forwarded-Host 等头部才被允许
],
'secureHeaders' => [ // frp配置: plugin_header_Forwarded =
'Forwarded', // 我们添加的 集合了各项 forwarded, 参考 yii\web\Request::getSecureForwardedHeaderParts()
'X-Forwarded-For',
'X-Forwarded-Host',
'X-Forwarded-Proto',
'X-Forwarded-Port',
'Front-End-Https',
'X-Rewrite-Url',
'X-Original-Host',
],
这样配置后,frp client 转发时会带上头部 Forwarded,并且这个Forwarded头部将被 yii2 允许(转发主机白名单,可接受头部等)。从而,代码执行流程会按前面图中红色路径走。
路径2、3、5也可以考虑,不过和第一个方案一样,需要指定 $_SERVER['HTTPS'] = 1来指定协议,而且端口不一定好处理。