国内网站全站 HTTPS 逐渐流行

刚才浏览豆瓣的时候,发现从 Google 结果页跳的豆瓣音乐页面已经是 https 开头了,于是再试了一次它的主站点(https://www.douban.com)也已经可以访问,支持了 TLS 协议,终于不是再像不久前那样跳回到未加密的 http 页面。

用浏览器控制台稍微看了下豆瓣的资源加载情况(https://music.douban.com/subject/26651208/):
(图片可以右键打开看原图)

对比通过 http 协议请求的时候:

两者的区别其实有些有意思的地方。

现如今一个 web 页面已经不太可能只是引用单一域名下面资源(因为性能、扩展等诸多方面的原因),而这点恰好成为现实中部署 https 的最大阻碍之一——你需要将页面包含的所有资源都走 https 才行!在 CDN 时代,这意味着你需要 CDN 的支持才行。举例来说就是 http://music.douban.com/subject/26651208/ 这样的 url,如果你想将它部署到 https,那么页面里面加载的 img#.douban.com 域名也同样要部署 https!

看起来走 https 的豆瓣的部署是通过将js, css, 图片等静态资源放到一个新的域名(doubanio.com)下面来提供的,而不是像一般的做法那样——直接在原来的 img#.douban.com 静态资源域名下面加 https。这在实现上需要后端根据不同协议在页面插入不同域名。这点来说,推测应该是这样:豆瓣为了尽量不影响 http 协议的访问,引入了新的 doubanio.com 域名来发布静态资源。实际上通过查看 doubanio.com 的 DNS CNAME 记录,可以看到这个域名是指向了腾讯云的 CDN,豆瓣静态资源站点是通过腾讯云来支持 CDN 的。说实话,看到国内 CDN 终于也开始支持 https,其实挺意外的(当然,也许是我在这点的认知已经太落伍了,12306 的“证书污染”可能是过去式了吧)。如果没有记错的话,以前 img#.douban.com 这些域名是在用蓝汛的 CDN,至于为啥不用了,本人当然是不知道的:)。目测豆瓣在 https 部署稳定后,也会将 http 页面的静态资源转向 doubanio.com (其实目前观察来看已经有这个趋势了)。这样,整个 https 化的过程就是平滑而用户感知不到的了。

腾讯云CDN 用了 spdy/3.1 协议,这点对于性能来说还是挺不错的。不过也许为了安全性,豆瓣正式推出后应该加个 HSTS 响应头的,毕竟,这么做的本意也就是为了用户的安全,默认的 http 还是会让用户被运营商劫持个遍。

另外一个观测到也正在部署 https 的站点是京东。

已经使用了通配证书,但是静态资源站点却并没有走 https,因此浏览器拒绝加载了。看了下,仍然是 CDN 不支持,直接访问 https 的话,是 12306 的证书。

这么多年以来,国内的一些大网站终于开始部署 https,此前有百度、阿里,现在豆瓣、京东正在部署。对于用户来说这是个好事,至少可以少很多中间人攻击/劫持,运营商插入广告等行为(这个应该才是电商网站部署的初衷)。但是更重要的也许还是数据库里面用户信息的保护。

Varnish: How do You Know Your Backend Responses Properly?

You might use varnishstat to monitor backend healthy status, but that is not so directly, instead you can do this:

# varnishlog -g session -C -q 'ReqHeader:Host ~ "example.com"' | grep Status
--  RespStatus     200
--  RespStatus     200
--  RespStatus     200
--  RespStatus     200
--- BerespStatus   200
--- ObjStatus      200
--  RespStatus     200
--  RespStatus     200
--  RespStatus     200
--  RespStatus     200
--- BerespStatus   200
--- ObjStatus      200
--  RespStatus     200
--- BerespStatus   200
--- ObjStatus      200
--  RespStatus     200
--- BerespStatus   200
--- ObjStatus      200
--  RespStatus     200
--- BerespStatus   200
...

Convenient, right? you can tail and grep whatever you want of varnishlog’s output.

Whitelisting IPs in Limiting Request Rate of Nginx and Varnish

Sometimes we want to exclude IP blocks from limited request rate zone of web servers, here is how we can do it in Nginx and Varnish, the Nginx way needs crappy hacks, on the other hand, Varnish handles it really elegant.

The Nginx way:

# in the http block, we define a zone, and use the geoip
# module to map IP addresses to variable
http {
    ...
    limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
    # geo directive maps $remote_addr to $block variable.
    # limited as default
    geo $block {
        default          limited;
        10.10.10.0/24    blacklist;
        192.168.1.0/24   whitelist;
        include more_geoip_map.conf;
    }
}

# server block
server {
    ...
    location /wherever {
        if ($block = whitelist) { return 600; }
        if ($block = limited)   { return 601; }
        if ($block = blacklist) { return 403; }
        # error code 600 and 601 goes to each's internal location.
        error_page 600 = @whitelist;
        error_page 601 = @limited;
    }

    # @whitelist have no limiting, it just passes
    # the requests to backend.
    location @whitelist {
        proxy_pass http://backend;
        # feel free to log into other file.
        #access_log /var/log/nginx/$host_whitelist.access.log;
    }

    # insert limit_req here.
    location @limited {
        limit_req zone=one burst=1 nodelay;
        proxy_pass http://backend;
        # feel free to log into other file.
        #access_log /var/log/nginx/$host_limited.access.log;
    }
    ...
}

The Varnish way:

vcl 4.0;

import vsthrottle;

acl blacklist {
    "10.10.10.0/24";
}

acl whitelist {
    192.168.1.0/24;
}

sub vcl_recv {
    # take client.ip as identify to distinguish clients.
    set client.identity = client.ip;
    if (client.ip ~ blacklist) {
        return (synth(403, "Forbidden"));
    }
    if ((client.ip !~ whitelist) && vsthrottle.is_denied(client.identity, 25, 10s)) {
        return (synth(429, "Too Many Requests"));
    }
}

As you can see, unlike Nginx, Varnish has the powerful if directive, it works just like you’d expect.

Advanced Limiting Request with Nginx (or Openresty)

Nginx had the ngx_http_limit_req_module which can be used to limit request processing rate, but most people seem to only used its basic feature: limiting request rate by the remote address, like this:

http {
    limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
    ...
    server {
        ...
        location /search/ {
            limit_req zone=one burst=5;
        }

This is the example configuration taken from Nginx’s official document, the limit_req_zone directive takes variable $binary_remote_addr as a key to limit incoming request. The key fills a zone named one, which is defined by the zone parameter, it could use up to 10m memory. And the rate parameter says that the maximum request rate of each $binary_remote_addr is 1 per second. In the search location block, we can use limit_req directive to refer the one zone, with the bursts not exceeding 5 requests.

Everything seems great, we have configured Nginx against rogue bots/spiders, right?

No! That configuration won’t work in real life, it should never used in your production environments! Take these following circumstances as examples:

  • When users access your website behind a NAT, they share the same public IP, thus Nginx will use only one $binary_remote_addr to do limit request. Hundreds of users in total could only be able access your website 1 time per second!
  • A botnet is used to crawl your website, it use different IP addresses each time. Again, in this situation, limiting by $binary_remote_addr is totally useless.

So what configuration should we use then? We need to use a different variable as the key, or even multiple variables combined together (since version 1.7.6, limit_req_zone‘s key can take multiple variables). Instead of remote address, it is better to use request HTTP headers to distinguish users apart, like User-Agent, Referer, Cookie, etc. These headers are easy to access in Nginx, they are exposed as built in variables, like $http_user_agent, $http_referer, $cookie_name, etc.

For example, this is a better way to define a zone:

http {
    limit_req_zone $binary_remote_addr$http_user_agent zone=two:10m rate=90r/m;
}

It combines $binary_remote_addr and $http_user_agent together, so different user agent behind NATed network can be distinguished. But it is still not perfect, multiple users could use a same browser, same version, thus they send same User-Agent header! Another problem is that the length of $http_user_agent variable is not fixed (unlike $binary_remote_addr), a long header could use a lot of memory of the zone, may exceeds it.

To solve the first problem, we can use more variables there, cookies would be great, since different user sends their unique cookies, like $cookie_userid, but this still leaves us the second problem. The answer it to use the hashes of variables instead.

Thers is a third-party module called set-misc-nginx-module, we can use it to generate hashes from variables. If you are using Openresty, this moule is already included. So the configuration is like this:

http {
    ...
    limit_req_zone $binary_remote_addr$cookie_hash$ua_hash zone=three:10m rate=90r/m;
    ...
    server {
        ...
        set_md5 $cookie_hash $cookie_userid;
        set_md5 $ua_hash $http_user_agent;
        ...
    }
}

It is OK we used $cookie_hash and $ua_hash in the http block before they are defined in server block. This configuration is great now.

Let’s continue with the distributed botnet problem now, we need to take $binary_remote_addr out of the key, and since those bots usually don’t send Referer header (else you can found what unique about it by yourself), we can take advantage of it. This configuration should take care of it:

http {
    ...
    limit_req_zone $cookie_hash$referer_hash$ua_hash zone=three:10m rate=90r/m;
    ...
    server {
        ...
        set_md5 $cookie_hash $cookie_userid;
        set_md5 $referer_hash $http_referer;
        set_md5 $ua_hash $http_user_agent;
        ...
    }
}

nginx 直接向 logstash 发送 JSON 格式日志

通过 nginx 的 log_format 指令可以很容易直接就在 nginx 这里就生成(几乎是)JSON 格式的消息发送到 logstash

log_format  logstash '{"@timestamp":"$time_iso8601",'
                     '"@version":"1",'
                     '"host":"$server_addr",'
                     '"client":"$remote_addr",'
                     '"size":$body_bytes_sent,'
                     '"domain":"$host",'
                     '"method":"$request_method",'
                     '"url":"$uri",'
                     '"status":"$status",' # status 有可能会是以0开头,比如“009”这样的状态码,因此不能以数值形式保存,需要加括号存为字符串
                     '"referer":"$http_referer",'
                     '"user_agent":"$http_user_agent",'
                     '"real_ip":"$http_x_real_ip",'
                     '"forwarded_for":"$http_x_forwarded_for",'
                     '"responsetime":$request_time,'
                     '"upstream":"$upstream_addr",'
                     '"upstream_response_time":"$upstream_response_time",'
                     '"cache_fetch":"$srcache_fetch_status",' # 统计srcahe缓存命中率
                     '"cache_store":"$srcache_store_status"}';

但是这个还是会有问题,因为中文字符编码的原因,有时候 url 或者 referer 头里面可能会出现”\x”这样的跳脱字符,导致 JSON 解析失败,这个时候需要在 logstash 的 filter 里面再加上配置

filter {
  if [type] == "nginx-access-syslog" {
    mutate {
      gsub => [
        # replace '\x' with '\\x', or json parser will fail
        "message", "\\x", "\\\\x"
      ]
    }
  }
}

把”\x”替换为”\\x”。(这个方法不通用,只对”\x”做了处理,但一般应该也只有”\x”会出现了。。。)
然后我们可以用 kibana 画图了,以上收集的参数还算比较丰富了,可以做出很不错的 dashboard 了

Android 系统更新,愿你一直这么蠢下去

最近 Android 6.0 版本发布了,而我正好有一个Nexus 7 2013,但我却还无法更新它的系统,因为 Google 的傻逼 OTA 更新方式,服务器那头拒绝给我检测到更新。

这当然不是我第一次遇到这种情况了,以前的每次系统更新也是一样,Android 升级的主动权完全不由用户掌握。

Android 系统更新本来就是个老大难的问题,但我实在想不出 Google 的人是怎么想的,就算是官方支持的设备其实也无法及时更新到最新版本的系统。

  • 如果 Google 是想采用灰度发布,那么其实根本就不应该发布。这么做只不过是先拿一部分用户直接来当小白鼠来测试你的系统而已,所有用户的设备都该被当成重要的,你应该事先完全测试好。
  • 如果 Google 是因为服务器带宽资源不足够以立即对所有用户分发,我其实不太相信这种原因。
  • 为甚么不提供方式让用户自己使用其它方式下载安装包手动更新

以上,Apple 实在做得比 Google 好得多,新版本发布后可以立即更新,无论是 OTA 还是自己本地下载更新。

为什么我讨厌引用第三方 Javascript 的网站

很多网站都喜欢在自己网站里面用到其它网站提供的第三方 javascript 库,比如 jQuery,为什么?因为有些老的教程告诉他:

  1. 第三方镜像网站做了CDN,速度访问会比较快。
  2. 用户很可能已经通过其它也是引用了那个 JS 的另外一个网站访问过它了,再次访问它的时候速度就更快了
  3. 节省自己的带宽流量
  4. 浏览器和服务端都有同一域名下的资源访问并发限制,引用其它网站的 JS 就可以规避这个问题

可我不是很认可以上的理由,因为:

  1. 你的网站的可用性打了折扣,因为使用第三方 JS库,用户需要正常访问你的网站,同时他还必须要能够访问那个第三方网站才行。而当用户只是无法访问那个第三方网站的话,你的网站是挂了(视具体功能而定),而且你几乎还无能为力(有信心那么快修改所有页面里面的引用代码吗?)。
  2. 也许用第三方静态资源会使得用户加载速度快点,但没有那么明显,并且也不是决定性因素。静态资源的加载本来就快,如果你的网站不是放在特别垃圾的线路,这并不会是什么问题。影响网站速度的还是动态内容这部分。
  3. 流量也不重要,如果你的网站本来流量就小,自己提供那些静态资源和用外部引用的区别不大;如果你的网站流量大,那是好事,意味着流量成本绝不是你该头疼的事情。
  4. 不安全,引用的外置 JS可能会被劫持,就像百度的被劫持用作攻击 Github 了一样。
  5. 隐私保护。第三方网站可以借此跟踪用户,也可以拿走本该只属于你自己的网站访问统计信息。
  6. 自己弄几个专门分发静态资源的站点同样更容易解决同源加载的问题。

Varnish 位于负载均衡后端的缓存清理

假设有如下的架构:
LB proxy pass to Varnish
我们有时清理缓存会遇到问题:虽然 Varnish 上面的缓存清理机制配置好了,但是使用客户端访问到的那个 URL 却无法清理缓存!具体一点来说是比如客户端使用”http://www.example.com/some/url” 访问到最前端的负载均衡(比如是使用 Nginx),那么当我们使用 PURGE 方法在 Varnish 服务器上面清除”http://www.example.com/some/url”这个 URL 的时候,会发现尽管 ban 规则添加了,但是客户端访问到的仍然是旧的资源;不过使用 Ctrl+f5 却能清除缓存,但这种方法在有多个 Varnish 缓存服务器的情况下不是一个好的清除缓存的方法。
出现这种情况的原因可能是负载均衡上面把 URL 做了修改了,比如 URL 重写、Host 头替换等。这个时候客户端发送过来的”http://www.example.com/some/url”到了 Varnish 服务器可能就变成了访问的是”http://www.example.com/some/other/url”或者”http://other.example.com/some/url”。这个时候自然是无法再使用客户端可见的 URL 来清理缓存,因为缓存服务器上面存在的是另外一个 URL 的缓存!
不过这个问题还是比较容易解决的。思路是是一定要把客户端访问的那个 URL 通过某种方式传递给缓存服务器,然后缓存服务器可以识别到那个前端 URL,并且使用那个客户端 URL 作为清除缓存的标志。
假设最前端是 Nginx 在做调度,我们可以通过以下步骤实现:
1. Nginx proxy pass 到后端的时候添加两个 HTTP 头,比如 X-Forwarded-Host 和 X-Forwarded-Url,分别保存用户请求的原始 Host 头和 URL,这两个会在 Nginx 请求后端的时候被发送。

proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Url  $request_uri;

(注意:proxy_set_header 指令如果在低级别位置定义,则不会继承高级别位置已经定义的。也就是说如果你在 http 这个配置段加了那两条配置指令,但是 location 配置段有其它的 proxy_set_header 指令存在了,那么 location 段不会继承到这两个 HTTP 头的配置,必须要单独在这里再配置一次。坑爹的 Nginx!)
2. Varnish 上面读取 X-Forwarded-Host 和 X-Forwarded-Url 这两个 HTTP 头,并且把它们作为清除缓存的标志。

sub vcl_backend_response {
        if (bereq.http.X-Forwarded-Host) {
                set beresp.http.X-PURGE-Host = bereq.http.X-Forwarded-Host;
        } else {
                set beresp.http.X-PURGE-Host = bereq.http.Host;
        }

        if (bereq.http.X-Forwarded-Url) {
                set beresp.http.X-PURGE-Url = bereq.http.X-Forwarded-Url;
        } else {
                set beresp.http.X-PURGE-Url = bereq.url;
        }
}

清理缓存的是这条命令:

ban ("obj.http.X-PURGE-Host == " + req.http.Host + " && obj.http.X-PURGE-Url ~ ^" + req.url + "$");

最终我们就都可以用和客户端一样的 URL 去清理缓存了。