Adalab的入学挑战需要申请人在GitHub(以下简称GH)上完成一个项目。众所周知,国内不少地方访问GH并不顺畅。偏偏Adalab的申请人中又有不少是完完全全零基础,从来没有接触过科学上网之类的概念。参与挑战的过程中,他们常常因为网络问题不能完成项目。
我们看在眼里,急在心里——明明对编程兴趣浓厚,思维也很活跃,却被一道长城挡在门外,实在有些遗憾。
因此,我们希望搭建一个GitHub反向代理,供Adalab的申请人使用。
我们的目标是:
除了域名(下文以gh.proxy为例)不一样之外,这个网站应该是对用户完全透明的,就跟直接使用GitHub一模一样。
由于国内cnpmjs(相关DNS解析已停止)和fastgit等代理只能命令行操作,不支持浏览器使用,所以我们只能自己动手。
一、软件选择
过往大家做反代很喜欢用nginx,包括fastgit开源的配置也是nginx的。不过我这次选择了Caddy——原因很简单,Caddy有全自动的HTTPS支持。
Caddy 2 – The Ultimate Server with Automatic HTTPScaddyserver.com/
GH从国内访问网速不理想,肯定是有原因的。如果通过HTTP来代理,GH上的内容会直接明文暴露在防火墙之下,就有可能被探测和封禁。尽管我们只需要短短一个月左右的代理,但考虑到潜在的可能性,通过HTTPS进行加密仍然是首要选择。所以,使用Caddy就顺理成章了。
二、支持克隆
参考Caddy官方文档,很快写出了第一版配置:
gh.proxy {
reverse_proxy https://github.com {
header_up Host {upstream_hostport}
header_up X-Forwarded-Host {host}
}
}
访问https://gh.proxy,已经能用了!
再随便找个仓库试着克隆了一下,就Hello World吧!git clone https://gh.proxy/octocat/Hello-World.git
,也没问题。竟然这么简单吗?!
再来个私有的仓库,git clone https://gh.proxy/account/reponame
。嗯?有错误?
remote: Support for password authentication was removed on August 13, 2021. Please use a personal access token instead.
remote: Please see https://github.blog/2020-12-15-token-authentication-requirements-for-git-operations/ for more information.
哦!想起来了,GHGH的安全政策更新之后,命令行不能再用密码了,必须使用token。
创建个token去!打开https://gh.proxy,熟悉的GitHub页面浮现了出来,输入用户名密码……嗯?
打开开发者工具,发现有一条422:
用正常的URL尝试登录GH,又一切正常。这是怎么回事呢?
三、支持POST
考虑到普通的访问没问题,登录失败,初步猜测是GitHub对HTTP POST请求做了额外的验证。如果发现不匹配,就报422错误。
可能性最大的,自然就是请求头。
究竟是哪条请求头呢?通过对比分析浏览器通过官方URL以及反代URL访问GH时不同的HTTP请求,很快发现,只有三个请求头包含了反代信息:
authority
origin
referer
会不会是Caddy转发请求到GH的时候,从这些请求头里暴露了反代的信息,导致GH拒绝正常响应呢?于是想到把Caddy的日志等级调高,看看Caddy究竟是是怎样访问GH的。修改配置如下:
{
debug
}
gh.proxy {
reverse_proxy https://github.com {
header_up Host {upstream_hostport}
header_up X-Forwarded-Host {host}
}
}
注意,这里光调log是不够的,因为在Caddy中这只包含access log。必须开启全局debug选项,才能看到反向代理模块的日志。很快就能发现,Caddy果然保留了这三个请求头。
查阅资料得知,referer
嫌疑最小,常常用来表示请求从哪个网页发起;authority
是一个在HTTP/2才有的请求头,考虑到web的兼容性,猜测GH不会做强制要求。所以最有可能的还是origin
。
所以继续修改Caddy配置文件,覆盖掉origin
:
{
debug
}
gh.proxy {
reverse_proxy https://github.com {
header_up Host {upstream_hostport}
header_up X-Forwarded-Host {host}
header_up origin https://github.com
}
}
再次尝试登录:
状态码302!哈哈!问题解……嗯?怎么又回来了:
我这次故意截了一张包含地址栏的大图——是的,细心的你可能已经想到了,302是重定向,结果给重定向到GH官网去了。如果你重新访问https://gh.proxy的话,会发现已经登录成功了。所以,问题变成了解决重定向。
四、支持登录
重定向的地址记录在HTTP响应头里的Location字段,我们只需要检测对应的字段,然后重写它就行了。思路非常简单,不过具体做的时候发现Caddy这部分文档不是非常完善,还是稍微花了点时间的。改好之后配置文件长这个样子:
{
debug
}
gh.proxy {
reverse_proxy https://github.com {
header_up Host {upstream_hostport}
header_up X-Forwarded-Host {host}
header_up origin https://github.com
header_down location (https://github.com/)(.*) https://gh.proxy/$2
}
}
试验一下, POST请求没问题,登录功能也可以正常使用了。
五、从注册到放弃
本以为已经全部搞完了,然而当模拟新用户注册的时候,又发现了问题——验证码出不来。
再次祭出开发者工具,发现控制台有错误:
嗯……captcha,从网址来看应该就是跟验证码有关的。错误信息提到了“Content Security Policy”、http://github.com,心中就猜了个八九不离十——估计是新的浏览器安全策略,想从http://octocaptcha.com获取验证码的话,请求必须是从指定的域名(即http://github.com、*.http://github.com、*.http://githubapp.com)发起,而不能gh.proxy之类的其他域名。查了一下文献,果不其然。
由于这条指令(directive)是记录在http://octocaptch.com的响应头里面,Caddy完全无法修改,这就意味着假如不能获取对用户环境的进一步控制的话,反代的策略已经走不通了。
六、结论
所以,在浏览器愈发注重安全的今天,国内用户想要靠反向代理来实现对GH近乎透明的访问是不可能的。
联想到最近GH等一众互联网公司封禁俄罗斯,内心不禁涌起一股悲凉:Web的初衷本是一个美好的乌托邦,现实却让我们筑起了一道道围墙;任凭你百般Hacking,到最后仍然只有the road to surfdom