搭建 Google JS 资源反向代理服务
查找出用到 google js 资源的地方
我们首先使用 ack 这个命令找出用到了 google js 的地方,
$ ack 'javascript_include_tag' --type-add haml=.haml --type=haml --type-add erb=.erb --type=erb
然后再通过一些人肉查找,最终得到了下面的一份列表:
https://ajax.googleapis.com => ajax_googleapis
http://code.google.com => code_google
http://google-code-prettify.googlecode.com => gcp_google
http://maps.googleapis.com => maps_apis_google
http://www.google-analytics.com => www_ana_g
https://ssl.google-analytics.com => ssl_ana_g
https://www.google.com => www_g
https://encrypted.google.com => encry_g
http://maps.google.com/ => maps_g
http://maps.gstatic.com => maps_st_g
https://maps-api-ssl.google.com => maps_api_ssl_g
https://gg.google.com => gg_g
http://maps.gstatic.com => maps_st_g
http://mt0.googleapis.com => mt0_apis_g
http://mt1.googleapis.com => mt1_apis_g
https://mts0.google.com => ssl_mts0_g
https://mts1.google.com => ssl_mts1_g
http://khm0.googleapis.com => khm0_apis_g
http://khm1.googleapis.com => khm1_apis_g
http://cbk0.googleapis.com => cbk0_apis_g
http://cbk1.googleapis.com => cbk1_apis_g
http://khms0.google.com => ssl_khms0_g
http://csi.gstatic.com => csi_st_g
http://maps.googleapis.com => maps_apis_google
http://gg.google.com => gg_g
http://khm.googleapis.com => khm_g_apis
http://earthbuilder.googleapis.com => earthbuilder_g_apis
http://g0.gstatic.com => g0_st_g
http://static.panoramio.com.storage.googleapis.com => st_pa_st_g_apis
http://geo0.ggpht.com => geo0_gg
http://geo1.ggpht.com => geo1_gg
http://geo2.ggpht.com => geo2_gg
上面这个列表看起来特别像一个 Hash, key 表示需要进行反代的 host, value 表示反代对象在 我们服务器上的访问路径。在前期把反代的 host 和访问路径做这样一个映射能够方便我们做后期的服务器 配置比如 nginx 的配置。
我们看一个具体的 js: http://www.google.com/jsapi, 这个 js 里面又包含了许多其他的 google host, 比如: http://www.google.com/uds, http://ajax.googleapis.com/ajax 等,
因此我们对 http://www.google.com/jsapi 的反代工作包括三个部分:
-
首先将 www.google.com 反代成我们服务器的一个访问路径: www_g
-
然后我们需要将反代后的内容里面的一些 google host 替换成我们服务器的访问路径, 比如:
www.google.com 替换成 www_g, ajax.googleapis.com 替换成 ajax_googleapis
- 最后因为我们的服务使用 https, 所以我们也需要将反代内容里面的 http 替换成 https
配置 Nginx
在服务器上运行 ps aux | grep nginx
, 此命令可以帮助我们找到 nginx.conf 文件路径。
我们的 nginx.conf 的路径为 /opt/nginx/conf/nginx.conf。
因为我们要反代的资源很多,所以我们建立一个单独的文件: proxy.mysite.com 来存放反代的配置代码。
在实际操作中,请使用你自己的站点名代替 proxy.mysite.com, 在后面的叙述中,我不会再提及这点了。
nginx.conf,
http {
+ proxy_temp_path /home/nginx_cache/temp;
+ proxy_cache_path /home/nginx_cache/cache levels=1:2 keys_zone=cache_one:4000m inactive=2d max_size=10g;
+ include /opt/nginx/conf/sites-enabled/proxy.mysite.com;
}
proxy_temp_path
和 proxy_cache_path
是用来缓存反代资源的,这样可以提高我们反代资源的访问速度。我们要注意下 keys_zone=cache_one
, 这个在后面的配置中会使用到。
现在我们开始编写 proxy.mysite.com 的配置代码。
配置 proxy.mysite.com
/opt/nginx/conf/sites-enabled/proxy.mysite.com,
+ upstream www_g {
+ server www.google.com:80;
+ }
+ location /www_g {
+ rewrite /www_g/(.*) /$1 break;
+ subs_filter 'http:' 'https:';
+ subs_filter 'www.google.com' 'proxy.mysite.com/www_g';
+ subs_filter 'ajax.googleapis.com' 'proxy.mysite.com/ajax_googleapis';
+ subs_filter 'books.google.com' 'proxy.mysite.com/books_g';
+ subs_filter 'encrypted.google.com' 'proxy.mysite.com/encry_g';
+ subs_filter 'maps-api-ssl.google.com' 'proxy.mysite.com/maps_api_ssl_g';
+ subs_filter 'maps.google.com' 'proxy.mysite.com/maps_g';
+ subs_filter 'gg.google.com' 'proxy.mysite.com/gg_g';
+ subs_filter_types *;
+ proxy_pass_header Server;
+ proxy_cache cache_one;
+ proxy_cache_valid 200 1d;
+ proxy_cache_use_stale error timeout invalid_header updating
http_500 http_502 http_503 http_504;
+ proxy_set_header Host www.google.com;
+ proxy_set_header Accept-Encoding '';
+ proxy_redirect off;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Scheme $scheme;
+ proxy_pass http://www_g;
}
rewrite /www_g/(.*) /$1 break;
的作用是当我们访问 https://proxy.mysite.com/www_g/jsapi 时,相当于访问 http://www.google.com/jsapi
subs_filter
的作用就是将反代资源里的内容替换为我们需要的内容,比如:
subs_filter 'http:' 'https:'
会将 http 替换为 https;
subs_filter 'www.google.com' 'proxy.mysite.com/www_g'
会将 www.google.com
替换为 proxy.mysite.com/www_g;
proxy_cache cache_one
这个地方用到的 cache_one 就是在前面
proxy_cache_path /home/nginx_cache/cache levels=1:2 keys_zone=cache_one:4000m inactive=2d max_size=10g;
声明时用到的 cache_one;
proxy_pass http://www_g;
表示 https://proxy.mysite.com/www_g 反代的是 http://www.google.com;
下面介绍怎么安装 ngx_http_sub_module
安装 ngx_http_sub_module
因为服务器上已经安装了 nginx, 所以我们需要在此基础上安装 ngx_http_sub_module, 而不是重新 安装 nginx, 再安装 ngx_http_sub_module。
安装步骤:
- 获取已安装的 nginx 的版本和 configure 参数, 得到 nginx 的版本是 1.4.1
$ /opt/nginx/sbin/nginx -V
- 下载 nginx-1.4.1 的源代码
$ wget http://nginx.org/download/nginx-1.4.1.tar.gz
有第 1 步 我们可以得到 nginx 的 configure 参数
--prefix=/opt/nginx --with-http_ssl_module --with-http_gzip_static_module \
--with-http_stub_status_module --with-cc-opt=-Wno-error \
--add-module=/usr/local/lib/ruby/gems/1.9.1/gems/passenger-4.0.5/ext/nginx \
--with-http_sub_module
- 下载 ngx_http_sub_module
$ git clone git://github.com/yaoweibin/ngx_http_substitutions_filter_module.git
- 将 ngx_http_sub_module 加到 nginx 的 configure 参数中,然后 configure, make
cd nginx-1.4.1
./configure --prefix=/opt/nginx --with-http_ssl_module --with-http_gzip_static_module \
--with-http_stub_status_module --with-cc-opt=-Wno-error \
--add-module=/usr/local/lib/ruby/gems/1.9.1/gems/passenger-4.0.5/ext/nginx \
--with-http_sub_module \
--add-module=/home/gjiang/ngx_http_substitutions_filter_module
make
注意我们将 –add-module=/home/gjiang/ngx_http_substitutions_filter_module 加入到了 configure 参数中。
- 将新生成的 nginx 命令拷贝到现安装的 nginx 命令目录中
$ sudo /opt/nginx/sbin/nginx -s stop
$ sudo cp objs/nginx /opt/nginx/sbin/nginx
$ sudo /opt/nginx/sbin/nginx
这样 ngx_http_sub_module 就安装好了。
配置 Valid Referers
配置 Valid Referers 是为了阻止其他站点引用我们的反代资源,这样只有我们自己的站点才能引用 proxy.mysite.com 的反代资源。
/opt/nginx/conf/sites-enabled/proxy.mysite.com,
server {
+ valid_referers server_name *.mysite.com *.mysite1.com;;
}
配置 SSL 服务
安装 SSL 证书
首先需要购买一个靠谱的证书, 这个不赘述了。当我们买好证书后,需要安装证书,安装证书需要下面的几个步骤:
- 生成 CSR 文件
$ openssl req -nodes -newkey rsa:2048 -keyout proxy.mysite.com.key -out proxy.mysite.com.csr
在这个过程中,命令行会提示我们填写对应的 csr 信息:
Country Name (2 letter code) [AU]: XX
State or Province Name (full name) [Some-State]: XX
Locality Name (eg, city) []: XX
Organization Name (eg, company) [Internet Widgits Pty Ltd]: Mysite Inc.
Organizational Unit Name (eg, section) []: IT
Common Name (eg, YOUR name) []: proxy.mysite.com
Email Address []: contactus@mysite.com
上面的命令会生成 proxy.mysite.com.key 和与之对应的 proxy.mysite.com.csr
- 将 proxy.mysite.com.csr 提供给你购买证书的网站,然后你就可以从网站下载 ssl 证书了
配置 Nginx SSL
/opt/nginx/conf/sites-enabled/proxy.mysite.com,
server {
+ listen 443 ssl;
+ server_name proxy.mysite.com;
+ ssl on;
+ ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;
+ ssl_prefer_server_ciphers on;
+ ssl_ciphers xxxxx:!aNULL:!MD5:!DSS;
+ ssl_certificate_key /etc/ssl/certs/proxy.mysite.key;
+ ssl_certificate /etc/ssl/certs/proxy.mysite.crt;
+ ssl_session_cache shared:SSL:10m;
+ ssl_session_timeout 10m;
}
ssl_certificate_key /etc/ssl/certs/proxy.mysite.key;
用到的 proxy.mysite.key 即我们在前面生成 csr 文件时生成的 key。
ssl_certificate /etc/ssl/certs/proxy.mysite.crt;
用到的 proxy.mysite.crt 即我们从购买证书的站点下载的证书。
判断中国大陆 IP
其实这一处的业务逻辑不单单是判断请求的 IP 是否来自中国大陆这么简单。首先判断请求是否来自中国大陆,如果来自大陆,则将 google 的资源用我们的反代地址替换,如果请求不是来自大陆,则还是使用 google 的资源,这么一说好像还是挺简单的。
为了判断 IP 来源我们使用 maxminddb 这个 gem, 而这 gem 又使用了 GeoIP2 MaxMind DB 作为其 IP 数据库。我们需要从 http://dev.maxmind.com/geoip/geoip2/downloadable/ 下载一个数据库放到项目里, 我下载的是 GeoLite2-Country.mmdb 数据库,将其存放在 db/GeoLite2-Country.mmdb。 现在直接上代码:
app/models/geo_country_ip.rb,
class GeoCountryIp
DB = MaxMindDB.new(Rails.root.join('db', 'GeoLite2-Country.mmdb'))
attr_reader :ip
def initialize(ip)
@ip = ip
@ret = DB.lookup(ip)
end
def found?
@ret.found?
end
def country_name
@ret.country.name
end
def country_iso
@ret.country.iso_code
end
def from_chinese_mainland?
country_iso == 'CN' || country_name == 'China'
end
end
geo_ip = GeoCountryIp.new('54.64.229.171')
geo_ip.country_iso #=> 'JP'
geo_ip.from_chinese_mainland? #=> false
将反代资源抽象为一个模型: ProxySource。
app/modles/proxy_source.rb,
class ProxySource
PATH_HOST_MAPPER = {
ssl_ga_g: 'ssl.google-analytics.com',
ajax_googleapis: 'ajax.googleapis.com',
code_google: 'code.google.com',
gcp_google: 'google-code-prettify.googlecode.com',
maps_apis_google: 'maps.googleapis.com',
www_ana_g: 'www.google-analytics.com',
ssl_ana_g: 'ssl.google-analytics.com',
www_g: 'www.google.com',
encry_g: 'encrypted.google.com',
books_g: 'books.google.com',
maps_g: 'maps.google.com',
maps_st_g: 'maps.gstatic.com',
mt0_apis_g: 'mt0.googleapis.com',
mt1_apis_g: 'mt1.googleapis.com',
ssl_mts0_g: 'mts0.google.com',
ssl_mts1_g: 'mts1.google.com',
khm0_apis_g: 'khm0.googleapis.com',
khm1_apis_g: 'khm1.googleapis.com',
cbk0_apis_g: 'cbk0.googleapis.com',
cbk1_apis_g: 'cbk1.googleapis.com',
ssl_khms0_g: 'khms0.google.com',
csi_st_g: 'csi.gstatic.com',
gg_g: 'gg.google.com',
khm_g_apis: 'khm.googleapis.com',
earthbuilder_g_apis: 'earthbuilder.googleapis.com',
g0_st_g: 'g0.gstatic.com',
st_pa_st_g_apis: 'static.panoramio.com.storage.googleapis.com',
geo0_gg: 'geo0.ggpht.com',
geo1_gg: 'geo1.ggpht.com',
geo2_gg: 'geo2.ggpht.com'
}
DEFAULT_PROXY_HOST = 'proxy.mysite.com'
FORBIDDEN_PROXY_PATHS = [:ssl_ga_g, :maps_apis_google]
attr_reader :target_url, :proxy_host
def initialize(target_url, opts = {})
@target_url = target_url
@proxy_host = opts[:proxy_host] || DEFAULT_PROXY_HOST
end
def url
@url ||= \
begin
path, target_host = PATH_HOST_MAPPER.detect{|k, v| target_url.include?(v) }
if path
proxy_str = [proxy_host, '/', path].join
target_url.sub(target_host, proxy_str)
else
target_url
end
end
end
def is_forbidden_to_proxy?
path, _ = PATH_HOST_MAPPER.detect {|k, v| target_url.include?(v) }
FORBIDDEN_PROXY_PATHS.include?(path)
end
end
在 application_controller.rb 里实现判断 ip 来源的方法:
app/controllers/application_controller.rb,
class ApplicationController < ActionController::Base
+ helper_method :ip_from_chinese_mainland?
+ def ip_from_chinese_mainland?
+ GeoCountryIp.new(request.remote_ip).from_chinese_mainland?
+ end
end
helper_method :ip_from_chinese_mainland?
使 ip_from_chinese_mainland?
成为了
一个 helper 方法。
实现 proxy_javascript_include_tag 方法。
app/helpers/application_helper.rb,
module ApplicationHelper
def proxy_javascript_include_tag(*sources)
if ip_from_chinese_mainland?
if request.port.to_i != 443
proxy_host = 'proxy.mysite.com:22443'
else
proxy_host = 'proxy.mysite.com'
end
proxy_sources = []
sources.each {|source|
if source.is_a? String
proxy_source = ProxySource.new(source, proxy_host: proxy_host)
next if proxy_source.is_forbidden_to_proxy?
proxy_sources << proxy_source.url
else
proxy_sources << source
end
}
javascript_include_tag(*proxy_sources)
else
javascript_include_tag(*sources)
end
end
end
然后我们在某个 view 里引入某个 google js,
= proxy_javascript_include_tag "https://www.google.com/jsapi"
如果请求来自中国大陆, 结果为:
<script src="https://proxy.mysite.com/www_g/jsapi" type="text/javascript"></script>
如果请求来自其他地方,结果为:
<script src="https://www.google.com/jsapi" type="text/javascript"></script>