Caddy – 方便够用的 HTTPS server 新手教程

说起 HTTP server,使用最广泛的就是 apache 和 nginx 了,功能都非常强大,但相对而言,学习它们的配置是有一定难度的。最近发现了一个 golang 开发的 HTTP server,叫做 Caddy,它配置起来十分简便,甚至可以 28 秒配置好一个支持 http2 的 server ,而且对各种 http 新特性都支持的比较早(比如 http2、quic都有支持)。因此对于不用于生产环境只搭建个人博客是十分友好的,我就简单介绍下 caddy。

Caddy – 方便够用的 HTTPS server 新手教程

aa

1. 安装

用过 golang 的应该都知道,golang 程序基本上不会有各种依赖,都是光秃秃一个可执行程序,cp 到 /usr/local/bin就算安装完成了,所以说安装 caddy 是很简单的,我给出三种方法。

1.1 脚本安装

  1. curl -s https://getcaddy.com | bash

caddy 官方给出了一个安装脚本,执行上面的命令就可以一键安装 caddy,等执行结束后,使用 which caddy,可以看到 caddy 已经被安装到了 /usr/local/bin/caddy

1.2 手动安装

https://caddyserver.com/download 点这个链接进入到 caddy 官网的下载界面,网页左侧可以选择平台和插件,如果在 Linux 服务器上使用的话,platform 选择 Linux 64-bit 就可以了,plugins 如果暂时不需要的话,可以不选。然后点击下面的 DOWNLOAD 按钮,就下载到 caddy 了。同理,解压之后用 cp 命令放到 /usr/local/bin/caddy 就完成了安装。

image_1bn3mbk8jov2es3vu41l842ac9.png-177.7kB

1.3 源码安装

  1. go get github.com/mholt/caddy/caddy

对于安装了 golang 编译器的同学,只需要执行 go get 就能到 $GOPATH/bin 里,是否 cp 到 /usr/local/bin 里就看心情了。使用源码安装可以安装到最新版本的 caddy,功能上一般是最新的,而且因为是本地编译,性能可能会稍微高一些,但是可能会存在不稳定的现象。

2. 配置

2.1 临时文件服务器

Caddy 的配置文件叫做 Caddyfile,Caddy 不强制你把配置文件放到哪个特定文件夹,默认情况下,把 Caddyfile 放到当前目录就可以跑起来了,如下:

  1. echo 'localhost:8888' >> Caddyfile
  2. echo 'gzip' >> Caddyfile
  3. echo 'browse' >> Caddyfile
  4. caddy

在随便一个目录里执行上面代码,然后在浏览器里打开 http://localhost:8888 发现 caddy 已经启动了一个文件服务器。当临时需要一个 fileserver 的时候(比如共享文件),使用 caddy 会很方便。

2.2 生产环境使用

当然了,在生产环境使用的时候就不能这么草率的把配置文件放到当前目录了,一般情况下会放到 /etc/caddy 里。

  1. sudo mkdir /etc/caddy
  2. sudo touch /etc/caddy/Caddyfile
  3. sudo chown -R root:www-data /etc/caddy

除了配置文件,caddy 会自动生成 ssl 证书,需要一个文件夹放置 ssl 证书。

  1. sudo mkdir /etc/ssl/caddy
  2. sudo chown -R www-data:root /etc/ssl/caddy
  3. sudo chmod 0770 /etc/ssl/caddy

因为 ssl 文件夹里会放置私钥,所以权限设置成 770 禁止其他用户访问。

最后,创建一下放置网站文件的目录,如果已经有了,就不需要创建了。

  1. sudo mkdir /var/www
  2. sudo chown www-data:www-data /var/www

创建好这些文件和目录了之后,我们需要把 caddy 配置成一个服务,这样就可以开机自动运行,并且管理起来也方便。因为目前大多数发行版都使用 systemd 了,所以这里只讲一下如何配置 systemd,不过 caddy 也支持配置成原始的 sysvinit 服务,具体方法看这里

  1. sudo curl -s https://raw.githubusercontent.com/mholt/caddy/master/dist/init/linux-systemd/caddy.service -o /etc/systemd/system/caddy.service # 从 github 下载 systemd 配置文件
  2. sudo systemctl daemon-reload # 重新加载 systemd 配置
  3. sudo systemctl enable caddy.service # 设置 caddy 服务自启动
  4. sudo systemctl status caddy.service # 查看 caddy 状态

3. Caddyfile

基本的安装配置搞定之后,最重要的就是如何写 Caddyfile了。可以直接 vim /etc/caddy/Caddyfile 来修改 Caddyfile,也可以再自己电脑上改好然后 rsync 到服务器上。如果修改了 Caddyfile 发现没有生效,是需要执行一下 sudo systemctl restart caddy.service 来重启 caddy 的。

3.1 Caddyfile 的格式

Caddfile的格式还是比较简单的,首先第一行必须是网站的地址,例如:

  1. localhost:8080

  1. lengzzz.com

地址可以带一个端口号,那么 caddy 只会在这个端口上开启 http 服务,而不会开启 https,如果不写端口号的话,caddy 会默认绑定 80 和 443 端口,同时启动 http 和 https 服务。

地址后面可以再跟一大堆指令(directive)。Caddyfile 的基本格式就是这样,由一个网站地址和指令组成,是不是很简单。

3.2 指令

指令的作用是为网站开启某些功能。指令的格式有三种,先说一下最简单的不带参数的指令比如:

  1. railgun.moe # 没错,moe后缀的域名也可以哦
  2. gzip

第二行的 gzip 就是一个指令,它表示打开 gzip 压缩功能,这样网站在传输网页是可以降低流量。

第二种指令的格式是带简单参数的指令:

  1. railgun.moe
  2. gzip
  3. log /var/log/caddy/access.log
  4. tls lengz@lengzzz.com
  5. root /var/www/

第三行,log 指令会为网站开启 log 功能,log 指令后的参数告诉 caddy log 文件存放的位置。第四行的 tls 指令告诉 caddy 为网站开启 https 并自动申请证书,后面的 email 参数是告知 CA 申请人的邮箱。(caddy 会默认使用 let’s encrypt 申请证书并续约,很方便吧)

另外,简单参数也可能不只一个,比如 redir 指令:

  1. railgun.moe
  2. gzip
  3. log /var/log/caddy/access.log
  4. tls /etc/ssl/cert.pem /etc/ssl/key.pem
  5. root /var/www/
  6. redir / https://lengzzz.com/archive/{uri} 301

上面的 redir 指令带了三个参数,意思是把所有的请求使用 301 重定向到 https://lengzzz.com/archive/xxx,这个指令在给网站换域名的时候很有用。另外 tls 指令变了,不单单传 email一个参数, 而是分别传了证书和私钥的路径,这样的话 caddy 就不会去自动申请证书,而是使用路径给出的证书了。

在这个例子里还使用了 {uri} 这样的占位符(placeholder),详细的列表可以在这里查询到:https://caddyserver.com/docs/placeholders

最后一种指令是带复杂参数的,这种指令包含可能很多参数,所以需要用一对花括号包起来,比如 header 指令:

  1. railgun.moe
  2. gzip
  3. log /var/log/caddy/access.log
  4. tls lengz@lengzzz.com
  5. root /var/www/
  6. header /api {
  7. Access-Control-Allow-Origin *
  8. Access-Control-Allow-Methods "GET, POST, OPTIONS"
  9. -Server
  10. }
  11. fastcgi / 127.0.0.1:9000 php {
  12. index index.php
  13. }
  14. rewrite {
  15. to {path} {path}/ /index.php?{query}
  16. }

6-10 行的 header 指令代表为所有的 /api/xxx 的请求加上 Access-Control-Allow-Origin 和 Access-Control-Allow-Methods 这两个 header,从而能支持 javascript 跨域访问 ,第 9 行代表删除 Server header,防止别人看到服务器类型。

11-13 行使用了 fastcgi 指令,代表把请求通过 fastcgi 传给 php,ruby 等后端程序。

14-15 行,使用了 rewrite 指令,这个指令的作用是 服务器内部重定向 在下面的参数 to 后面,又跟了三个参数,这个功能上有点类似 nginx 的 try_files 。告诉 caddy 需要先查看网址根目录 /var/www 里有没有 {path} 对应的文件,如果没有再查看有没有 {path} 对应的目录,如果都没有,则转发给 index.php 入口文件。这个功能一般会用在 PHP 的 MVC 框架上使用。

随着一步步完善这个 Caddyfile,目前这个版本的 Caddyfaile 已经可以直接在网站中使用了。

3.3 多 HOST 网站

刚才说的一直都是单个域名的网址,那么如果在同一个服务器上部署多个域名的网站呢?很简单,只需要在域名后面跟一个花括号扩起来就可以了,如下:

  1. railgun.moe {
  2. gzip
  3. log /var/log/caddy/railgun_moe.log
  4. tls lengz@lengzzz.com
  5. root /var/www/
  6. header /api {
  7. Access-Control-Allow-Origin *
  8. Access-Control-Allow-Methods "GET, POST, OPTIONS"
  9. -Server
  10. }
  11. fastcgi / 127.0.0.1:9000 php {
  12. index index.php
  13. }
  14. rewrite {
  15. to {path} {path}/ /index.php?{query}
  16. }
  17. }
  18. lengzzz.com {
  19. tls lengz@lengzzz.com
  20. log /var/log/caddy/lengzzz_com.log
  21. redir / https://railgun.moe/{uri} 301
  22. }

好了,基本的 caddy 配置就这些,详细的内容可以去官网上看文档学习。

https://caddyserver.com/docs

转自: https://lengzzz.com/note/caddy-http-2-web-server-guide-for-beginners

WebSocket(叁) 生成数据帧

昨天的文章中介绍了WebSocket数据帧的结构和解析。其实对从服务器发送往客户端的数据也是同样的数据帧。但因此觉得这看似和解析数据帧一样简单那就错了。我们需要自己去生成数据帧。而且会遇上和解析时候不同的问题,比如数据帧分片传输的情况。
从服务器发送到客户端的数组帧不需要掩码,这是非常值得庆幸的地方。于是要写出一个生成数据帧的函数并不难

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//NodeJS
function encodeDataFrame(e){
  var s=[],o=new Buffer(e.PayloadData),l=o.length;
  //输入第一个字节
  s.push((e.FIN<<7)+e.Opcode);
  //输入第二个字节,判断它的长度并放入相应的后续长度消息
  //永远不使用掩码
  if(l<126)s.push(l);
  else if(l<0x10000)s.push(126,(l&0xFF00)>>8,l&0xFF);
  else s.push(
    127, 0,0,0,0, //8字节数据,前4字节一般没用留空
    (l&0xFF000000)>>24,(l&0xFF0000)>>16,(l&0xFF00)>>8,l&0xFF
  );
  //返回头部分和数据部分的合并缓冲区
  return Buffer.concat([new Buffer(s),o]);
};

可以把它用于一个实例中

1
2
3
4
5
//客户端程序
var ws=new WebSocket("ws://127.0.0.1:8000/");
ws.onmessage=function(e){
  console.log(e);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//服务器程序
var crypto=require('crypto');
var WS='258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
 
require('net').createServer(function(o){
  var key;
  o.on('data',function(e){
    if(!key){
      //握手
      key=e.toString().match(/Sec-WebSocket-Key: (.+)/)[1];
      key=crypto.createHash('sha1').update(key+WS).digest('base64');
      o.write('HTTP/1.1 101 Switching Protocols\r\n');
      o.write('Upgrade: websocket\r\n');
      o.write('Connection: Upgrade\r\n');
      o.write('Sec-WebSocket-Accept: '+key+'\r\n');
      o.write('\r\n');
      //握手成功后给客户端发送个数据
      o.write(encodeDataFrame({
        FIN:1,Opcode:1,PayloadData:"次碳酸钴"
      }));
    };
  });
}).listen(8000);

上面是最基本的用法。但是有时候数据需要分成多个数据包来发送,这就需要用到分片,也就是使用多个数据帧来传输一个数据。分片传输分为三个部分:
开始帧:FIN=0,Opcode>0;一个
传输帧:FIN=0,Opcode=0;零个或多个
终止帧:FIN=1,Opcode=0;一个
FIN是FINAL的缩写,它为1时表示一个数据传输结束,而开始和传输帧的时候数据都没结束,所以是0,之后最后的结束帧FIN是1。同一个数据即使分片传输,它的每个数据帧的Opcode也应该相同,为了避免冲突,只对分片传输的开始帧设置Opcode,传输帧和结束帧的Opcode留0。因此把上面实例的部分代码改成

1
2
3
4
5
6
7
8
9
10
 //握手成功后给客户端发送个数据
      o.write(encodeDataFrame({
        FIN:0,Opcode:1,PayloadData:"ABC"
      }));
      o.write(encodeDataFrame({
        FIN:0,Opcode:0,PayloadData:"-DEF-"
      }));
      o.write(encodeDataFrame({
        FIN:1,Opcode:0,PayloadData:"GHI"
      }));

就可以在客户端得到

这就是分片传输的关键所在。

转自: https://www.web-tinker.com/article/20307.html

WebSocket(贰) 解析数据帧

知道了怎么握手只是让客户端和服务器建立连接而已,WebSocket真正麻烦的地方是在数据的传输上!为了环保,它使用了特定格式的数据帧,这个数据帧需要自己去解析(当然也有别人编写好的库可以用)。虽然官方文档描述的很详细,但是看起来还是蛋疼。
当客户端向服务器发送一个数据时服务器收到一个数据帧,比如下面的程序

1
2
3
4
5
//客户端程序
var ws=new WebSocket("ws://127.0.0.1:8000");
ws.onopen=function(e){
  ws.send("次碳酸钴"); //发送数据
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//服务器程序
var crypto=require('crypto');
var WS='258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
 
require('net').createServer(function(o){
  var key;
  o.on('data',function(e){
    if(!key){ //握手
      key=e.toString().match(/Sec-WebSocket-Key: (.+)/)[1];
      key=crypto.createHash('sha1').update(key+WS).digest('base64');
      o.write('HTTP/1.1 101 Switching Protocols\r\n');
      o.write('Upgrade: websocket\r\n');
      o.write('Connection: Upgrade\r\n');
      o.write('Sec-WebSocket-Accept: '+key+'\r\n');
      o.write('\r\n');
    }else onmessage(e); //接收并交给处理函数
  });
}).listen(8000);
 
function onmessage(e){
  console.log(e); //把数据输出到控制台
};

这里是直接把接收到的数据输出了,得到这样一个东西

这就是一个完整的数据帧,直接的16进制数据我们当然无法直接阅读,需要按照数据帧的格式把它里面的数据取出来才行。对于这个数据帧,官方文档提供了一个结构图

 0                   1                   2                   3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-------+-+-------------+-------------------------------+
 |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
 |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
 |N|V|V|V|       |S|             |   (if payload len==126/127)   |
 | |1|2|3|       |K|             |                               |
 +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
 |     Extended payload length continued, if payload len == 127  |
 + - - - - - - - - - - - - - - - +-------------------------------+
 |                               |Masking-key, if MASK set to 1  |
 +-------------------------------+-------------------------------+
 | Masking-key (continued)       |          Payload Data         |
 +-------------------------------- - - - - - - - - - - - - - - - +
 :                     Payload Data continued ...                :
 + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
 |                     Payload Data continued ...                |
 +---------------------------------------------------------------+

光拿出这个实在很难看懂,顶部数字用十进制而不是八进制太让人蛋疼了。当然官方文档在后面的描述中也有详细介绍,看完后再回头来看图表才能看明白。其实WebSocket目前还不太完善,很多实验性的东西,所以完全按照官方文档来理解是蛋疼的。这里就说我自己的理解。
现在再看左上角上面的图标,左上角的四个小列,也就是4位,第一位是FIN,后面三位是RSV1到3。官方文档上说RSV是预留的空间,正常为0,这就意味着,正常情况下他们可以当做0填充,那么前4位只有第一位的FIN需要设置,FIN表示帧结束,由于这篇中它不重要就不特别介绍了。接着后面的四位是储存opcode的值,这个opcode是标识数据类型的。这样数据的第一个字节我们就能理解它的含义了,看上面16进制的数据的第一个字节81换成二进制是1000001,第一个1是FIN的值,最后一个1是opcode的值。
接着是第二个字节的数据,它由1位的MASK和7位的PayloadLen组成,MASK标识这个数据帧的数据是否使用掩码,PayloadLen表示数据部分的长度。但是PayloadLen只有7位,换成无符号整型的话只有0到127的取值,这么小的数值当然无法描述较大的数据,因此规定当数据长度小于或等于125时候它才作为数据长度的描述,如果这个值为126,则时候后面的两个字节来储存储存数据长度,如果为127则用后面八个字节来储存数据长度。所以上面的图片第一行的最右侧那块和第二行看起来有些颓然。从我们的示例数据来看,第二个字节的8C中80是最高位为1,这意味着MASK为1,后面的C表示这个数据部分有12个字节。
再接着是上面图表中的MaskingKey,它占四个字节,储存掩码的实体部分。但是只有在前面的MASK被设置为1时候才存在这个数据,否则不使用掩码也就没有这个数据了。看我们的示例数据,由于前面的MASK为1,所以3到6字节的“79 77 3d 41”是数据的掩码实体。
最后是数据部分,如果掩码存在,那么所有数据都需要与掩码做一次异或运算,四个字节的掩码与所有数据字节轮流发生性关系。如果不存在掩码,那么后面的数据就可以直接使用。
这样数据帧就解析完了。下面是我写的数据帧解析的程序,请不要吐槽代码没优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function decodeDataFrame(e){
  var i=0,j,s,frame={
    //解析前两个字节的基本数据
    FIN:e[i]&gt;&gt;7,Opcode:e[i++]&amp;15,Mask:e[i]&gt;&gt;7,
    PayloadLength:e[i++]&amp;0x7F
  };
  //处理特殊长度126和127
  if(frame.PayloadLength==126)
    frame.PayloadLength=(e[i++]&lt;&lt;8)+e[i++];
  if(frame.PayloadLength==127)
    i+=4, //长度一般用四字节的整型,前四个字节通常为长整形留空的
    frame.PayloadLength=(e[i++]&lt;&lt;24)+(e[i++]&lt;&lt;16)+(e[i++]&lt;&lt;8)+e[i++];
  //判断是否使用掩码
  if(frame.Mask){
    //获取掩码实体
    frame.MaskingKey=[e[i++],e[i++],e[i++],e[i++]];
    //对数据和掩码做异或运算
    for(j=0,s=[];j&lt;frame.PayloadLength;j++)
      s.push(e[i+j]^frame.MaskingKey[j%4]);
  }else s=e.slice(i,i+frame.PayloadLength); //否则直接使用数据
  //数组转换成缓冲区来使用
  s=new Buffer(s);
  //如果有必要则把缓冲区转换成字符串来使用
  if(frame.Opcode==1)s=s.toString();
  //设置上数据部分
  frame.PayloadData=s;
  //返回数据帧
  return frame;
};

既然有了解析程序,那么我们就可以把上面实例服务器端的onmessage方法修改一下

1
2
3
4
function onmessage(e){
  e=decodeDataFrame(e); //解析数据帧
  console.log(e); //把数据帧输出到控制台
};

这样服务器接收客户端穿过了的数据就没问题了。嘛,这篇文章就只说接收,至于从服务器发送到客户的情况会有更复杂的情况出现,咱下一篇再说。

转自: https://www.web-tinker.com/article/20306.html

WebSocket(壹) 握手连接

  WebSocket虽然很先进,很好用,但却是个很麻烦的东西。与普通的Web通信机制不同,它本身可以算是一个协议。要使用WebSocket首先得让客户端和服务器建立连接,而且这个连接蛋疼的比TCP那样的传输层协议还复杂,需要通过验证KEY来做握手工作。
这个握手协议使用的是HTTP格式的请求,并再头部分带上一个Sec-WebSocket-Key字段,服务器对这个字段加上一个特定的字符串后做一次sha1运算,然后把结果用Base64的形式以同样的方式发送回去就可以完成握手的工作了。当然,WebSocket这个协议还在不断完善更新,目前这么用是没问题的,历史版本中还出现过其它方式,这里就无视他们了。
下面是足以完成握手的服务器端程序,使用NodeJS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var crypto=require('crypto');
var WS='258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
 
require('net').createServer(function(o){
var key;
o.on('data',function(e){
if(!key){
//获取发送过来的KEY
key=e.toString().match(/Sec-WebSocket-Key: (.+)/)[1];
//连接上WS这个字符串,并做一次sha1运算,最后转换成Base64
key=crypto.createHash('sha1').update(key+WS).digest('base64');
//输出返回给客户端的数据,这些字段都是必须的
o.write('HTTP/1.1 101 Switching Protocols\r\n');
o.write('Upgrade: websocket\r\n');
o.write('Connection: Upgrade\r\n');
//这个字段带上服务器处理后的KEY
o.write('Sec-WebSocket-Accept: '+key+'\r\n');
//输出空行,使HTTP头结束
o.write('\r\n');
}else{
//数据处理
};
});
}).listen(8000);

客户端一段简单的代码即可

1
2
3
4
5
6
7
var ws=new WebSocket("ws://127.0.0.1:8000");
ws.onerror=function(e){
  console.log(e);
};
ws.onopen=function(){
  console.log("握手成功");
};

一旦握手成功就会触发open事件,否则触发error事件。在open之后就可以进行数据传输了,至于数据要如何传输这比这个握手连接要复杂的多,咱下一篇再说。

转自: https://www.web-tinker.com/article/20305.html