标签 Nginx 下的文章

不知什么时候起,新浪图床开始对图片采取防盗链措施。还好博客的图片很早之前就迁移到了 OneDrive,免费的才是最贵的来描述使用新浪作为图床的朋友再适合不过。

不过我看了一下防盗链机制,发现还有机会抢救一下。目前发现第三方域名调用图片,Network 为 403,通过图片链接点击也是 403,而直接复制新标签页打开则正常显示。

这种防盗链机制我在 donwa/oneindex 程序上也写过,根据请求头的 Referer 来判断请求的来源页,如果非白名单域名则直接返回 403。

使用 CURL 证实猜想:

$ curl http://ww1.sinaimg.cn/large/0061wtobgy1fxmqoe86rkj30gb0gb76n.jpg -H "Referer:https://blog.wangmao.me/" -G -I 
HTTP/1.1 403 Forbidden
Server: Tengine
Date: Wed, 22 May 2019 08:48:35 GMT
Content-Type: text/html
Content-Length: 254
Connection: keep-alive
X-Tengine-Error: denied by Referer ACL
X-Via-CDN: f=alicdn,s=cache1.cn64,c=183.62.230.102;
Via: cache1.cn64[,403003]
Timing-Allow-Origin: *
EagleId: 7793461515585149156862720e

$ curl http://ww1.sinaimg.cn/large/0061wtobgy1fxmqoe86rkj30gb0gb76n.jpg -H "Referer:https://mobile.sina.com.cn/" -G -I
HTTP/1.1 200 OK
Server: Tengine
Content-Type: image/jpeg
Content-Length: 97491
Connection: keep-alive
Date: Tue, 21 May 2019 03:42:08 GMT
x-debug-hit: sto(97491,0.001)
Pragma: public
Cache-Control: max-age=7776000
Last-Modified: Mon, 08 Jul 2013 18:06:40 GMT
Expires: Mon, 19 Aug 2019 03:42:08 GMT
X-Request-ID: g2.125-1558410128.494000-2249042609
LB_HEADER: wbtngx.29.wbg1.shx.lb.sinanode.com
Via: http/1.1 cnc.beixian.ha2ts4.212 (ApacheTrafficServer/6.2.1 [cMsSfW]), http/1.1 cmcc.beijing.ha2ts4.160 (ApacheTrafficServer/6.2.1 [cMsSfW]), cache4.l2cm12-1[0,200-0,H], cache19.l2cm12-1[0,0], cache8.cn64[70,200-0,M], cache10.cn64[71,0]
X-Via-Edge: 15584101284105dd10d6fdec1b3dd569fc895
X-Via-CDN: f=alicdn,s=cache10.cn64,c=183.62.230.102;f=alicdn,s=cache19.l2cm12-1,c=119.147.70.28;f=edge,s=cmcc.beijing.ha2ts4.205.nb.sinaedge.com,c=111.13.209.93;f=Edge,s=cmcc.beijing.ha2ts4.160,c=221.179.175.205;f=edge,s=cnc.beixian.ha2ts4.213.nb.sinaedge.com,c=172.16.181.61;f=Edge,s=cnc.beixian.ha2ts4.212,c=123.126.157.213
Ali-Swift-Global-Savetime: 1558410128
X-Swift-SaveTime: Tue, 21 May 2019 03:42:08 GMT
X-Swift-CacheTime: 7776000
Age: 104777
X-Cache: MISS TCP_MISS dirn:-2:-2
X-Swift-SaveTime: Wed, 22 May 2019 08:48:25 GMT
X-Swift-CacheTime: 7671223
Timing-Allow-Origin: *
EagleId: 7793461e15585149053975458e

这就好办了,Nginx 直接代理 Referer 欺骗图床服务器。实现代码如下:

server {
  listen 80;
  listen 443 ssl http2;
  server_name sina-img.wangmao.me;
  index index.html index.htm index.php;
  ...
  #全站代理
  location / {
    #设置 host
    proxy_set_header Host $proxy_host;
    #设置 referer 这里我用的手机版的新浪首页你可以自己找找其他的
    proxy_set_header Referer https://mobile.sina.com.cn;
    #最后代理域名 ws1 ww1 wx3 wx4 等
    proxy_pass https://ww1.sinaimg.cn/;
  }
  ...
}

当然这样会走自己的服务器的流量,你需要修改全文替换新浪的域名为你的域名。就像这样 https://sina-img.wangmao.me/large/0061wtobgy1fxmqoe86rkj30gb0gb76n.jpg

还是趁早迁移吧,毕竟免费的才是最贵的。

关于 CORS 的介绍,可以参见往期文章:简单谈谈跨域请求(CORS)

简单来说,CORS 是一种解决浏览器跨域问题的方法,NPM 的实现有 Rob--W/cors-anywhere 这个轮子。

我也曾写过 PHP 的轮子 isecret/gh-oauth-server 用于解决 Github 的跨域请求,最近又冒出了利用 Nginx 反向代理来再造轮子。

思路如下:针对预检请求(OPTION)响应头增加 Access-Control-Allow-* 相关的头信息,查阅文档发现 add_header 可实现;其次是代理URL地址的问题,我选择直接获取域名后的字符作为代理的地址,格式如下:https://cors.wangmao.me/https://github.com/login/oauth/access_token,取 Host https://cors.wangmao.me/ 之后的字符作为 URL,这个地址看起来很诡异,但确实是最优的解决方式,代理地址通过正则可以匹配出来,但是注意这里匹配出来的 uri 实际上是 https:/github.com/login/oauth/access_token,协议部分只有一个 /,所以需要自己补充;另外是代理的 HostReferer,这俩就直接取 $proxy_host 可以搞定。

具体实现如下:

http {
  # 代理变量时需要告知DNS,不然会报出 no resolver defined to resolve 错误
  resolver 8.8.8.8;
}

server {
  listen 80;
  listen 443 ssl http2;
  ssl_certificate /usr/local/nginx/conf/ssl/cors.wangmao.me.crt;
  ssl_certificate_key /usr/local/nginx/conf/ssl/cors.wangmao.me.key;
  ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
  ssl_ciphers TLS13-AES-256-GCM-SHA384:TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-128-GCM-SHA256:TLS13-AES-128-CCM-8-SHA256:TLS13-AES-128-CCM-SHA256:EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
  ssl_prefer_server_ciphers on;
  ssl_session_timeout 10m;
  ssl_session_cache builtin:1000 shared:SSL:10m;
  ssl_buffer_size 1400;
  add_header Strict-Transport-Security max-age=15768000;
  ssl_stapling on;
  ssl_stapling_verify on;
  server_name cors.wangmao.me;
  access_log off;
  error_log /data/wwwlogs/cors.wangmao.me.error.log;
  index index.html index.htm index.php;
  root /data/wwwroot/cors.wangmao.me;
  if ($ssl_protocol = "") { return 301 https://$host$request_uri; }

  include /usr/local/nginx/conf/rewrite/none.conf;
  #error_page 404 /404.html;
  #error_page 502 /502.html;

  #取代理地址 $1 为协议,$2 为地址
  #另外这里取到的 requst_uri https://xxx.com 实际为 http:/xxx.com 只有一个 /
  location ~* "/(.*):/(.*)" {
    #增加响应头允许请求的域和方法等
    add_header Access-Control-Allow-Origin "*";
    add_header Access-Control-Allow-Methods "POST,GET,PUT,OPTIONS,DELETE";
    add_header Access-Control-Max-Age "3600";
    add_header Access-Control-Allow-Headers "Origin,X-Requested-With,Content-Type,Accept,Authorization,FOO";
    add_header Content-Length 0;
    add_header Content-Type "application/json;charset=utf-8,text/plain";
    add_header Proxy-Addr https://cors.wangmao.me;
    #如果为预检请求则直接响应204
    if ($request_method = OPTIONS ) {
      return 204;
    }
    #设置代理 Host 和 Referer
    proxy_set_header Host $proxy_host;
    proxy_set_header Referer $proxy_host;
    #告知代理内容类型为 json
    proxy_set_header Accept "application/json";
    #增加一个代理UA头
    proxy_set_header User-Agent "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36";
    #代理地址
    proxy_pass $1://$2/;
  }

  location ~ /\.ht {
    deny all;
  }
}

紧接上次的 使用 Supervisor 守护 php-fpm 进程,在 Supervisor 控制台中能看见有 Nginx 的任务。这个任务并不是我加的,而是我拿到服务器就已经配好了,很可能是运维配置的。

今天调 Bug 的时候发现了问题,所以分为两篇来讲。

问题描述

上回使用 Laravel Admin 搭建了后台,功能看似一切正常,然而今天给同事演示导出功能的时候出了幺蛾子。问题也实在奇怪:当「导出当前页」能正常导出,「导出全部」则始终网络错误。

神操作

刚开始以为是 Laravel Admin 使用的 Excel 拓展类(maatwebsite/Laravel-Excel)的问题,将导出类替换为 league/csv,然鹅。我发现在测试环境无论是 Laravel-Excel 还是 csv 都能导出。也就是说我白忙活了半天?

日志!

一边安慰自己是在排除代码问题,一边去查看 Nginx 的错误日志。Nginxroot 用户安装的,查看日志必须加 sudo,忽然发现日志一直在输出错误:

2018/09/20 16:14:38 [emerg] 23817#0: bind() to 0.0.0.0:8800 failed (98: Address already in use)
2018/09/20 16:14:38 [emerg] 23817#0: bind() to 0.0.0.0:80 failed (98: Address already in use)
2018/09/20 16:14:38 [emerg] 23817#0: bind() to 0.0.0.0:443 failed (98: Address already in use)
2018/09/20 16:14:38 [emerg] 23817#0: bind() to 0.0.0.0:8800 failed (98: Address already in use)
2018/09/20 16:14:38 [emerg] 23817#0: still could not bind()

每秒都在输出,惊得我立马查看线上环境,然而一切正常。缓过神来,发现这个日志实在眼熟,我们是不是在哪儿见过?简直和 使用 Supervisor 守护 php-fpm 进程 的 php-fpm 的错误日志如出一辙啊。那我可大概知道是什么原因了。

查证

首先在 Nginx 中文文档 中找到 Nginx 主模块,找到 daemon 命令,官方给出的解释是:

语法: daemon on | off

缺省值: on

Do not use the "daemon" and "master_process" directives in a production mode, these options are mainly used for development only. You can use daemon off

大意:在生产环境中 daemonmaster_process 配置均不可使用,仅用于开发测试。

为了方便开发测试 Nginxdaemon 参数默认值为 on

然后找到 Nginx 的配置文件 /usr/local/nginx/conf/nginx.conf,检索 daemon 参数。然后是意料之中 Pattern not found: daemon

解决方案

第一种是直接在 nginx.conf 配置文件中增加 daemon off; 参数。

第二种则是在启动 Nginx 时追加命令,命令为:

/usr/local/nginx/sbin/nginx -g 'daemon off;'

由于线上环境 Nginx 配置文件由 Supervisor 守护,所以直接修改 supervisord.conf

[program:nginx]
command=/usr/local/nginx/sbin/nginx -g 'daemon off;'
directory=/usr/local/nginx
autostart=true
autorestart=true
redirect_stderr=true
priority=10
stdout_logfile=/data/logs/supervisord/nginx.log

修改后记得更新 Supervisor 以及重启 Nginx 进程,命令:

$ supervisorctl reread # 重新读取配置
$ supervisorctl update # 更新配置
$ supervisorctl restart nginx  # 重启 nginx
$ killall nginx  # 杀掉所有的 nginx 进程

至此 Nginx 日志终于消停下来,我也能慢慢的查问题了。

解决问题

如上文,解决完 Nginx 默认进程守护后,日志消停下来终于能看到报错信息。

错误日志:

2018/09/20 15:02:36 [crit] 3396#0: *10 open()
"/usr/local/nginx/fastcgi_temp/2/00/0000000002" failed (13: Permission denied)

看到 Permission denied 瞬间菊花一紧。

回想起来 Nginx 的运行用户是一个普通用户,对以 root 用户的目录确实是不可写的(Nginx 以 root 用户身份安装)。

找到 Nginx 的配置文件 nginx.conf,修改 user 参数为 root

重启 Nginx 后,导出功能正常。问题已经解决。

问题还原

但是,为什么会「导出当前页」能正常导出而「导出全部」就失败呢?刨根问底得去查 参考资料 找到解释:

先简单的说一下 Nginx 的 buffer 机制,对于来自 FastCGI Server 的 Response,Nginx 将其缓冲到内存中,然后依次发送到客户端浏览器。缓冲区的大小由 fastcgi_buffers 和 fastcgi_buffer_size 两个值控制。

Nginx 默认配置如下:

fastcgi_buffers      8 4/8K;
>
> fastcgi_buffers 控制 nginx 最多创建 8 个大小为 4K 的缓冲区,而 fastcgi_buffer_size 则是处理 Response 时第一个缓冲区的大小,不包含在前者中。所以总计能创建的最大内存缓冲区大小是 8*4K+4K = 36k。而这些缓冲区是根据实际的 Response 大小动态生成的,并不是一次性创建的。比如一个 8K 的页面,Nginx 会创建 2*4K 共 2 个 buffers。
>
> 当 Response 小于等于 36k 时,所有数据当然全部在内存中处理。如果 Response 大于 36k 呢?fastcgi_temp 的作用就在于此。多出来的数据会被临时写入到文件中,放在这个目录下面。

也就是说,当前几台服务器上的站点,一旦响应的数据超过 36 KB 超出的部分将写到 *fastcgi_temp* 目录,如果 *fastcgi_temp* 不可写的话将只返回前 36 KB 的内容,难怪手动将分页条数参数给到 *1000* 页面不完整。

## 参考资料

- [分析 fastcgi_temp 错误以及 Nginx 的 Buffer 机制](https://blog.csdn.net/crx05/article/details/70210323)

此文为 造轮子之谷歌镜像站 的衍生。

其实谷歌字体在 2017 年左右就已经解封了,现在解析到的一个北京的 IP 上。对于前端来说,使用谷歌字体再也不用担心加载不出来了。

最早玩博客的时候,谷歌字体是我必屏蔽的(解决不了问题,就解决出问题的代码),到后来我使用过 360 旗下的 http://fonts.useso.com (挂了)也用过 Cat Networks 下的 https://fonts.cat.net,到后来谷歌字体解封,喜大普奔连忙换上 https://fonts.googleapis.com,享受着谷歌给开发者带来的福利。

问题出现在最近,公司的网络防火墙貌似把谷歌字体库给墙了。以上所有字体库全都凉凉(<de;>防火墙:对,不是针对谁,在座的都是辣鸡)。这样带来的问题就是——我特么打开一个带谷歌字体的网页先让我看近半分钟的开场白。我的博客,我刚写的 OpenAPI,无一幸免。

我得做点什么。首先得明白是什么原因,打开终端输入命令 ping fonts.googleapis.com 得到 IP 172.217.24.42(香港,且无法 Ping 通);拔掉网线,连上手机热点终端输入命令 ping fonts.googleapis.com 得到 IP 203.208.50.70(北京,能 Ping 通)。

其实现在问题变得很简单了,公司的网络将谷歌字体库的域名仍解析在国外的服务器上。所以我们只需要将本地的 Hosts 文件手动指向北京的 IP 就好了。Windows 在 C:\Windows\System32\drivers\etc\hosts,Mac / UNIX 在 /etc/hosts 新增一行 203.208.50.70 fonts.googleapis.com 就能解决一半的问题。

然鹅。我就喜欢用复杂的方式来解决简单的问题!脑海中冒出一个有趣的想法——为什么不自己搭建一个谷歌字体镜像呢?

利用 Nginx 进行反向代理,然后用 CDN 做缓存

套路和搭建谷歌镜像站差不多,不过值得注意的是在 fonts.google.com 中得到的 CSS 文件里有 fonts.gstatic.com (北京 IP 为 203.208.51.56,修改 Hosts 文件的话需要新增一行 203.208.51.56 fonts.gstatic.com)的路径,如果 fonts.googleapis.com 被墙了,那么这个肯定也不可幸免。所以需要反向代理两个站点。

搭建这个镜像站也不需要境外服务器,只需要你的服务器能正常访问就行。配置如下:

server
{
    listen 80;
    # listen 443 ssl http2;
    server_name fonts.openapi.link;
  
    # 强制跳转 HTTPS
    #if ($server_port !~ 443){
    #    rewrite ^(/.*)$ https://$host$1 permanent;
    #}
    #HTTP_TO_HTTPS_END
    # HTTPS 证书地址
    # ssl_certificate    /foo/bar.key; 
    # ssl_certificate_key    /foo/bar.pem;
    # ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    # ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
    # ssl_prefer_server_ciphers on;
    # ssl_session_cache shared:SSL:10m;
    # ssl_session_timeout 10m;
    # error_page 497  https://$host$request_uri;
    
    # 用于 fonts.googleapis.com 代理
    location /css {
      sub_filter 'fonts.gstatic.com' 'fonts.openapi.link';
      sub_filter_once off;
      sub_filter_types text/css;
      proxy_pass_header Server;
      proxy_set_header Host fonts.googleapis.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 https://fonts.googleapis.com;
      proxy_cache cache_one;
      proxy_cache_key $host$request_uri$is_args$args;
      proxy_cache_valid 200 304 301 302 1h;
      expires 365d;
    }
    # 用于 fonts.gstatic.com 代理
    location / 
    {
      proxy_pass_header Server;
      proxy_set_header Host fonts.gstatic.com;
      proxy_redirect off;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Scheme $scheme;
      proxy_pass https://fonts.gstatic.com;
      proxy_cache cache_one;
      proxy_cache_key $host$request_uri$is_args$args;
      proxy_cache_valid 200 304 301 302 1h;
      expires 365d;
    }
    # 日志
      access_log  /www/wwwlogs/fonts.openapi.link.log;
}

总算折腾完了,配置上 CDN,速度杠杠的。也欢迎使用我的谷歌字体镜像 fonts.openapi.link

嗷~对了!各位看官再等等,我有一个大宝贝给你们介绍一下(掏裤裆)——开放 API:为开发者而生。

后来我想想,我折腾了这么大一圈图什么呢?