header image

删除Firefox过期的HPKP信息

前不久手贱,试了试HPKP,结果不知为突然出问题了。然后Firefox就彻底拒绝了我的网站。

HPKP,即The Public Key Pinning Extension for HTTP,是将证书的发行者的验证信息添加到一个HTTP头信息。用户代理在访问时,不光验证证书的有效性,还验证证书的发行者,从而避免中间人攻击。

出问题的原因很简单,证书的发行者不知为啥和原来有所不同(我用的是上一篇介绍的Let’s Encrypt,证书是自动颁发的),而HPKP又没有修改,所以Firefox就拒绝了。

但是不想折腾了,于是就删除了HPKP头信息,结果由于之前设置的过期时间太长,造成Firefox还是拒绝。很明显是Firefox缓存了相关信息。放狗找了一下,找到了一篇文章(见引用来源)。

只要把Firefox用户档案下面的SiteSecurityServiceState.txt删除就行了。

Linux是在用户的家目录下面,也就是~/.mozilla/firefox/xxxxxxxx.default/SiteSecurityServiceState.txt,其中xxxxxxxx是一串随机字符。

而Windows就在%APPDATA%\Mozilla\Firefox\Profiles\xxxxxxxx.default\SiteSecurityServiceState.txt%APPDATA%是一个系统变量,代表C:\Users\[username]\AppData\Roaming这个目录([username]是你的用户名)。

删除之前关闭Firefox,删除后再打开就正常了。

在Ubuntu上获取Let’s Encrypt免费证书

Let’s Encrypt是电子前哨基金会(EFF)发布的免费SSL证书服务,受到微软、谷歌等大佬的支持,但是配置比较复杂(主要是证书有效事件较短,需要重新签发,虽然签发的过程都是在自己的机器上通过命令行实现),之前研究过一下,最终还是放弃了。

但最最近由消息称沃通的CA将有可能被Mozilla拒绝,所以还是重新弄起来吧。下面说一下步骤。

安装Certbot

Certbot是EFF开发的一个简单的工具,比起我之前研究的时候弄的ACME要简单得多,访问https://certbot.eff.org/ 之后,选择你的服务器(webserver)和操作系统(Operating System),就会给出简单的步骤了,我们还是一步一步来吧。

我的服务器是Nginx,操作系统是Ubuntu 16.04。由于自带了软件包,安装只需要一行命令即可:

sudo apt-get install letsencrypt

如果是其他版本的Ubuntu,只需要下载一个脚本就行了(下面的命令在需要下载到的目录里执行):

wget https://dl.eff.org/certbot-auto
chmod a+x certbot-auto
./certbot-auto

签发证书

很简单,直接运行letsencrypt命令即可(使用其他版本的Ubuntu的把letsencrypt换成上面下载的certbot-auto,注意执行脚本需要完整路径/path/to/certbot-auto,在脚本所在目录下是就是./certbot-auto)。

sudo letsencrypt certonly --webroot -w /var/www -d example.com -d www.example.com -w /var/www/sub -d sub.example.com

第一次运行时会问你一些你的信息,以后就不会再询问了。

解释一下,certonly是它的子命令,表示只颁发证书。--webroot是它的插件,用于自动验证域名。-w是指定网站的目录,-d是指定目录对应的域名。一个-w后可以跟着多个-d就对应多个域名。可以用多个-w设置多个目录对应不同的域名。这些域名都在一个证书里。

需要注意的是,域名下的\.well-know\acme-challenge\目录下的文件必须要可以访问。这个是Let’s Encrypt用于验证域名所有权的。它会在上面-w参数设置的目录下建立临时文件,然后通过HTTP访问,比如上面的设置就会新建诸如/var/www/.well-known/acme-chanllenge/xxxxxxx(xxxxxxx是随机字符),然后通过http://example.com/.well-known/acme-chanllenge/xxxxxxx来验证。如果你的网站是纯HTTPS的,你可以用把这个地址重定向到https://example.com/.well-known/acme-chanllenge/xxxxxxx,可以成功通过验证。

看到Congratulations!就代表成功了,下面我们就可以配置Nginx了。

配置HTTP服务器

证书所在的地方是/etc/letsencrypt/下,archive里面是所有证书的存档,keys里面是所有证书,不过我们不用管,我们需要的证书在live下面,对应网站域名的目录下面就是了。一共有四个文件。

  • privkey.pem 这是私匙,对应Nginx的ssl_certificate_key选项,或者Apache2的SSLCertificateKeyFile选项。

  • cert.pem 服务器证书,这个只有Apache2低于2.4.8版本需要,对应SSLCertificateFile选项。

  • chain.pem 除服务器证书之外的所有证书,对于1.3.7版以上的Nginx对应ssl_trusted_certificate选项,对于低于2.4.8的Apache2对应SSLCertificateChainFile选项。

  • fullchain.pem 包括上面的服务器证书和其他证书,Nginx对应ssl_certificate选项,2.4.8版以上的Apache2对应SSLCertificateFile

所以对于我用的Nginx来说,只需要privkey.pemfullchain.pem这两个就够了。Apache2的话参考设置HTTPS的文章,对应设置上面提到的文件和选项就行了。

在Nginx的server下设置如下响应的ssl选项就行了,如:

server {
    listen 443 ssl;
    server_name example.com www.example.com;

    root /var/www;
    index index.html;

    ssl on;
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate /etc/letsencrypt/live/example.com/privkey.pem;

    location / {
        try_files $uri $uri/ =404;
    }
}

如果你是第一次设置HTTPS,记得在listen选项那里要监听443端口,并在后面加上ssl。如果要启用HTTP/2,请参考这篇文章

最好在原来的80端口上设置好跳转:

server {
    listen 80;
    server_name example.com www.example.com;
    rewrite ^(.*)$ https://example.com$1 permanent;
}

设置好了别忘了重启Nginx。

自动更新证书

更新证书很简单,直接如下命令就可以了:

sudo letsencrypt renew

可以用--dry-run选项来模拟更新证书,看看会不会出错。由于Ubuntu自带的版本有点旧,所以会出错……(certbot-auto的是最新的所以不会有问题)。这时候就需要加上--force-renew选项来强制更新。第一次运行可能还要你同意服务条款,所以根据提示加上--agree-tos(以后就不需要了)。

在正式更新的时候,要使证书生效还需要重启Nginx。如果是certbot-auto我们可以用--pre-hook--post-hook来设置更新前后的命令,用来停止和启动Nginx。

sudo /path/to/certbot-auto renew --pre-hook "systemctl stop nginx" --post-hook "systemctl start nginx"

但是Ubuntu自带的版本有点旧,所以就只好自己写脚本了。

#!/bin/sh
systemctl stop nginx
letsencrypt renew --force-renew
systemctl start nginx

保存文件,这里我们保存到/opt/scripts/letsencrypt_renew

我们可以用crond或者systemd来自动更新证书,这里就简单点,用crond吧。用sudo crontab -e命令编辑root用户的crontab。在最后面新插入行并添加如下内容:

0 */12 * * * sh /opt/scripts/letsencrypt_renew

上面的意思是,每12小时的第0分执行脚本一次。因为Let’s Encrypt的推荐每12小时更新一次。更新操作并不一定会真的更新证书,只有在快到期的时候才会更新。

如果不想修改root的crontab,可以在/etc/cron.d下新建文件写入如下内容:

0 */12 * * * root sh /opt/scripts/letsencrypt_renew

比起crontab,就是在要执行的命令前面多了一个表示用户的root

更新证书是检测/etc/letsencrypt/renewal下的配置文件进行的,所以即使不管颁发多少个证书,上面的更新脚本都不用修改。

最后

如果要删除证书的话,把/etc/letsencrypt/下的archiveliverenewal中对应域名的文件或目录删除就可以了。

此外,还可以用-m选项来设置Email地址。还有更多的功能就去看看官方的文档吧:https://certbot.eff.org/docs/

Nginx启用HTTP/2

本着更快、更快、更快精神搞出来的HTTP/2,不用还是人?下面就来说一下吧。

准备工作

首先你需要Nginx 1.9.5以上版本。如果是Ubuntu Server 16.04 LTS的话,自带的是1.10的版本所以没有问题。其他系统请自行解决。

其次你需要启用ngx_http_v2_module模块。如果是Ubuntu Server 16.04 LTS的话就需要安装nginx-corenginx-fullnginx-extras,反正nginx-light是没有的。如果是自行编译需要在设置时加上--with-http_v2_module选项。

再次需要配合OpenSSL 1.0.2以上版本使用,如果是Ubuntu Server 16.04 LTS的话,自带的是1.0.2g的版本所以没有问题。其他系统请自行解决。

最后,你需要在Nginx上启用HTTPS,这涉及到SSL证书的申请、配置,又是一篇文章的内容了,以后有机会再说,这里请自行解决吧。

配置

禁用旧版加密

如果不禁用的话,会造成虽然启用了但是没法正常访问的问题。

Ubuntu上,只要修改/etc/nginx/nginx.conf文件,在http一节里找到或新加入这样一行(如果是软件包自带的配置文件的话,可以加在ssl_prefer_server_ciphers一行后面):

ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;

配置HTTP/2

之后我们只要修改站点的配置文件,在listen监听的HTTPS端口443后面加上ssl http2就行了。监听80端口的并不能启用HTTP/2,请放弃。

比如:

server {
    listen 443 default_server ssl http2;
    listen [::]:443 default_server ssl http2;

    server_name example.com;

    ssl on;
    ssl_certificate /path/to/example.com.crt;
    ssl_certificate_key /path/to/example.com.key;

    root /var/www;

    index index.html index.htm;

    location / {
        try_files $uri $uri/ =404;             
    }
}

重启Nginx

Ubuntu只要如下命令就行了:

sudo systemctl restart nginx

最后

如果使用Firefox,可以安装一个叫做HTTP/2 and SPDY indicator的扩展,安装之后会在地址栏右侧显示一个闪电样的小图标,如果网站是HTTP/2就会显示蓝色,如果是SPDY(HTTP/2的前身)就会显示绿色,如果没有则显示灰色。

也可以打开开发者工具里的网络,可以看到“版本”一项显示“HTTP/2.0”。

HTTP/2好处都有啥,可以参考维基百科HTTP/2条目英文的更详细)。

在Ubuntu Server 16.04 LTS上安装PHP7

之前我们说过在Ubuntu Server 14.04 LTS上安装PHP7的事宜。现在新的LTS已经出了,当然也要说一说。

安装

很简单(以FPM为例,需要Apache2模块的自行研究):

sudo apt-get install php7.0-fpm php7.0-mysql php7.0-curl php7.0-mcrypt php7.0-json php7.0-gd

基本上这样就能运行大多数PHP程序了。还有其他一些包需要的请自行安装。

配置

配置文件和过去有点变化,在/etc/php/7.0/fpm下。和过去一样,修改php.ini等相关文件就行了。

默认的监听池的路径也有点变化,在/var/run/php/php7.0-fpm.sock,所以NginxApache2的相关设置就需要改一下。

因为16.04使用了SystemD,和过去不同,重启服务的方式如下:

sudo systemctl restart php7.0-fpm

总结

就这么简单。

添加代码高亮功能

技术类的博客,不来点代码高亮总觉得逼格不够。下面就说说如何加入这个功能吧,所有主题通用(其他类型网页也可以举一反三,方法都是类似的)。

准备

按照效率来说,使用PHP的方案自然是最好的。但鉴于使用PHP会造成需要修改的地方比较多比较麻烦,所以就不考虑了。这次就用一个比较常见JavaScript库来解决——highlight.js。这个库简单易用,而且自带很多种样式选择,总有一种适合你。

选择样式

这里有很多种预设样式可以选择。左边的Language categories就是选择语言的分类,Styles就是选择样式,右边就可以查看效果。确定好要使用的样式之后,到这里找到对应的css文件,点开后把代码复制下来保存到主题目录就行了。比如我就保存到主题下的css/highlight.css文件中。

然后就要把样式引入主题中,最简单的方法就是修改主题的头部文件header.php(视主题而定,一般是这个文件)的</head>前添加这样一行:

<link rel="stylesheet" href="<?php echo esc_url( get_template_directory_uri() ); ?>/css/highlight.css">

<?php echo esc_url( get_template_directory_uri() ); ?>是输出当前主题的网络路径。可以根据需要修改。

关于CDN

highlight.js我们可以通过网上的CDN来引入,也可以放到自己的网站上。比如使用百度的CDN,就上百度静态资源公共库搜索hightlight,然后就会得到结果了。如果放到自己的网站上,你就得考虑相对路径、绝对路径等等,这里不做讨论(有问题请留言)。下面会以path/to/highlight.js来统一指代。

题外话:关于HTTPS和HTTP的引用

有的网站既有HTTP访问又有HTTPS访问,这时网页中的资源引用就会出现问题。如果从HTTPS页面引用HTTP的资源就会被浏览器拒绝。

这时可以使用一种无协议的引用方式,当然前提是被引用资源要两种都能访问。比如:

<script src="//example.com/code.js"></script>

这样在HTTP页面上,浏览器就会引用http://example.com/code.js,在HTTPS页面上就会引用https://example.com/code.js。对于CSS也可以使用这种方式引用。

最简单的方法:让highlight自动处理

这种方法是让highlight自动处理相关的代码块,只要在主题脚部文件footer.php(视主题而定,一般是这个文件)的</body>前加入如下代码:

<script src="path/to/highlight.js"></script>
<script>hljs.initHighlightingOnLoad();</script>

第一行是引用highlight.js的文件,第二行是执行初始化。

highlight.js会自动寻找结构为<pre><code>...</code></pre>的代码块,并自动判断语言。也可以通过<code>元素的类来指定语言或者不要高亮。支持的语言名称和别名可以在这里查到(右边那一列就是)。语言前也可以加上language-或者lang-。不要高亮则设置为nohighlight。比如:

<pre><code class="php">PHP代码</code></pre>
<pre><code class="lang-html">HTML代码</code></pre>
<pre><code class="language-js">JavaScript代码</code></pre>
<pro><code class="nohighlight">不要高亮</code></pre>

如果装过之前介绍的Markdown插件的,还可以更方便。可以用三个或更多`来代表代码块,并在后面加上直接加上类名。比如:

```php
PHP代码
```

```language-html
HTML代码
```

```lang-js
JS代码
```

```nohighlight
不要高亮
```

高级方法:无法保证代码块的结构时,或是代码块动态生成时

上面的方法要求代码块的结构是<pre><code>...</code></pre>这样的,如果不能保证这样的结构我们就需要额外的处理了。另外,如果代码块是动态加载的,也需要额外处理。

加载JS的代码是一样的,只不过不用初始化了:

<script src="path/to/highlight.js"></script>

我们要把元素传递给highlight.js的hljs.highlightBlock接口处理,这个接口接受一个参数,就是需要处理的DOM元素。和上面类似,我们可以用类属性来指定语言种类。

如果代码不是在pre或者设置过CSS属性white-space保留换行的元素之中的话我们需要用<br>元素来换行。这种情况我们要先设置一下让highlight使用<br>来换行(以下为JavaScript代码,需要放到<script>元素中,或者放到外部文件中通过<script>元素的src属性引用):

hljs.configure({useBR: true});

然后我们就要选择所有的包含代码的元素然后传递给highlight.js处理。比如我们的代码块结构是<div><code>...</code></div>这样的,那么就要用如下代码:

code_elms = document.querySelectorAll('div code');
// code_elms = Array.prototype.slice.call(code_elms);
for(var i=0; i <= code_elms.length; i++) {
    code_elm = code_elms[i];
    hljs.highlightBlock(code_elm);
}

document.querySelectorAll是受到主流浏览器的支持的通过类CSS选择器来选择元素的接口,和jQuery的选择器很类似,返回的是一个类数组对象。但实际上并不是数组,其中的成员会随着DOM的变化而变化,所以如果页面上的元素会动态发生变化,最好取消第二行的注释以将其转换为真正的数组。

如果对ECMAScript6感兴趣,那么这个还可以更简化

Array.from( document.querySelectorAll('div code') ).forEach( code => hljs.highlightBlock(code) );

这里用到了ES6的Array.from来转换为数组元素,然后用forEach方法来遍历每个成员。然后用箭头函数来定义了forEach的回调函数。鉴于浏览器支持的原因,不建议这样使用。

还可以用jQuery,这个在highlight.js的官网上就给出例子了:

$('div.code').each(function(i, block) {
  hljs.highlightBlock(block);
});

高级方法:使用Web Workers

什么是Web Workers,简单点说就是在后台执行JavaScript代码的。使用Web Worker可以避免在执行耗时代码时当前页面假死。highlight.js在处理量非常大的代码时有可能会假死,所以放到Web Workers中执行是很好的解决方法。

使用Web Workers就不需要在页面中引用highlight.js,我们将在Worker中引用。

首先我们需要修改header.php文件,在</head>之前加入:

<script>
    addEventListener('load', function() {
        var codes = document.querySelectorAll('pre code');
        if(codes.length > 0) {
            for(var i=0; i<codes.length;i++) {
                var code=codes[i];
                var msg = {
                    lang: Array.prototype.slice.call(code.classList),
                    content: code.textContent
                };
                var worker = new Worker('<?php echo esc_url( get_template_directory_uri() ) ?>/js/highlight-worker.js');
                worker.onmessage = function(c) {
                    return function(event) { c.innerHTML = event.data; }
                }(code);
                worker.postMessage(msg);
            }
        }
    });
</script>

第10行中因为我们引用了外部文件,所以用<?php echo esc_url( get_template_directory_uri() ) ?>输出了主题目录的网络路径。这段代码的意思就是,在窗口的load事件上绑定了行为:选择所有符合pre code选择器的元素,然后遍历每一个元素,把元素的内容和类属性通过msg对象传递给Worker,然后把Worker返回的数据替换元素的内容。Worker是使用一个叫做highlight-worker.js的文件建立的。

根据上面的代码可以看出,highlight-worker.js我放到了主题目录下的js/highlight-worker.js,其内容如下:

onmessage = function(event) {
    importScripts('path/to/highlight.js');
    var result = event.data.lang.length==0 ? self.hljs.highlightAuto(event.data.content) : self.hljs.highlightAuto(event.data.content, event.data.lang);
    postMessage(result.value);
}

这段代码是在Worker的message事件上绑定行为:先导入highlight.js文件,然后根据传入的数据(也就是前面的msg对象)的lang属性的有无,调用hljs.highlightAuto接口来处理传入的数据的content属性(也就是代码内容)。然后把处理结果返回。

这样就行了。

最后

如果想查看highlight.js的完整API列表,可以到这里。还可以研究一下自己修改样式。虽然这个方案并不是最佳的,比如效率问题,比如高亮还是不如一些代码编辑器,比如不会加行号等等,但对于代码的可读性已经是很不错的提升了,更复杂的功能以后再研究吧。