查找出用到 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_pathproxy_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。

安装步骤:

  1. 获取已安装的 nginx 的版本和 configure 参数, 得到 nginx 的版本是 1.4.1
$ /opt/nginx/sbin/nginx -V
  1. 下载 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
  1. 下载 ngx_http_sub_module
$ git clone git://github.com/yaoweibin/ngx_http_substitutions_filter_module.git
  1. 将 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 参数中。

  1. 将新生成的 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 证书

首先需要购买一个靠谱的证书, 这个不赘述了。当我们买好证书后,需要安装证书,安装证书需要下面的几个步骤:

  1. 生成 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

  1. 将 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>