一直以来我自己都是用 nginx 作为自己的web服务后端。这两天帮朋友搭建服务,觉得 nginx 太过麻烦,不适合快速实现,所以调研了一下 Caddy2 觉得用起还算比较方便,所以就用 Caddy2 帮朋友把服务搭建好了。帮朋友搭建完成后,配置一了下,觉得自己的服务也可以用 Caddy2 来实现,所以就趁机把服务器切换成 Caddy2 了。

需求背景

对于 web 服务有以下几个需求:

  1. 支持 https

  2. 支持代理转发

  3. 支持 webdav

  4. 支持 letsencrypt 证书自动获取

实现这几个功能,那么web服务的功能基本就齐全了。还有一点也是本次改造重点关注的地方,那就是可以实现简单的快速部署。

Nginx存在的问题

对于 nginx 来说,主要有以下几个问题:

  1. 首先 nginx 不能自动申请 letsencrypt 证书,我的解决方案是通过 lego 来定时获取的。

  2. 其次,默认的 nginxwebdav 命令支持不全,如果有需要,还要额外编译 ngx-dav-ext-module

  3. 第三,还有就是 nginxwebdav 模块处理路径最后 ’/‘ 时,还有点小bug。所以使用起来不是很方便。

  4. 最后,nginx配置文件写起来真的很麻烦。

申请证书

因为 nginx 本身不能申请证书,所以申请证书的工作是通过 lego 来实现的。而 lego 的使用也有很多麻烦的地方。

lego 命令本身使用起来非常复杂,为了使用方便需要封装一个脚本方便调用。

  #!/bin/bash

  lego_bin=/path/to/lego
  data=/path/to/letsencrypt
  domain="*.leenzhu.com"
  log=/path/to/lego.log
  account_email="a@b.com"

  dns_provider="alidns"
  export ALICLOUD_ACCESS_KEY=<ali-acc>
  export ALICLOUD_SECRET_KEY=<ali-sec>

  case $1 in

  run)
      $lego_bin --email="$account_email" --domains="$domain" --accept-tos --path=$data --dns="$dns_provider" run --run-hook="`readlink -f $0` reload"  >> $log
      ;;

  renew)
      for domain in `$lego_bin  --path=$data list -n` ; do
          $lego_bin --email="$account_email" --domains="$domain" --accept-tos --path=$data --dns="$dns_provider" renew --reuse-key --renew-hook="`readlink -f $0` reload" >> $log
      done
      ;;

  reload)
      #cp -f $data/certificates/*.crt /usr/local/openresty/nginx/conf/vhosts/keys/
      #cp -f $data/certificates/*.key /usr/local/openresty/nginx/conf/vhosts/keys/
      #rm -rf /usr/local/openresty/nginx/conf/vhosts/keys/*.issuer.crt
      #/usr/local/openresty/bin/openresty -s reload
      sudo /usr/sbin/nginx -s reload

      ;;

  ,*)
     echo "黙认签发泛域名证书,只写根域名即可"
     echo "证书保存在 $data"
     echo "用法: 1. 签发证书 $0 run nixops.me"
     echo "用法: 2. 续签证书 $0 renew "
     echo "用法: 3. 复制证书,reload服务 $0 reload"

     ;;
  esac

因为证书需要定时签发,所以需要把 lego 做成一个定时服务,通过 systemd 来实现定时任务,就需要编写两个文件,一个是 lego.service

  [Unit]
  Description=Lego for let's encrypt auto issue cert

  [Service]
  Type=oneshot
  ExecStart=/path/to/lego.sh renew

一个是 lego.timer

  [Unit]
  Description=Daily check for lego.service

  [Timer]
  OnCalendar=daily
  AccuracySec=15m
  Persistent=true

  [Install]
  WantedBy=timers.target

dav配置

以下配置记不得是从哪抄来的,虽然知道每配置的意思,但是不知道为什么要配置这么多。

  location / {
      root    /path/to/site/root;

      add_header 'Access-Control-Allow-Origin' '*' always;
      add_header 'Access-Control-Allow-Credentials' 'true' always;
      add_header 'Access-Control-Allow-Methods' 'GET, HEAD, POST, PUT, OPTIONS, MOVE, DELETE, COPY, LOCK, UNLOCK' always;
      add_header 'Access-Control-Allow-Headers' 'Authorization,DNT,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,X-Accept-Charset,X-Accept,origin,accept,if-match,destination,overwrite' always;
      add_header 'Access-Control-Expose-Headers' 'ETag' always;
      add_header 'Access-Control-Max-Age' 1728000 always;

      if ($request_method = 'OPTIONS') {
        add_header 'Content-Type' 'text/plain charset=UTF-8';
        add_header 'Content-Length' 0;
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Credentials' 'true';
        add_header 'Access-Control-Allow-Methods' 'GET, HEAD, POST, PUT, OPTIONS, MOVE, DELETE, COPY, LOCK, UNLOCK';
        add_header 'Access-Control-Allow-Headers' 'Authorization,DNT,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,X-Accept-Charset,X-Accept,origin,accept,if-match,destination,overwrite';
        add_header 'Access-Control-Expose-Headers' 'ETag';
        add_header 'Access-Control-Max-Age' 1728000;
        return 204;
      }

      autoindex on;
      dav_methods PUT DELETE MKCOL COPY MOVE;
      dav_ext_methods PROPFIND OPTIONS;
      create_full_put_path  on;
      dav_access user:rw group:rw all:rw;
      auth_basic "Authorized Users Only";
      auth_basic_user_file /path/to/htpasswd;

      # MKCOL不以/结尾
      if ($request_method = MKCOL) { rewrite ^(.*[^/])$ $1/ break; }

      # Windows下MOVE文件夹不以/结尾
      if (-d $request_filename) {
          rewrite ^(.*[^/])$ $1/;
          set $md /;
      }

      # 重命名文件夹Destination不以/结尾,需要headers-more-nginx-module
      set $x $http_destination$request_method;
      if ($x ~ [^/]MOVE) {
          more_set_input_headers -r "Destination: ${http_destination}${md}";
      }

      #没有PROPPATCH指令,用PROPFIND处理。
      proxy_method PROPFIND;
      include proxy_params;
      proxy_set_header Host "webdav.leenzhu.com";
      if ($request_method = PROPPATCH) {
          proxy_pass https://127.0.0.1;
      }

  }

dav bug处理

nginxwebdav 主要有3处bug:

  1. MKCOL不以 '/' 结尾

  2. Windows下MOVE文件夹不以 '/' 结尾

  3. 重命名文件夹 Destination 不以 '/' 结尾,需要 headers-more-nginx-module

  4. 没有PROPPATCH指令,用PROPFIND处理。

  # MKCOL不以/结尾
  if ($request_method = MKCOL) { rewrite ^(.*[^/])$ $1/ break; }

  # Windows下MOVE文件夹不以/结尾
  if (-d $request_filename) {
      rewrite ^(.*[^/])$ $1/;
      set $md /;
  }

  # 重命名文件夹Destination不以/结尾,需要headers-more-nginx-module
  set $x $http_destination$request_method;
  if ($x ~ [^/]MOVE) {
      more_set_input_headers -r "Destination: ${http_destination}${md}";
  }

  #没有PROPPATCH指令,用PROPFIND处理。
  proxy_method PROPFIND;
  include proxy_params;
  proxy_set_header Host "webdav.leenzhu.com";
  if ($request_method = PROPPATCH) {
      proxy_pass https://127.0.0.1;
  }

nginx 配置示例

对于一个代理转发,nginx至少要配置以下的内容,如果有10个服务要配置,那么下面的代码就要写10遍。

  server {
          listen 443 ssl;
          server_name rss.leenzhu.com;
          location / {
              proxy_set_header Host $http_host;
              proxy_pass http://localhost:8280;
          }
  }

遇到 websocket 代理,上述配置并不能正常工作,需要参考如下配置:

  server {
      listen 443 ssl;
      server_name sy.leenzhu.com;
      location / {
          proxy_http_version 1.1;
          proxy_set_header Upgrade $http_upgrade;
          proxy_set_header Connection "Upgrade";
          proxy_pass http://localhost:6806;
      }
  }

Caddy2 实现

Caddy2 安装

因为 caddy2 提供了编译工具 xcaddy ,所以定制编译 caddy2 比较简单:

  export GOPROXY=https://goproxy.cn
  go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
  xcaddy build v2.6.2 \
  --with github.com/mholt/caddy-webdav \
  --with github.com/caddy-dns/alidns \
  --with github.com/caddy-dns/cloudflare

Caddy2 配置

Caddy2 配置需要进行以下处理:

  1. 全局配置

  2. 泛域名证书获取

  3. 子域名共享泛域名证书

  4. 本机目录静态资源访问

  5. 本机反向代理配置

  6. 跨机器反向代理配置

  7. 本机静态资源混合反向代理实现

  8. webdav配置

    1. webdav 用户认证配置

    2. webdav 多用户配置

    3. 不同用户不同目录配置

    4. webdav 目录浏览支持

  {
      http_port 80
      https_port 443
      email i@leenzhu.com
  }

  (local) {
      @{args.0} host {args.0}.{labels.1}.{labels.0}
      handle @{args.0} {
          root * /path/to/caddy/www/{args.0}.{labels.1}.{labels.0}
      }
  }

  (host) {
      @{args.0} host {args.0}.{labels.1}.{labels.0}
  }

  (fwd) {
      @{args.0} host {args.0}.{labels.1}.{labels.0}
      handle @{args.0} {
          reverse_proxy 127.0.0.1:{args.1}
      }
  }

  (fwdex) {
      @{args.0} host {args.0}.{labels.1}.{labels.0}
      handle @{args.0} {
          reverse_proxy @{args.0} {args.1}
      }
  }
  https://*.leenzhu.com {
      tls {
          dns alidns {
              access_key_id {env.ALIYUN_ACCESS_KEY_ID}
              access_key_secret {env.ALIYUN_ACCESS_KEY_SECRET}
          }
      }

      import fwd git 3000

      import fwdex say 192.168.50.100:8086

      import local keeweb

      import host sync
      handle @sync {
          reverse_proxy 127.0.0.1:8384 {
              header_up Host {upstream_hostport}
              header_up X-Forwarded-Host {host}
          }
     }

      import host down
      handle @down {
          reverse_proxy /jsonrpc 127.0.0.1:6800
          file_server {
              root /path/to/caddy/www/down.leenzhu.com
          }
      }

      import host webdav
      handle @webdav {
          basicauth {
              user1 xxxxxxxxxxxxxx
              user2 xxxxxxxxxxxxxx
          }
          route {
              @get method GET
              root * /path/to/caddy/www/webdav.leenzhu.com/{http.auth.user.id}
              file_server @get browse
              webdav
          }
      }
      handle {
          abort
      }
  }

总结

  1. caddy2 安装部署过程还算顺利,没有遇到卡了很久的问题 。

  2. caddy2import 功能可以简化不少配置代码的编写。

  3. webdav 不能控制读写权限,需要通过系统文件权限来控制

  4. caddy2 默认反向代理会默认传递原始请求 Host 值给后端,这点与 nginx 相反,在部署 syntching 服务时,需要通过 header_up 指令修正。