├── .chglog ├── CHANGELOG.tpl.md └── config.yml ├── .github └── workflows │ ├── lint.yml │ └── tests.yml ├── .gitignore ├── .luacheckrc ├── CHANGELOG.md ├── Makefile ├── README.md ├── dist.ini ├── lib └── resty │ └── acme │ ├── autossl.lua │ ├── challenge │ ├── dns-01.lua │ ├── http-01.lua │ └── tls-alpn-01.lua │ ├── client.lua │ ├── dns_provider │ ├── cloudflare.lua │ ├── dnspod-intl.lua │ └── dynv6.lua │ ├── eab │ └── zerossl-com.lua │ ├── openssl.lua │ ├── storage │ ├── README.md │ ├── consul.lua │ ├── etcd.lua │ ├── file.lua │ ├── redis.lua │ ├── shm.lua │ └── vault.lua │ └── util.lua ├── lua-resty-acme-0.15.0-1.rockspec ├── scripts └── prepare_new_release.sh └── t ├── autossl.t ├── e2e.t ├── fixtures ├── docker-compose.yml ├── multiple_certs.pem ├── pebble.minica.pem ├── prepare_env.sh └── serviceaccount.jwt ├── openssl.t ├── storage ├── consul.t ├── etcd.t ├── file.t ├── redis.t ├── shm.t ├── vault.t ├── vault_kubernetes.t └── vault_tls.t └── util.t /.chglog/CHANGELOG.tpl.md: -------------------------------------------------------------------------------- 1 | {{ if .Versions -}} 2 | 3 | ## [Unreleased] 4 | 5 | {{ if .Unreleased.CommitGroups -}} 6 | {{ range .Unreleased.CommitGroups -}} 7 | ### {{ .Title | lower }} 8 | {{ range .Commits -}} 9 | - {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} 10 | {{ end }} 11 | {{ end -}} 12 | {{ end -}} 13 | {{ end -}} 14 | 15 | {{ range .Versions }} 16 | 17 | ## {{ if .Tag.Previous }}[{{ .Tag.Name }}]{{ else }}{{ .Tag.Name }}{{ end }} - {{ datetime "2006-01-02" .Tag.Date }} 18 | {{ range .CommitGroups -}} 19 | ### {{ lower .Title }} 20 | {{ range .Commits -}} 21 | - {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} [{{ .Hash.Short }}]({{ $.Info.RepositoryURL }}/commit/{{ .Hash.Long }}) 22 | {{ end }} 23 | {{ end -}} 24 | 25 | {{- if .NoteGroups -}} 26 | {{ range .NoteGroups -}} 27 | ### {{ .Title }} 28 | {{ range .Notes }} 29 | {{ .Body }} 30 | {{ end }} 31 | {{ end -}} 32 | {{ end -}} 33 | {{ end -}} 34 | 35 | {{- if .Versions }} 36 | [Unreleased]: {{ .Info.RepositoryURL }}/compare/{{ $latest := index .Versions 0 }}{{ $latest.Tag.Name }}...HEAD 37 | {{ range .Versions -}} 38 | {{ if .Tag.Previous -}} 39 | [{{ .Tag.Name }}]: {{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }} 40 | {{ end -}} 41 | {{ end -}} 42 | {{ end -}} 43 | -------------------------------------------------------------------------------- /.chglog/config.yml: -------------------------------------------------------------------------------- 1 | style: github 2 | template: CHANGELOG.tpl.md 3 | info: 4 | title: CHANGELOG 5 | repository_url: https://github.com/fffonion/lua-resty-acme 6 | options: 7 | sort: "semver" 8 | commits: 9 | filters: 10 | Type: 11 | - feat 12 | - fix 13 | - refactor 14 | - perf 15 | commit_groups: 16 | title_maps: 17 | feat: Features 18 | fix: Bug Fixes 19 | perf: Performance Improvements 20 | refactor: Code Refactoring 21 | header: 22 | pattern: "^(\\w*)(?:\\(([\\w\\$\\.\\-\\*\\s\\/]*)\\))?:?\\s(.*)$" 23 | pattern_maps: 24 | - Type 25 | - Scope 26 | - Subject 27 | notes: 28 | keywords: 29 | - BREAKING CHANGE 30 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | paths: 6 | - lib/**.lua 7 | pull_request: 8 | paths: 9 | - lib/**.lua 10 | 11 | jobs: 12 | tests: 13 | name: Lint 14 | runs-on: ubuntu-22.04 15 | 16 | steps: 17 | - name: Checkout source code 18 | uses: actions/checkout@v2 19 | - uses: Jayrgo/luacheck-action@v1 20 | name: luacheck 21 | with: 22 | # List of files, directories and rockspecs to check. 23 | # Default: . 24 | files: 'lib' 25 | 26 | # Path to configuration file. 27 | # Default: .luacheckrc 28 | config: '.luacheckrc' 29 | 30 | # Arguments passed to luacheck. 31 | # Default: -q 32 | args: '-q' 33 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | # ignore top-level markdown files (CHANGELOG.md, README.md, etc.) 7 | - '*.md' 8 | branches: 9 | - master 10 | - release/* 11 | - test-please/* 12 | - use-atomic-set 13 | pull_request: 14 | paths-ignore: 15 | # ignore top-level markdown files (CHANGELOG.md, README.md, etc.) 16 | - '*.md' 17 | schedule: 18 | - cron: '0 7 * * *' 19 | 20 | # cancel previous runs if new commits are pushed to the PR, but run for each commit on master 21 | concurrency: 22 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 23 | cancel-in-progress: true 24 | 25 | jobs: 26 | tests: 27 | name: Tests 28 | runs-on: ubuntu-22.04 29 | 30 | strategy: 31 | matrix: 32 | include: 33 | - nginx: "1.25.3" 34 | openssl: "1.1.1w" 35 | lua_nginx_module: "v0.10.26" 36 | stream_lua_nginx_module: "v0.0.14" 37 | lua_resty_core: "v0.1.28" 38 | - nginx: "1.25.3" 39 | openssl: "3.0.15" 40 | lua_nginx_module: "v0.10.26" 41 | stream_lua_nginx_module: "v0.0.14" 42 | lua_resty_core: "v0.1.28" 43 | - nginx: "1.25.3" 44 | openssl: "3.1.7" 45 | lua_nginx_module: "v0.10.26" 46 | stream_lua_nginx_module: "v0.0.14" 47 | lua_resty_core: "v0.1.28" 48 | - nginx: "1.25.3" 49 | openssl: "3.2.3" 50 | lua_nginx_module: "v0.10.26" 51 | stream_lua_nginx_module: "v0.0.14" 52 | lua_resty_core: "v0.1.28" 53 | - nginx: "1.25.3" 54 | openssl: "3.3.2" 55 | lua_nginx_module: "v0.10.26" 56 | stream_lua_nginx_module: "v0.0.14" 57 | lua_resty_core: "v0.1.28" 58 | - nginx: "1.25.3" 59 | openssl: "3.4.0" 60 | lua_nginx_module: "v0.10.26" 61 | stream_lua_nginx_module: "v0.0.14" 62 | lua_resty_core: "v0.1.28" 63 | 64 | env: 65 | JOBS: 3 66 | SH: bash 67 | NGX_BUILD_JOBS: 3 68 | BASE_PATH: /home/runner/work/cache 69 | LUAJIT_PREFIX: /home/runner/work/cache/luajit21 70 | LUAJIT_LIB: /home/runner/work/cache/luajit21/lib 71 | LUAJIT_INC: /home/runner/work/cache/luajit21/include/luajit-2.1 72 | LUA_INCLUDE_DIR: /home/runner/work/cache/luajit21/include/luajit-2.1 73 | OPENSSL_PREFIX: /home/runner/work/cache/ssl 74 | OPENSSL_LIB: /home/runner/work/cache/ssl/lib 75 | OPENSSL_INC: /home/runner/work/cache/ssl/include 76 | TEST_NGINX_SLEEP: 0.005 77 | TEST_NGINX_RANDOMIZE: 1 78 | LUACHECK_VER: 0.21.1 79 | CC: gcc 80 | NGX_BUILD_CC: gcc 81 | 82 | NGINX_CC_OPTS: "" 83 | LUAJIT_CC_OPTS: "" 84 | 85 | services: 86 | # Redis with auth disabled 87 | redis: 88 | image: redis 89 | # Set health checks to wait until redis has started 90 | options: >- 91 | --health-cmd "redis-cli ping" 92 | --health-interval 10s 93 | --health-timeout 5s 94 | --health-retries 5 95 | ports: 96 | - 6379:6379 97 | # Redis with auth enabled 98 | redis-auth: 99 | image: redis/redis-stack-server 100 | # Set health checks to wait until redis has started 101 | options: >- 102 | --health-cmd "redis-cli ping" 103 | --health-interval 10s 104 | --health-timeout 5s 105 | --health-retries 5 106 | ports: 107 | - 6380:6379 108 | env: 109 | REDIS_ARGS: "--requirepass passdefault" 110 | 111 | steps: 112 | - name: Checkout source code 113 | uses: actions/checkout@v2 114 | 115 | - name: Setup cache 116 | uses: actions/cache@v2 117 | with: 118 | path: | 119 | /home/runner/work/cache 120 | key: ${{ runner.os }}-${{ hashFiles('**/tests.yml') }}-nginx-${{ matrix.nginx }}-openssl-${{ matrix.openssl }} 121 | 122 | - name: Prepare environment 123 | run: | 124 | bash t/fixtures/prepare_env.sh 125 | 126 | - name: Setup tools 127 | run: | 128 | sudo apt-get install -qq -y cpanminus axel ca-certificates 129 | mkdir -p $OPENSSL_PREFIX $LUAJIT_PREFIX 130 | # perl cache 131 | pushd /home/runner/work/cache 132 | if [ ! -e perl ]; then sudo cpanm --notest Test::Nginx > build.log 2>&1 || (cat build.log && exit 1); cp -r /usr/local/share/perl/ .; else sudo cp -r perl /usr/local/share; fi 133 | # build tools at parent directory of cache 134 | cd .. 135 | git clone https://github.com/openresty/openresty.git ./openresty 136 | git clone https://github.com/openresty/nginx-devel-utils.git 137 | git clone https://github.com/simpl/ngx_devel_kit.git ./ndk-nginx-module 138 | git clone https://github.com/openresty/lua-nginx-module.git ./lua-nginx-module -b ${{ matrix.lua_nginx_module }} 139 | git clone https://github.com/openresty/stream-lua-nginx-module.git ./stream-lua-nginx-module -b ${{ matrix.stream_lua_nginx_module }} 140 | git clone https://github.com/openresty/no-pool-nginx.git ./no-pool-nginx 141 | # lua libraries at parent directory of current repository 142 | popd 143 | mkdir ../lib 144 | git clone https://github.com/openresty/lua-resty-core.git ../lua-resty-core -b ${{ matrix.lua_resty_core }} 145 | git clone https://github.com/openresty/lua-resty-lrucache.git ../lua-resty-lrucache 146 | git clone https://github.com/openresty/lua-resty-redis.git ../lua-resty-redis 147 | git clone -b v0.15 https://github.com/ledgetech/lua-resty-http ../lua-resty-http 148 | git clone https://github.com/fffonion/lua-resty-openssl ../lua-resty-openssl 149 | git clone -b 0.3.0 https://github.com/spacewander/luafilesystem ../luafilesystem-ffi 150 | git clone https://github.com/jkeys089/lua-resty-hmac ../lua-resty-hmac && pushd ../lua-resty-hmac && git checkout 79a4929 && popd 151 | git clone https://github.com/iresty/lua-typeof ../lua-typeof 152 | git clone https://github.com/api7/lua-resty-etcd ../lua-resty-etcd -b v1.4.4 153 | cp -r ../lua-resty-lrucache/lib/* ../lua-resty-redis/lib/* ../lua-resty-http/lib/* ../lua-resty-openssl/lib/* ../lua-typeof/lib/* ../lua-resty-etcd/lib/* ../lib/ 154 | cp ../luafilesystem-ffi/lfs_ffi.lua ../lib/ 155 | find ../lib 156 | 157 | - name: Build OpenSSL 158 | run: | 159 | if [ "X$OPENSSL_HASH" != "X" ]; then wget https://github.com/openssl/openssl/archive/$OPENSSL_HASH.tar.gz -O - | tar zxf ; pushd openssl-$OPENSSL_HASH/; fi 160 | if [ "X$OPENSSL_HASH" = "X" ] ; then (wget https://github.com/openssl/openssl/releases/download/openssl-${{ matrix.openssl }}/openssl-${{ matrix.openssl }}.tar.gz -qO - || wget https://openssl.org/source/old/1.1.1/openssl-${{ matrix.openssl}}.tar.gz -qO -)| tar zxf -; pushd openssl-${{ matrix.openssl }}/; fi 161 | if [ ! -e $OPENSSL_PREFIX/include ]; then ./config shared -d --prefix=$OPENSSL_PREFIX -DPURIFY > build.log 2>&1 || (cat build.log && exit 1); fi 162 | if [ ! -e $OPENSSL_PREFIX/include ]; then make -j$JOBS > build.log 2>&1 || (cat build.log && exit 1); fi 163 | if [ ! -e $OPENSSL_PREFIX/include ]; then sudo make PATH=$PATH install_sw > build.log 2>&1 || (cat build.log && exit 1); fi 164 | mkdir -p $OPENSSL_PREFIX/certs/ && sudo cp -r /etc/ssl/certs/* $OPENSSL_PREFIX/certs/ 165 | 166 | - name: Build LuaJIT 167 | run: | 168 | cd $LUAJIT_PREFIX 169 | if [ ! -e luajit2 ]; then git clone -b v2.1-agentzh https://github.com/openresty/luajit2.git; fi 170 | cd luajit2 171 | make -j$JOBS CCDEBUG=-g Q= PREFIX=$LUAJIT_PREFIX CC=$CC XCFLAGS="-DLUA_USE_APICHECK -DLUA_USE_ASSERT -DLUAJIT_ENABLE_LUA52COMPAT $LUAJIT_CC_OPTS" > build.log 2>&1 || (cat build.log && exit 1) 172 | make install PREFIX=$LUAJIT_PREFIX > build.log 2>&1 || (cat build.log && exit 1) 173 | 174 | - name: Build lua-cjson 175 | run: | 176 | if [ ! -e lua-cjson ]; then git clone https://github.com/openresty/lua-cjson.git ./lua-cjson; fi 177 | pushd ./lua-cjson && make && sudo PATH=$PATH make install && popd 178 | 179 | - name: Build Nginx 180 | run: | 181 | export PATH=$BASE_PATH/work/nginx/sbin:$BASE_PATH/../nginx-devel-utils:$PATH 182 | export LD_LIBRARY_PATH=$LUAJIT_LIB:$LD_LIBRARY_PATH 183 | cd $BASE_PATH 184 | if [ ! -e work ]; then ngx-build ${{ matrix.nginx }} --add-module=../ndk-nginx-module --add-module=../lua-nginx-module --add-module=../stream-lua-nginx-module --with-http_ssl_module --with-stream --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt="-I$OPENSSL_INC $NGINX_CC_OPTS" --with-ld-opt="-L$OPENSSL_LIB -Wl,-rpath,$OPENSSL_LIB" --with-debug > build.log 2>&1 || (cat build.log && exit 1); fi 185 | nginx -V 186 | ldd `which nginx`|grep -E 'luajit|ssl|pcre' 187 | 188 | - name: Run Tests 189 | run: | 190 | export LD_LIBRARY_PATH=$LUAJIT_LIB:$LD_LIBRARY_PATH 191 | export PATH=$BASE_PATH/work/nginx/sbin:$PATH 192 | 193 | TEST_NGINX_TIMEOUT=60 prove -j$JOBS -r t/ 194 | 195 | - name: Show logs 196 | if: failure() 197 | run: | 198 | pushd t/fixtures 199 | docker compose logs 200 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | t/servroot 2 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | std = "ngx_lua" 2 | unused_args = false 3 | redefined = false 4 | max_line_length = false 5 | 6 | 7 | not_globals = { 8 | "string.len", 9 | "table.getn", 10 | } 11 | 12 | 13 | ignore = { 14 | "6.", -- ignore whitespace warnings 15 | } 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## [Unreleased] 3 | 4 | 5 | 6 | ## [0.15.0] - 2024-08-14 7 | ### bug fixes 8 | - **tests:** use tlsv1.2 in dual cert test [415be3f](https://github.com/fffonion/lua-resty-acme/commit/415be3fe2a5bfcc3cd6aac5ab8a736f0a672475c) 9 | - **tests:** uses v3 protocol for etcd [c3928b5](https://github.com/fffonion/lua-resty-acme/commit/c3928b5e92dd66e9a22d497935a878b59cb26b36) 10 | 11 | ### features 12 | - **etcd:** etcd storage to use v3 protocol [a3353b3](https://github.com/fffonion/lua-resty-acme/commit/a3353b3b26b4cb0c17e98dd36f829a0db18e4ef7) 13 | - **redis:** add support for username/password auth ([#121](https://github.com/fffonion/lua-resty-acme/issues/121)) [186ab23](https://github.com/fffonion/lua-resty-acme/commit/186ab2367c66725b6a38a8f81743328e9a4455e3) 14 | 15 | 16 | 17 | ## [0.14.0] - 2024-05-28 18 | ### bug fixes 19 | - ***:** provide better robustness when waiting for DNS propagation [3ce2614](https://github.com/fffonion/lua-resty-acme/commit/3ce261462ff91deda67bd541fcd35ad924169a36) 20 | - ***:** cleanup API for dns-01 challenge [a1b43f1](https://github.com/fffonion/lua-resty-acme/commit/a1b43f1a7980ee4f88a3cfe3ab7b1bd5a46471be) 21 | 22 | ### features 23 | - ***:** support dns-01 challenge [67a5711](https://github.com/fffonion/lua-resty-acme/commit/67a5711d6e1bd0f36735fd4ffcf53141bb73a0f6) 24 | - **autossl:** support create wildcard cert in SAN [8ed36c3](https://github.com/fffonion/lua-resty-acme/commit/8ed36c3a959a759356c608a4f385a5dcc3f887df) 25 | - **dns-01:** add dnspod-intl provider [0c12f89](https://github.com/fffonion/lua-resty-acme/commit/0c12f89f3d54f1f935fd12a0a148a7aa136dd482) 26 | - **dns-01:** add cloudflare and dynv6 DNS provider [be1a27a](https://github.com/fffonion/lua-resty-acme/commit/be1a27a5f82fcb0dd7105be04f816427655a06ca) 27 | 28 | 29 | 30 | ## [0.13.0] - 2024-03-28 31 | ### bug fixes 32 | - **autossl:** log the errors on the list certificates request ([#110](https://github.com/fffonion/lua-resty-acme/issues/110)) [6c9760f](https://github.com/fffonion/lua-resty-acme/commit/6c9760f21d38fccd7971a70019afc5fe1fc6f1be) 33 | 34 | ### features 35 | - **autossl:** add option to delete none whitelisted domains in certificate renewal ([#112](https://github.com/fffonion/lua-resty-acme/issues/112)) [1bbf39c](https://github.com/fffonion/lua-resty-acme/commit/1bbf39c84de90a54a3f61b3ee2e331e613eb5e7a) 36 | 37 | 38 | 39 | ## [0.12.0] - 2023-09-05 40 | ### bug fixes 41 | - **tests:** use hashicorp published docker images [78eb7bf](https://github.com/fffonion/lua-resty-acme/commit/78eb7bf375834a8161622588dc36702573cafd64) 42 | 43 | ### features 44 | - **autossl:** add redis namespace constraint ([#104](https://github.com/fffonion/lua-resty-acme/issues/104)) [7f92be9](https://github.com/fffonion/lua-resty-acme/commit/7f92be931e2da6c03689bb9fc4f40b3d34ca8384) 45 | 46 | ### performance improvements 47 | - **storage:** replace redis keys call with scan ([#106](https://github.com/fffonion/lua-resty-acme/issues/106)) [d806c19](https://github.com/fffonion/lua-resty-acme/commit/d806c19602313f80c724ad91d4797c63c826ff13) 48 | 49 | 50 | 51 | ## [0.11.0] - 2023-03-24 52 | ### features 53 | - **redis:** support redis namespace ([#101](https://github.com/fffonion/lua-resty-acme/issues/101)) [9c59736](https://github.com/fffonion/lua-resty-acme/commit/9c5973628476db3535223f27717d06b8cdfb30bf) 54 | 55 | 56 | 57 | ## [0.10.1] - 2022-12-06 58 | ### bug fixes 59 | - **zerossl:** concatenate response body as string instead of table ([#98](https://github.com/fffonion/lua-resty-acme/issues/98)) [986b1db](https://github.com/fffonion/lua-resty-acme/commit/986b1dbde6c7cc8261d10d5e8c65942e72eb9a32) 60 | 61 | 62 | 63 | ## [0.10.0] - 2022-11-18 64 | ### features 65 | - **autossl:** expose function to get cert from LRU cache ([#96](https://github.com/fffonion/lua-resty-acme/issues/96)) [6135d0e](https://github.com/fffonion/lua-resty-acme/commit/6135d0e3ccc31f58193af1f49ec6fcdd9f45d6da) 66 | - **autossl:** better cache handling in blocking mode [40f5d2d](https://github.com/fffonion/lua-resty-acme/commit/40f5d2d679a684eab81ccb4fcd1282a4255d8c37) 67 | - **autossl:** fix behavior change in non blocking mode [aa484cc](https://github.com/fffonion/lua-resty-acme/commit/aa484ccc0ecd7ee1db4162c46feb9617776e0907) 68 | - **autossl:** move chains set condition back inside the main loop [b83a535](https://github.com/fffonion/lua-resty-acme/commit/b83a53521d967d9c0f7f2e990ba920734eb27b0f) 69 | - **autossl:** add blocking mode [5a623a5](https://github.com/fffonion/lua-resty-acme/commit/5a623a5d975341aadbf8d09d23cca24156178374) 70 | 71 | 72 | 73 | ## [0.9.0] - 2022-10-27 74 | ### features 75 | - **redis:** support redis ssl ([#86](https://github.com/fffonion/lua-resty-acme/issues/86)) [0aa3568](https://github.com/fffonion/lua-resty-acme/commit/0aa35681fe4357248e21676db122551d7fc20020) 76 | 77 | 78 | 79 | ## [0.8.2] - 2022-10-20 80 | ### bug fixes 81 | - **autossl:** fallback to stale cache if storage is not accessible ([#82](https://github.com/fffonion/lua-resty-acme/issues/82)) [9a4e190](https://github.com/fffonion/lua-resty-acme/commit/9a4e190a966a766eb69aa7a090d7ca2c72cd33de) 82 | - **autossl:** skip the execution of check_renew function on Nginx worker shutdown ([#79](https://github.com/fffonion/lua-resty-acme/issues/79)) [bed9c35](https://github.com/fffonion/lua-resty-acme/commit/bed9c350ea78a58666c8eeac548cb7da0967d190) 83 | 84 | 85 | 86 | ## [0.8.1] - 2022-07-28 87 | ### bug fixes 88 | - **client:** skip checking eab_handler if eab_kid and eab_hmac_key is set ([#71](https://github.com/fffonion/lua-resty-acme/issues/71)) [6004738](https://github.com/fffonion/lua-resty-acme/commit/6004738222718c678d612afb61a0a428ee25fdb4) 89 | 90 | ### features 91 | - **storage:** add Vault namespace ([#63](https://github.com/fffonion/lua-resty-acme/issues/63)) [e241933](https://github.com/fffonion/lua-resty-acme/commit/e24193396af96900f3436dc55c5b0be98bc2ccca) 92 | 93 | 94 | 95 | ## [0.8.0] - 2022-04-08 96 | ### bug fixes 97 | - **tests:** allow to try infinitely for bad nonce in CI [0170c6d](https://github.com/fffonion/lua-resty-acme/commit/0170c6d2fe29128dfcf29c56b378f7a3dd9319b6) 98 | - **tls-alpn-01:** delegate resty.ssl to set alpns [c59098b](https://github.com/fffonion/lua-resty-acme/commit/c59098b4686c63171502606985526d8452fdd8a2) 99 | 100 | ### features 101 | - **autossl:** add certificate renewal cooloff period ([#59](https://github.com/fffonion/lua-resty-acme/issues/59)) [9255220](https://github.com/fffonion/lua-resty-acme/commit/9255220e17d299604441c442af10d52fd7e8d1d7) 102 | 103 | 104 | 105 | ## [0.7.2] - 2021-09-18 106 | ### bug fixes 107 | - ***:** use a standarlized log interface [0ff01bd](https://github.com/fffonion/lua-resty-acme/commit/0ff01bd2ab39ff3973106946644223d1740a31b8) 108 | - **autossl:** release update_lock after cert is created to allow multiple type of certs for same domain to be created within short time [e315070](https://github.com/fffonion/lua-resty-acme/commit/e315070834a6c5b516110d61bb12bb9052f896a8) 109 | - **autossl:** increase cert lock time ([#47](https://github.com/fffonion/lua-resty-acme/issues/47)) [efb0602](https://github.com/fffonion/lua-resty-acme/commit/efb0602ab286f93f25d6f98d9ec26521970f743b) 110 | - **tls-alpn-01:** set version 3 in certificate generated ([#49](https://github.com/fffonion/lua-resty-acme/issues/49)) [887cad8](https://github.com/fffonion/lua-resty-acme/commit/887cad8b2ee02748c863f85e8f8afdec3ca897bf) 111 | 112 | 113 | 114 | ## [0.7.1] - 2021-07-22 115 | ### features 116 | - **autossl:** add challenge_start_delay [df4ba0b](https://github.com/fffonion/lua-resty-acme/commit/df4ba0b71a1f92b87d7f9f203475bc7115c56b9a) 117 | - **client:** add challenge_start_callback [1c9b2d5](https://github.com/fffonion/lua-resty-acme/commit/1c9b2d5a03eb644cc0770ec54e4d711bc03cdd42) 118 | 119 | 120 | 121 | ## [0.7.0] - 2021-06-25 122 | ### bug fixes 123 | - ***:** popup errors from lower functions [a19e9c8](https://github.com/fffonion/lua-resty-acme/commit/a19e9c8af9179a81815c653d176aa0bfc27e532b) 124 | - **autossl:** pass storage config to acme client ([#43](https://github.com/fffonion/lua-resty-acme/issues/43)) [ef1e541](https://github.com/fffonion/lua-resty-acme/commit/ef1e54112d1bdda187812a0e6c96d8b134fd4d04) 125 | 126 | ### features 127 | - **autossl:** check if domain is whitelisted before cert renewal ([#35](https://github.com/fffonion/lua-resty-acme/issues/35)) [942c007](https://github.com/fffonion/lua-resty-acme/commit/942c007711ba1a0f04b8f30f81443a46ae0ed412) 128 | - **client:** allow to read "alternate" link and select preferred chain ([#42](https://github.com/fffonion/lua-resty-acme/issues/42)) [ff17a74](https://github.com/fffonion/lua-resty-acme/commit/ff17a741d36f2058a21621c9191fda8513cb2c73) 129 | - **storage/vault:** add support for kubernetes auth ([#37](https://github.com/fffonion/lua-resty-acme/issues/37)) [93c2121](https://github.com/fffonion/lua-resty-acme/commit/93c212132a5d28b93269675c63a88a4e452001dc) 130 | 131 | 132 | 133 | ## [0.6.2] - 2021-07-22 134 | ### features 135 | - **autossl:** add challenge_start_delay [abc2e2e](https://github.com/fffonion/lua-resty-acme/commit/abc2e2eab2eb1220096163f84fdeee09df193db4) 136 | - **client:** add challenge_start_callback [2dc8df7](https://github.com/fffonion/lua-resty-acme/commit/2dc8df782b95d593dfbdea2186d2b8ab5d6af6be) 137 | 138 | 139 | 140 | ## [0.6.1] - 2021-06-25 141 | ### bug fixes 142 | - ***:** popup errors from lower functions [4e25b4d](https://github.com/fffonion/lua-resty-acme/commit/4e25b4dc4b10a77594546eaaceefe0418c91c3b7) 143 | - **autossl:** pass storage config to acme client ([#43](https://github.com/fffonion/lua-resty-acme/issues/43)) [102312f](https://github.com/fffonion/lua-resty-acme/commit/102312f51711ad0a5d12a30909fbb76134f973bd) 144 | - **autossl:** get_certkey always returning raw PEM text instead of cdata ([#33](https://github.com/fffonion/lua-resty-acme/issues/33)) [a1782c9](https://github.com/fffonion/lua-resty-acme/commit/a1782c994209450fc41deca8bf970d005fd17126) 145 | - **client:** retry on bad nonce ([#34](https://github.com/fffonion/lua-resty-acme/issues/34)) [bed74d3](https://github.com/fffonion/lua-resty-acme/commit/bed74d367c23c430a73d0bcd0764417cbec7b40e) 146 | - **client:** trigger only pending challenges ([#32](https://github.com/fffonion/lua-resty-acme/issues/32)) [3e3e940](https://github.com/fffonion/lua-resty-acme/commit/3e3e940a187e58dbb414fe543a11964454567c63) 147 | - **tls-alpn-01:** delegate get_ssl_ctx to lua-resty-openssl [cd99b84](https://github.com/fffonion/lua-resty-acme/commit/cd99b8481a7b57adc344fbae4b0c66fa09f8086b) 148 | 149 | 150 | 151 | ## [0.6.0] - 2021-02-19 152 | ### bug fixes 153 | - **autossl:** check if domain is set before trying to alter it ([#27](https://github.com/fffonion/lua-resty-acme/issues/27)) [fe36fc9](https://github.com/fffonion/lua-resty-acme/commit/fe36fc992b2d1c834eb8acbb8489f88f814653c4) 154 | - **autossl:** returns error in update_cert_handler ([#25](https://github.com/fffonion/lua-resty-acme/issues/25)) [a7dff99](https://github.com/fffonion/lua-resty-acme/commit/a7dff99ef5dc30dcecbea534b124231c5b0aa9cf) 155 | - **client:** BREAKING: do not force /directory at the end of api_url ([#31](https://github.com/fffonion/lua-resty-acme/issues/31)) [e4ea134](https://github.com/fffonion/lua-resty-acme/commit/e4ea134a0214f9df6f73fa8d31621cc96a382a6c) 156 | - **client:** allow charset Content-Type header of ACME responses ([#30](https://github.com/fffonion/lua-resty-acme/issues/30)) [3a9ade6](https://github.com/fffonion/lua-resty-acme/commit/3a9ade62867d304835fb888f3dfbdc872afc133d) 157 | - **openssl:** fix version import [6cb94be](https://github.com/fffonion/lua-resty-acme/commit/6cb94beb4b3911e28e55aa0b40ba547357e862e0) 158 | 159 | 160 | 161 | ## [0.5.11] - 2021-01-05 162 | ### bug fixes 163 | - **storage/etcd:** fix etcd list, add and add tests [7ddc1b4](https://github.com/fffonion/lua-resty-acme/commit/7ddc1b4a5e0c40850fa7f3d62bc460398518a7aa) 164 | 165 | ### features 166 | - **storage:** add etcd storage backend ([#13](https://github.com/fffonion/lua-resty-acme/issues/13)) [841e0c3](https://github.com/fffonion/lua-resty-acme/commit/841e0c3b527c442fdf0a7dc75c71d5cc8088b194) 167 | 168 | 169 | 170 | ## [0.5.10] - 2020-12-08 171 | ### features 172 | - ***:** allow to set account key in client and use account key from storage in autossl [6ec9ef5](https://github.com/fffonion/lua-resty-acme/commit/6ec9ef5bbb54d2afb437dcc423c4f410ae8f15f0) 173 | - **tls-alpn-01:** mark compatible with 1.19.3 [bec79ec](https://github.com/fffonion/lua-resty-acme/commit/bec79eca8b748f419e8b0f50b3393f6134331b4d) 174 | 175 | 176 | 177 | ## [0.5.9] - 2020-11-26 178 | ### bug fixes 179 | - **autossl:** always use lower cased domain [7fb0c83](https://github.com/fffonion/lua-resty-acme/commit/7fb0c83439dd3e7841bbb192ff2b6ed599e06ed2) 180 | - **tests:** correct asn1parse result in tests [99a8b01](https://github.com/fffonion/lua-resty-acme/commit/99a8b0121a7a75d0c233f9acd17b53f55739fe79) 181 | 182 | ### features 183 | - ***:** external account binding (EAB) support ([#19](https://github.com/fffonion/lua-resty-acme/issues/19)) [91383ed](https://github.com/fffonion/lua-resty-acme/commit/91383ed114f8a81d09f1b91394c831376e3233bb) 184 | 185 | 186 | 187 | ## [0.5.8] - 2020-09-10 188 | ### bug fixes 189 | - **autossl:** emit renewal success log correctly [63ee6ef](https://github.com/fffonion/lua-resty-acme/commit/63ee6ef7c3540b2d17ee546d5f005dd4df537c6e) 190 | - **storage:** vault backend uses correct TTL [21c4044](https://github.com/fffonion/lua-resty-acme/commit/21c4044f0bb560c3269c38289975598d3793726b) 191 | 192 | ### features 193 | - **autossl:** expose get_certkey function [#10](https://github.com/fffonion/lua-resty-acme/issues/10) [daaaf5f](https://github.com/fffonion/lua-resty-acme/commit/daaaf5fa7cc83166af6df6d60f1219664000b018) 194 | - **autossl:** add domain_whitelist_callback for dynamic domain matching [#9](https://github.com/fffonion/lua-resty-acme/issues/9) [dfe6991](https://github.com/fffonion/lua-resty-acme/commit/dfe6991445b032fe257162f9f479f04d243c3f2a) 195 | 196 | 197 | 198 | ## [0.5.7] - 2020-08-31 199 | ### bug fixes 200 | - **tls-alpn-01:** support openresty 1.17.8 [8e93d3b](https://github.com/fffonion/lua-resty-acme/commit/8e93d3ba8be84ae4bd688a84d8bb5109765258e5) 201 | 202 | 203 | 204 | ## [0.5.6] - 2020-08-12 205 | ### bug fixes 206 | - **tests:** pin lua-nginx-module and lua-resty-core [6266c56](https://github.com/fffonion/lua-resty-acme/commit/6266c5651e54c56442cef2584303781d16f84d3a) 207 | 208 | 209 | 210 | ## [0.5.5] - 2020-06-29 211 | ### bug fixes 212 | - **storage:** remove slash in consul and vault key path [5ddf210](https://github.com/fffonion/lua-resty-acme/commit/5ddf21071ce06a7e003a381440ff75df3faff78e) 213 | 214 | 215 | 216 | ## [0.5.4] - 2020-06-24 217 | ### features 218 | - **vault:** allow overriding tls options in vault storage [fed57b9](https://github.com/fffonion/lua-resty-acme/commit/fed57b9cc2a1d080dd10af398aeb48b1b55874d7) 219 | 220 | 221 | 222 | ## [0.5.3] - 2020-05-18 223 | ### features 224 | - **storage:** fully implement the file storage backend ([#6](https://github.com/fffonion/lua-resty-acme/issues/6)) [f1183e4](https://github.com/fffonion/lua-resty-acme/commit/f1183e4c4947dad6edd185631358f1d705a2d98e) 225 | 226 | 227 | 228 | ## [0.5.2] - 2020-04-27 229 | ### bug fixes 230 | - ***:** allow API endpoint to include or exclude /directory part [c7feb94](https://github.com/fffonion/lua-resty-acme/commit/c7feb944db40dc7d8e571cc09594aebffc496bd7) 231 | 232 | 233 | 234 | ## [0.5.1] - 2020-04-25 235 | ### bug fixes 236 | - ***:** fix domain key sanity check and http-01 challenge matching [687de21](https://github.com/fffonion/lua-resty-acme/commit/687de2134335278697220cf67ef0b26c4be34e07) 237 | - **client:** better error handling on directory request [984bfad](https://github.com/fffonion/lua-resty-acme/commit/984bfad031cef1a6ee3554c8c736ace596ed10d3) 238 | 239 | 240 | 241 | ## [0.5.0] - 2020-02-09 242 | ### bug fixes 243 | - **autossl:** add renewal success notice in error log [b1257de](https://github.com/fffonion/lua-resty-acme/commit/b1257de80bb0e55ff70694bba96bbcf9f9507ae8) 244 | - **autossl:** renew uses unparsed pkey [796b6e3](https://github.com/fffonion/lua-resty-acme/commit/796b6e3005b4301371ca99b2573e56644a456f01) 245 | - **client:** catch pkey new error in order_certificate [393a573](https://github.com/fffonion/lua-resty-acme/commit/393a573b3cb7d3c931f3860c4d99e1e5714edb67) 246 | - **client:** refine error message [5aac0fa](https://github.com/fffonion/lua-resty-acme/commit/5aac0fa92b84ba1b483f6c8d6913e67c7722a7cb) 247 | 248 | ### features 249 | - **client:** implement tls-alpn-01 challenge handler [25dc135](https://github.com/fffonion/lua-resty-acme/commit/25dc135eaf25c604d21b31664bb36e526a72ad2f) 250 | 251 | 252 | 253 | ## [0.4.2] - 2019-12-17 254 | ### bug fixes 255 | - **autossl:** fix lock on different types of keys [09180a2](https://github.com/fffonion/lua-resty-acme/commit/09180a25ea7864e07ef3d94ebb3b8456f072f967) 256 | - **client:** json decode on application/problem+json [2aabc1f](https://github.com/fffonion/lua-resty-acme/commit/2aabc1f5d535f273b97989f5874d45987fa0ebc9) 257 | 258 | 259 | 260 | ## [0.4.1] - 2019-12-11 261 | ### bug fixes 262 | - **client:** log authz final result [52ac754](https://github.com/fffonion/lua-resty-acme/commit/52ac754d8f888ed2f2ffa7976a5c3d6d18e63a48) 263 | 264 | 265 | 266 | ## [0.4.0] - 2019-12-11 267 | ### bug fixes 268 | - **client:** use POST-as-GET pattern [7198557](https://github.com/fffonion/lua-resty-acme/commit/7198557c616ef9f6d7b89809c4eef300a0e690bd) 269 | - **client:** fix parsing challenges [a4a37b5](https://github.com/fffonion/lua-resty-acme/commit/a4a37b572041dc6a1ea2b24ae14b7dea9e30782f) 270 | 271 | ### features 272 | - ***:** relying on storage to do cluster level sync [b513009](https://github.com/fffonion/lua-resty-acme/commit/b513009154cd8dbefdfe84f85c81c920d4104f9d) 273 | 274 | 275 | 276 | ## [0.3.0] - 2019-11-12 277 | ### bug fixes 278 | - **autossl:** fix typo [7c41e36](https://github.com/fffonion/lua-resty-acme/commit/7c41e36415d13e364fd58b694c3b4066d60ef1f4) 279 | - **renew:** api name in renew [9ecba64](https://github.com/fffonion/lua-resty-acme/commit/9ecba64ad928f4570f0f205459f042c06403efb8) 280 | - **storage:** fix third party storage module test [ef3e110](https://github.com/fffonion/lua-resty-acme/commit/ef3e1107506bfccc843153766ebcae2eee6f82a2) 281 | - **storage:** typo in redis storage, unified interface for file [2dd6cfa](https://github.com/fffonion/lua-resty-acme/commit/2dd6cfa2c77ab36d0254e1fedb832f2ecabcec99) 282 | 283 | ### features 284 | - **storage:** introduce add/setnx api [895b041](https://github.com/fffonion/lua-resty-acme/commit/895b041750ef4e920c3ed8ec432353f8e7e8eced) 285 | - **storage:** add consul and vault storage backend [028daa5](https://github.com/fffonion/lua-resty-acme/commit/028daa5bc965ab10621aa3f16d7ffabe619fd38a) 286 | 287 | 288 | 289 | ## [0.1.3] - 2019-10-18 290 | ### bug fixes 291 | - ***:** compatibility to use in Kong [6cc5688](https://github.com/fffonion/lua-resty-acme/commit/6cc568813d03a5ab8311ebdccf77131c204094d9) 292 | - **openssl:** follow up with upstream openssl library API [e791cb3](https://github.com/fffonion/lua-resty-acme/commit/e791cb302ce04665eaea722e9c0dc2f551f8c829) 293 | 294 | 295 | 296 | ## [0.1.2] - 2019-09-25 297 | ### bug fixes 298 | - ***:** reduce test flickiness, fix 1-index [706041b](https://github.com/fffonion/lua-resty-acme/commit/706041bec1dd062d6d0114619688c8f289b73779) 299 | - ***:** support openssl 1.0, cleanup error handling [1bb82ad](https://github.com/fffonion/lua-resty-acme/commit/1bb82ada64cab77468878654d324730bd06381e1) 300 | - **openssl:** remove premature error [f1853ab](https://github.com/fffonion/lua-resty-acme/commit/f1853abbb7a0f19a1bf98de99b70fd5b7779985c) 301 | - **openssl:** fix support for OpenSSL 1.0.2 [42c6e1c](https://github.com/fffonion/lua-resty-acme/commit/42c6e1c3de59a24da1b31b03ca517b858417e741) 302 | 303 | ### features 304 | - **crypto:** ffi support setting subjectAlt [2d992e8](https://github.com/fffonion/lua-resty-acme/commit/2d992e8973e65617d41c2c49dd9cb259deeaf84f) 305 | 306 | 307 | 308 | ## [0.1.1] - 2019-09-20 309 | ### features 310 | - **autossl:** whitelist domains [3dfc058](https://github.com/fffonion/lua-resty-acme/commit/3dfc05876d5947c869ab2f80cc9ae4e12cf601a8) 311 | 312 | 313 | 314 | ## 0.1.0 - 2019-09-20 315 | ### bug fixes 316 | - ***:** cleanup [2e8f3ed](https://github.com/fffonion/lua-resty-acme/commit/2e8f3ed8ac95076537272311338c1256e2a31e67) 317 | 318 | ### features 319 | - ***:** ffi-based openssl backend [ddbc37a](https://github.com/fffonion/lua-resty-acme/commit/ddbc37a227a5855a5a6caa60606d4534363f3204) 320 | - **autossl:** use lrucache [a6999c7](https://github.com/fffonion/lua-resty-acme/commit/a6999c7e154d21ff0b71358735527408836f36a7) 321 | - **autossl:** support ecc certs [6ed6a78](https://github.com/fffonion/lua-resty-acme/commit/6ed6a78e175ba4e6d6511d1c309d239e43b80ef9) 322 | - **crypto:** ffi pkey.new supports DER and public key as well [a18837b](https://github.com/fffonion/lua-resty-acme/commit/a18837b340f612cc4863903d57e5c3f0225c5919) 323 | - **crypto:** ffi openssl supports generating ec certificates [bc9d989](https://github.com/fffonion/lua-resty-acme/commit/bc9d989b4eb8bfa954f2f1ab08b0449957a27402) 324 | 325 | 326 | [Unreleased]: https://github.com/fffonion/lua-resty-acme/compare/0.15.0...HEAD 327 | [0.15.0]: https://github.com/fffonion/lua-resty-acme/compare/0.14.0...0.15.0 328 | [0.14.0]: https://github.com/fffonion/lua-resty-acme/compare/0.13.0...0.14.0 329 | [0.13.0]: https://github.com/fffonion/lua-resty-acme/compare/0.12.0...0.13.0 330 | [0.12.0]: https://github.com/fffonion/lua-resty-acme/compare/0.11.0...0.12.0 331 | [0.11.0]: https://github.com/fffonion/lua-resty-acme/compare/0.10.1...0.11.0 332 | [0.10.1]: https://github.com/fffonion/lua-resty-acme/compare/0.10.0...0.10.1 333 | [0.10.0]: https://github.com/fffonion/lua-resty-acme/compare/0.9.0...0.10.0 334 | [0.9.0]: https://github.com/fffonion/lua-resty-acme/compare/0.8.2...0.9.0 335 | [0.8.2]: https://github.com/fffonion/lua-resty-acme/compare/0.8.1...0.8.2 336 | [0.8.1]: https://github.com/fffonion/lua-resty-acme/compare/0.8.0...0.8.1 337 | [0.8.0]: https://github.com/fffonion/lua-resty-acme/compare/0.7.2...0.8.0 338 | [0.7.2]: https://github.com/fffonion/lua-resty-acme/compare/0.7.1...0.7.2 339 | [0.7.1]: https://github.com/fffonion/lua-resty-acme/compare/0.7.0...0.7.1 340 | [0.7.0]: https://github.com/fffonion/lua-resty-acme/compare/0.6.2...0.7.0 341 | [0.6.2]: https://github.com/fffonion/lua-resty-acme/compare/0.6.1...0.6.2 342 | [0.6.1]: https://github.com/fffonion/lua-resty-acme/compare/0.6.0...0.6.1 343 | [0.6.0]: https://github.com/fffonion/lua-resty-acme/compare/0.5.11...0.6.0 344 | [0.5.11]: https://github.com/fffonion/lua-resty-acme/compare/0.5.10...0.5.11 345 | [0.5.10]: https://github.com/fffonion/lua-resty-acme/compare/0.5.9...0.5.10 346 | [0.5.9]: https://github.com/fffonion/lua-resty-acme/compare/0.5.8...0.5.9 347 | [0.5.8]: https://github.com/fffonion/lua-resty-acme/compare/0.5.7...0.5.8 348 | [0.5.7]: https://github.com/fffonion/lua-resty-acme/compare/0.5.6...0.5.7 349 | [0.5.6]: https://github.com/fffonion/lua-resty-acme/compare/0.5.5...0.5.6 350 | [0.5.5]: https://github.com/fffonion/lua-resty-acme/compare/0.5.4...0.5.5 351 | [0.5.4]: https://github.com/fffonion/lua-resty-acme/compare/0.5.3...0.5.4 352 | [0.5.3]: https://github.com/fffonion/lua-resty-acme/compare/0.5.2...0.5.3 353 | [0.5.2]: https://github.com/fffonion/lua-resty-acme/compare/0.5.1...0.5.2 354 | [0.5.1]: https://github.com/fffonion/lua-resty-acme/compare/0.5.0...0.5.1 355 | [0.5.0]: https://github.com/fffonion/lua-resty-acme/compare/0.4.2...0.5.0 356 | [0.4.2]: https://github.com/fffonion/lua-resty-acme/compare/0.4.1...0.4.2 357 | [0.4.1]: https://github.com/fffonion/lua-resty-acme/compare/0.4.0...0.4.1 358 | [0.4.0]: https://github.com/fffonion/lua-resty-acme/compare/0.3.0...0.4.0 359 | [0.3.0]: https://github.com/fffonion/lua-resty-acme/compare/0.1.3...0.3.0 360 | [0.1.3]: https://github.com/fffonion/lua-resty-acme/compare/0.1.2...0.1.3 361 | [0.1.2]: https://github.com/fffonion/lua-resty-acme/compare/0.1.1...0.1.2 362 | [0.1.1]: https://github.com/fffonion/lua-resty-acme/compare/0.1.0...0.1.1 363 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | OPENRESTY_PREFIX=/usr/local/openresty 2 | 3 | #LUA_VERSION := 5.1 4 | PREFIX ?= /usr/local 5 | LUA_INCLUDE_DIR ?= $(PREFIX)/include 6 | LUA_LIB_DIR ?= $(PREFIX)/lib/lua/$(LUA_VERSION) 7 | INSTALL ?= install 8 | 9 | .PHONY: all test install 10 | 11 | all: ; 12 | 13 | DIRS = acme acme/storage acme/crypto/openssl acme/challenge 14 | 15 | $(DIRS): 16 | $(INSTALL) -d $(DESTDIR)$(LUA_LIB_DIR)/resty/$@ 17 | $(INSTALL) lib/resty/$@/*.lua $(DESTDIR)$(LUA_LIB_DIR)/resty/$@ 18 | 19 | install: all $(DIRS) 20 | 21 | test: all 22 | PATH=$(OPENRESTY_PREFIX)/nginx/sbin:$$PATH TEST_NGINX_TIMEOUT=60 SUBDOMAIN=localtest prove -I../test-nginx/lib -r t 23 | 24 | 25 | -------------------------------------------------------------------------------- /dist.ini: -------------------------------------------------------------------------------- 1 | name = lua-resty-acme 2 | abstract = Automatic Let's Encrypt certificate serving and Lua implementation of ACME procotol 3 | author = fffonion 4 | is_original = yes 5 | license = 3bsd 6 | lib_dir = lib 7 | doc_dir = lib 8 | repo_link = https://github.com/fffonion/lua-resty-acme 9 | main_module = lib/resty/acme/client.lua 10 | requires = luajit, openresty/lua-resty-lrucache >= 0.08, ledgetech/lua-resty-http >= 0.12, fffonion/lua-resty-openssl >= 0.7.0, spacewander/luafilesystem >= 0.1 11 | exclude_files=*.rock, *.rockspec 12 | -------------------------------------------------------------------------------- /lib/resty/acme/challenge/dns-01.lua: -------------------------------------------------------------------------------- 1 | local util = require("resty.acme.util") 2 | local digest = require("resty.openssl.digest") 3 | local resolver = require("resty.dns.resolver") 4 | local base64 = require("ngx.base64") 5 | local log = util.log 6 | local encode_base64url = base64.encode_base64url 7 | 8 | local _M = {} 9 | local mt = {__index = _M} 10 | 11 | function _M.new(storage) 12 | local self = setmetatable({ 13 | storage = storage, 14 | -- dns_provider_accounts_mapping = { 15 | -- ["*.domain.com"] = { 16 | -- provider = "cloudflare", 17 | -- secret = "token" 18 | -- }, 19 | -- ["www.domain.com"] = { 20 | -- provider = "dynv6", 21 | -- secret = "token" 22 | -- } 23 | -- } 24 | dns_provider_accounts_mapping = {}, 25 | dns_provider_modules = {}, 26 | }, mt) 27 | return self 28 | end 29 | 30 | local function calculate_txt_record(keyauthorization) 31 | local dgst = assert(digest.new("sha256"):final(keyauthorization)) 32 | local txt_record = encode_base64url(dgst) 33 | return txt_record 34 | end 35 | 36 | local function ch_key(challenge) 37 | return challenge .. "#dns-01" 38 | end 39 | 40 | local function choose_dns_provider(self, domain) 41 | local prov = self.dns_provider_accounts_mapping[domain] 42 | if not prov then 43 | return nil, "no dns provider key configured for domain " .. domain 44 | end 45 | 46 | if not prov.provider or not prov.secret then 47 | return nil, "provider config malformed for domain " .. domain 48 | end 49 | 50 | local module = self.dns_provider_modules[prov.provider] 51 | if not module then 52 | return nil, "provider " .. prov.provider .. " is not loaded for domain " .. domain 53 | end 54 | 55 | local handler, err = module.new(prov.secret) 56 | if not err then 57 | return handler 58 | end 59 | return nil, "dns provider init error: " .. err .. " for domain " .. domain 60 | end 61 | 62 | local function verify_txt_record(record_name, expected_record_content) 63 | local r, err = resolver:new{ 64 | nameservers = {"8.8.8.8", "8.8.4.4"}, 65 | retrans = 5, 66 | timeout = 2000, 67 | no_random = true, 68 | } 69 | if not r then 70 | return false, "failed to instantiate the resolver: " .. err 71 | end 72 | local answers, err, _ = r:tcp_query(record_name, { qtype = r.TYPE_TXT }, {}) 73 | if not answers then 74 | return false, "failed to query the DNS server: " .. err 75 | end 76 | if answers.errcode then 77 | return false, "server returned error code: " .. answers.errcode .. ": " .. (answers.errstr or "nil") 78 | end 79 | for _, ans in ipairs(answers) do 80 | if ans.txt == expected_record_content then 81 | log(ngx.DEBUG, "verify txt record ok: ", ans.name, ", content: ", ans.txt) 82 | return true 83 | end 84 | end 85 | return false, "txt record mismatch" 86 | end 87 | 88 | function _M:update_dns_provider_info(dns_provider_accounts) 89 | self.dns_provider_accounts_mapping = {} 90 | self.dns_provider_modules = {} 91 | 92 | for i, account in ipairs(dns_provider_accounts) do 93 | if not account.name then 94 | return nil, "#" .. i .. " element in dns_provider_accounts doesn't have a name" 95 | end 96 | if not account.secret then 97 | return nil, "dns provider account " .. account.name .." doesn't have a secret" 98 | end 99 | if not account.provider then 100 | return nil, "dns provider account " .. account.name .." doesn't have a provider" 101 | end 102 | 103 | if not self.dns_provider_modules[account.provider] then 104 | local ok, perr = pcall(require, "resty.acme.dns_provider." .. account.provider) 105 | if not ok then 106 | return nil, "dns provider " .. account.provider .. " failed to load: " .. perr 107 | end 108 | 109 | self.dns_provider_modules[account.provider] = perr 110 | end 111 | 112 | for _, domain in ipairs(account.domains) do 113 | self.dns_provider_accounts_mapping[domain] = account 114 | end 115 | end 116 | 117 | return true 118 | end 119 | 120 | function _M:register_challenge(_, response, domains) 121 | local dnsapi, err 122 | for _, domain in ipairs(domains) do 123 | err = self.storage:set(ch_key(domain), response, 3600) 124 | if err then 125 | return err 126 | end 127 | dnsapi, err = choose_dns_provider(self, domain) 128 | if err then 129 | return err 130 | end 131 | 132 | local txt_record_name = "_acme-challenge." .. domain:gsub("*.", "") 133 | local txt_record_content = calculate_txt_record(response) 134 | log(ngx.DEBUG, "calculated txt record: ", txt_record_content, " for domain: ", domain) 135 | 136 | local _, err = dnsapi:post_txt_record(txt_record_name, txt_record_content) 137 | if err then 138 | return err 139 | end 140 | 141 | log(ngx.INFO, "waiting up to 5 minutes for dns record propagation on ", txt_record_name) 142 | 143 | local wait_verify_counts = 0 144 | while true do 145 | local ok, err = verify_txt_record(txt_record_name, txt_record_content) 146 | if ok then 147 | break 148 | end 149 | log(ngx.DEBUG, "unable to verify txt record, last error was: ", err, ", retrying in 5 seconds") 150 | ngx.sleep(5) 151 | wait_verify_counts = wait_verify_counts + 1 152 | if wait_verify_counts >= 60 then 153 | return "timeout (5m) exceeded to verify txt record, latest error was: " .. (err or "nil") 154 | end 155 | end 156 | 157 | log(ngx.INFO, "txt record for ", txt_record_name, " verified, continue to next domain") 158 | end 159 | end 160 | 161 | function _M:cleanup_challenge(_--[[challenge]], domains) 162 | local dnsapi, err 163 | for _, domain in ipairs(domains) do 164 | err = self.storage:delete(ch_key(domain)) 165 | if err then 166 | return err 167 | end 168 | dnsapi, err = choose_dns_provider(self, domain) 169 | if err then 170 | return err 171 | end 172 | local trim_domain = domain:gsub("*.", "") 173 | local result, err = dnsapi:delete_txt_record("_acme-challenge." .. trim_domain) 174 | if err then 175 | return err 176 | end 177 | log(ngx.DEBUG, "dns provider delete_txt_record returns: ", result) 178 | end 179 | end 180 | 181 | return _M 182 | -------------------------------------------------------------------------------- /lib/resty/acme/challenge/http-01.lua: -------------------------------------------------------------------------------- 1 | local util = require "resty.acme.util" 2 | local log = util.log 3 | 4 | local _M = {} 5 | local mt = {__index = _M} 6 | 7 | function _M.new(storage) 8 | local self = setmetatable({ 9 | -- TODO: will this ever change? 10 | uri_prefix = "acme-challenge", 11 | storage = storage, 12 | }, mt) 13 | return self 14 | end 15 | 16 | local function ch_key(challenge) 17 | return challenge .. "#http-01" 18 | end 19 | 20 | 21 | function _M:register_challenge(challenge, response, _--[[domains]]) 22 | return self.storage:set(ch_key(challenge), response, 3600) 23 | end 24 | 25 | function _M:cleanup_challenge(challenge, _--[[domains]]) 26 | return self.storage:delete(ch_key(challenge)) 27 | end 28 | 29 | function _M:serve_challenge() 30 | if ngx.config.subsystem ~= "http" then 31 | log(ngx.ERR, "http-01 challenge can't be used in ", ngx.config.subsystem, " subsystem") 32 | ngx.exit(500) 33 | end 34 | 35 | local captures, err = 36 | ngx.re.match(ngx.var.request_uri, [[\.well-known/]] .. self.uri_prefix .. "/(.+)", "jo") 37 | 38 | if err or not captures or not captures[1] then 39 | log(ngx.ERR, "error extracting token from request_uri ", err) 40 | ngx.exit(400) 41 | end 42 | 43 | local token = captures[1] 44 | 45 | log(ngx.DEBUG, "token is ", token) 46 | 47 | local value, err = self.storage:get(ch_key(token)) 48 | 49 | if err then 50 | log(ngx.ERR, "error getting challenge response from storage ", err) 51 | ngx.exit(500) 52 | end 53 | 54 | if not value then 55 | log(ngx.WARN, "no corresponding response found for ", token) 56 | ngx.exit(404) 57 | end 58 | 59 | ngx.say(value) 60 | -- this following must be set to allow the library to be used in other openresty framework 61 | ngx.exit(0) 62 | end 63 | 64 | return _M 65 | -------------------------------------------------------------------------------- /lib/resty/acme/challenge/tls-alpn-01.lua: -------------------------------------------------------------------------------- 1 | local ssl = require "ngx.ssl" 2 | local util = require "resty.acme.util" 3 | local log = util.log 4 | 5 | local pkey = require("resty.openssl.pkey") 6 | local digest = require("resty.openssl.digest") 7 | local x509 = require("resty.openssl.x509") 8 | local altname = require("resty.openssl.x509.altname") 9 | local extension = require("resty.openssl.x509.extension") 10 | local objects = require("resty.openssl.objects") 11 | local ssl_ctx = require("resty.openssl.ssl_ctx") 12 | 13 | 14 | local _M = {} 15 | local mt = {__index = _M} 16 | 17 | -- Ref: https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-07 18 | 19 | -- local ssl_find_proto_acme_tls = function(client_alpn) 20 | -- local len = 1 21 | -- local acme_found 22 | -- while len < #client_alpn do 23 | -- local i = string.byte(sub(client_alpn, len, len+1)) 24 | -- local proto = sub(client_alpn, len+1, len+2+i) 25 | -- if proto == acme_protocol_name then 26 | -- acme_found = true 27 | -- break 28 | -- end 29 | -- len = len + i + 1 30 | -- end 31 | -- return acme_found 32 | -- end 33 | 34 | local acme_protocol_only = { 'acme-tls/1' } 35 | 36 | local injected = false 37 | 38 | local function inject_tls_alpn() 39 | if injected then 40 | return true 41 | end 42 | local ssl_ctx, err = ssl_ctx.from_request() 43 | if err then 44 | log(ngx.WARN, "inject_tls_alpn: ", err) 45 | return 46 | end 47 | local _, err = ssl_ctx:set_alpns(acme_protocol_only) 48 | if err then 49 | log(ngx.WARN, "inject_tls_alpn: ", err) 50 | return 51 | end 52 | injected = true 53 | return true 54 | end 55 | 56 | function _M.new(storage) 57 | local self = setmetatable({ 58 | storage = storage, 59 | }, mt) 60 | return self 61 | end 62 | 63 | local function ch_key(challenge) 64 | return challenge .. "#tls-alpn-01" 65 | end 66 | 67 | 68 | function _M:register_challenge(_, response, domains) 69 | local err 70 | for _, domain in ipairs(domains) do 71 | err = self.storage:set(ch_key(domain), response, 3600) 72 | if err then 73 | return err 74 | end 75 | end 76 | end 77 | 78 | function _M:cleanup_challenge(_--[[challenge]], domains) 79 | local err 80 | for _, domain in ipairs(domains) do 81 | err = self.storage:delete(ch_key(domain)) 82 | if err then 83 | return err 84 | end 85 | end 86 | end 87 | 88 | local id_pe_acmeIdentifier = "1.3.6.1.5.5.7.1.31" 89 | local nid = objects.txt2nid(id_pe_acmeIdentifier) 90 | if not nid or nid == 0 then 91 | nid = objects.create( 92 | id_pe_acmeIdentifier, -- nid 93 | "pe-acmeIdentifier", -- sn 94 | "ACME Identifier" -- ln 95 | ) 96 | end 97 | 98 | local function serve_challenge_cert(self) 99 | local domain = assert(ssl.server_name()) 100 | local challenge, err = self.storage:get(ch_key(domain)) 101 | if err then 102 | log(ngx.ERR, "error getting challenge response from storage ", err) 103 | ngx.exit(500) 104 | end 105 | 106 | if not challenge then 107 | log(ngx.WARN, "no corresponding response found for ", domain) 108 | ngx.exit(404) 109 | end 110 | 111 | local dgst = assert(digest.new("sha256"):final(challenge)) 112 | -- 0x04: OCTET STRING 113 | -- 0x20: length 114 | dgst = "DER:0420" .. dgst:gsub("(.)", function(s) return string.format("%02x", string.byte(s)) end) 115 | log(ngx.DEBUG, "token: ", challenge, ", digest: ", dgst) 116 | 117 | local key = pkey.new() 118 | local cert = x509.new() 119 | cert:set_pubkey(key) 120 | assert(cert:set_version(3)) 121 | local ext = assert(extension.new(nid, dgst)) 122 | ext:set_critical(true) 123 | cert:add_extension(ext) 124 | 125 | local alt = assert(altname.new():add( 126 | "DNS", domain 127 | )) 128 | assert(cert:set_subject_alt_name(alt)) 129 | cert:sign(key) 130 | 131 | local key_ct = assert(ssl.parse_pem_priv_key(key:to_PEM("private"))) 132 | local cert_ct = assert(ssl.parse_pem_cert(cert:to_PEM())) 133 | 134 | ssl.clear_certs() 135 | assert(ssl.set_cert(cert_ct)) 136 | assert(ssl.set_priv_key(key_ct)) 137 | 138 | log(ngx.DEBUG, "served tls-alpn challenge") 139 | end 140 | 141 | function _M:serve_challenge() 142 | if ngx.config.subsystem ~= "stream" then 143 | log(ngx.ERR, "tls-apln-01 challenge can't be used in ", ngx.config.subsystem, " subsystem") 144 | ngx.exit(500) 145 | end 146 | 147 | local phase = ngx.get_phase() 148 | if phase == "ssl_cert" then 149 | if inject_tls_alpn() then 150 | serve_challenge_cert(self) 151 | end 152 | else 153 | log(ngx.ERR, "tls-apln-01 challenge don't know what to do in ", phase, " phase") 154 | ngx.exit(500) 155 | end 156 | end 157 | 158 | return _M 159 | -------------------------------------------------------------------------------- /lib/resty/acme/client.lua: -------------------------------------------------------------------------------- 1 | local http = require("resty.http") 2 | local cjson = require("cjson") 3 | local util = require("resty.acme.util") 4 | local openssl = require("resty.acme.openssl") 5 | 6 | local encode_base64url = util.encode_base64url 7 | local decode_base64url = util.decode_base64url 8 | 9 | local log = util.log 10 | local ngx_ERR = ngx.ERR 11 | local ngx_INFO = ngx.INFO 12 | local ngx_DEBUG = ngx.DEBUG 13 | local ngx_WARN = ngx.DEBUG 14 | 15 | local json = cjson.new() 16 | -- some implemntations like ZeroSSL doesn't like / to be escaped 17 | if json.encode_escape_forward_slash then 18 | json.encode_escape_forward_slash(false) 19 | end 20 | 21 | local wait_backoff_series = {1, 1, 2, 3, 5, 8, 13, 21} 22 | 23 | local TEST_TRY_NONCE_INFINITELY = not not os.getenv("TEST_TRY_NONCE_INFINITELY") 24 | 25 | local _M = { 26 | _VERSION = '0.15.0' 27 | } 28 | local mt = {__index = _M} 29 | 30 | local default_config = { 31 | -- the ACME v2 API endpoint to use 32 | api_uri = "https://acme-v02.api.letsencrypt.org/directory", 33 | -- the account email to register 34 | account_email = nil, 35 | -- the account key in PEM format text 36 | account_key = nil, 37 | -- the account kid (as an URL) 38 | account_kid = nil, 39 | -- external account binding key id 40 | eab_kid = nil, 41 | -- external account binding hmac key, base64url encoded 42 | eab_hmac_key = nil, 43 | -- external account registering handler 44 | eab_handler = nil, 45 | -- storage for challenge 46 | storage_adapter = "shm", 47 | -- the storage config passed to storage adapter 48 | storage_config = { 49 | shm_name = "acme" 50 | }, 51 | -- the challenge types enabled 52 | enabled_challenge_handlers = {"http-01"}, 53 | -- select preferred root CA issuer's Common Name if appliable 54 | preferred_chain = nil, 55 | -- callback function that allows to wait before signaling ACME server to validate 56 | challenge_start_callback = nil, 57 | -- the dict of dns providers, each provider should have following struct: 58 | dns_provider_accounts = {}, 59 | } 60 | 61 | local function new_httpc() 62 | local httpc = ngx.ctx.acme_httpc 63 | if not httpc then 64 | httpc = http.new() 65 | ngx.ctx.acme_httpc = httpc 66 | end 67 | return httpc 68 | end 69 | 70 | local function set_account_key(self, account_key) 71 | local account_pkey = openssl.pkey.new(account_key) 72 | self.account_pkey = account_pkey 73 | local account_thumbprint, err = util.thumbprint(account_pkey) 74 | if err then 75 | return false, "failed to calculate thumbprint: " .. err 76 | end 77 | self.account_thumbprint = account_thumbprint 78 | return true, nil 79 | end 80 | 81 | function _M.new(conf) 82 | conf = setmetatable(conf or {}, {__index = default_config}) 83 | 84 | local self = setmetatable( 85 | { 86 | directory = nil, 87 | conf = conf, 88 | account_pkey = nil, 89 | account_kid = conf.account_kid, 90 | nonce = nil, 91 | eab_required = false, -- CA requires external account binding or not 92 | eab_handler = conf.eab_handler, 93 | eab_kid = conf.eab_kid, 94 | eab_hmac_key = decode_base64url(conf.eab_hmac_key), 95 | challenge_handlers = {}, 96 | }, mt 97 | ) 98 | 99 | local storage_adapter = conf.storage_adapter 100 | -- TODO: catch error and return gracefully 101 | if not storage_adapter:find("%.") then 102 | storage_adapter = "resty.acme.storage." .. storage_adapter 103 | end 104 | local storagemod = require(storage_adapter) 105 | local storage, err = storagemod.new(conf.storage_config) 106 | if err then 107 | return nil, err 108 | end 109 | self.storage = storage 110 | 111 | if not conf.enabled_challenge_handlers then 112 | return nil, "at least one challenge handler is needed" 113 | end 114 | 115 | -- TODO: catch error and return gracefully 116 | for _, c in ipairs(conf.enabled_challenge_handlers) do 117 | local handler = require("resty.acme.challenge." .. c) 118 | self.challenge_handlers[c] = handler.new(self.storage) 119 | if c == "dns-01" then 120 | local ok, err = self.challenge_handlers[c]:update_dns_provider_info(self.conf.dns_provider_accounts) 121 | if not ok then 122 | return nil, err 123 | end 124 | end 125 | end 126 | 127 | if conf.account_key then 128 | local _, err = set_account_key(self, conf.account_key) 129 | if err then 130 | return nil, err 131 | end 132 | end 133 | 134 | return self 135 | end 136 | 137 | _M.set_account_key = set_account_key 138 | 139 | function _M:init() 140 | local httpc = new_httpc() 141 | 142 | local resp, err = httpc:request_uri(self.conf.api_uri) 143 | if err then 144 | return "acme directory request failed: " .. err 145 | end 146 | 147 | if resp and resp.status == 200 and resp.headers["content-type"] and 148 | resp.headers["content-type"]:match("application/json") 149 | then 150 | local directory = json.decode(resp.body) 151 | if not directory then 152 | return "acme directory listing response malformed" 153 | end 154 | self.directory = directory 155 | else 156 | local status = resp and resp.status 157 | local content_type = resp and resp.headers and resp.headers["content-type"] 158 | return string.format("acme directory listing failed: status code %s, content-type %s", 159 | status, content_type) 160 | end 161 | 162 | if not self.directory["newNonce"] or 163 | not self.directory["newAccount"] or 164 | not self.directory["newOrder"] or 165 | not self.directory["revokeCert"] then 166 | return "acme directory endpoint is missing at least one of ".. 167 | "newNonce, newAccount, newOrder or revokeCert endpoint" 168 | end 169 | 170 | if self.directory['meta'] and 171 | self.directory['meta']['externalAccountRequired'] then 172 | 173 | self.eab_required = true 174 | 175 | if not self.eab_handler and 176 | (not self.eab_kid or not self.eab_hmac_key) then 177 | 178 | -- try to load a predefined eab handler 179 | local website = self.directory['meta'] and self.directory['meta']['website'] 180 | if website then 181 | -- load the module based on website metadata 182 | website = ngx.re.sub(website, [=[^https?://([^/]+).*$]=], "$1"):gsub("%.", "-") 183 | local pok, eab_handler_module = pcall(require, "resty.acme.eab." .. website) 184 | if pok and eab_handler_module and eab_handler_module.handle then 185 | log(ngx_INFO, "loaded EAB module ", "resty.acme.eab." .. website) 186 | self.eab_handler = eab_handler_module.handle 187 | return 188 | end 189 | end 190 | 191 | return "CA requires external account binding, either define a eab_handler to automatically ".. 192 | "register account, or define eab_kid and eab_hmac_key for existing account" 193 | end 194 | end 195 | 196 | return nil 197 | end 198 | 199 | --- Enclose the provided payload in JWS 200 | -- 201 | -- @param url ACME service URL 202 | -- @param payload (json) data which will be wrapped in JWS 203 | -- @param nonce nonce to be used in JWS, if not provided new nonce will be requested 204 | function _M:jws(url, payload, nonce) 205 | if not self.account_pkey then 206 | return nil, "account key does not specified" 207 | end 208 | 209 | if not url then 210 | return nil, "url is not defined" 211 | end 212 | 213 | if not nonce then 214 | local err 215 | nonce, err = self:new_nonce() 216 | if err then 217 | return nil, "can't get new nonce from acme server: " .. err 218 | end 219 | end 220 | 221 | local jws = { 222 | protected = { 223 | alg = "RS256", 224 | nonce = nonce, 225 | url = url 226 | }, 227 | payload = payload 228 | } 229 | 230 | -- TODO: much better handling 231 | if payload and payload.contact then 232 | local params, err = self.account_pkey:get_parameters() 233 | if not params then 234 | return nil, "can't get parameters from account key: " .. (err or "nil") 235 | end 236 | 237 | jws.protected.jwk = { 238 | e = encode_base64url(params.e:to_binary()), 239 | kty = "RSA", 240 | n = encode_base64url(params.n:to_binary()) 241 | } 242 | 243 | if self.eab_required then 244 | local eab_jws = { 245 | protected = { 246 | alg = "HS256", 247 | kid = self.eab_kid, 248 | url = url 249 | }, 250 | payload = jws.protected.jwk, 251 | } 252 | 253 | log(ngx_DEBUG, "eab jws payload: ", json.encode(eab_jws)) 254 | 255 | eab_jws.protected = encode_base64url(json.encode(eab_jws.protected)) 256 | eab_jws.payload = encode_base64url(json.encode(eab_jws.payload)) 257 | local hmac = openssl.hmac.new(self.eab_hmac_key, "SHA256") 258 | local sig = hmac:final(eab_jws.protected .. "." .. eab_jws.payload) 259 | eab_jws.signature = encode_base64url(sig) 260 | 261 | payload['externalAccountBinding'] = eab_jws 262 | end 263 | elseif not self.account_kid then 264 | return nil, "account_kid is not defined, provide via config or create account first" 265 | else 266 | jws.protected.kid = self.account_kid 267 | end 268 | 269 | log(ngx_DEBUG, "jws payload: ", json.encode(jws)) 270 | 271 | jws.protected = encode_base64url(json.encode(jws.protected)) 272 | -- if payload is not set, we are doing a POST-as-GET (https://tools.ietf.org/html/rfc8555#section-6.3) 273 | -- set it to empty string 274 | jws.payload = payload and encode_base64url(json.encode(payload)) or "" 275 | local digest = openssl.digest.new("SHA256") 276 | digest:update(jws.protected .. "." .. jws.payload) 277 | jws.signature = encode_base64url(self.account_pkey:sign(digest)) 278 | 279 | return json.encode(jws) 280 | end 281 | 282 | --- ACME wrapper for http.post() 283 | -- 284 | -- @param url ACME service URL 285 | -- @param payload Request content 286 | -- @param headers Lua table with request headers 287 | -- 288 | -- @return Response object or tuple (nil, msg) on errors 289 | function _M:post(url, payload, headers, nonce) 290 | local httpc = new_httpc() 291 | if not headers then 292 | headers = { 293 | ["content-type"] = "application/jose+json" 294 | } 295 | elseif not headers["content-type"] then 296 | headers["content-type"] = "application/jose+json" 297 | end 298 | 299 | local jws, err = self:jws(url, payload, nonce) 300 | if not jws then 301 | return nil, nil, err 302 | end 303 | 304 | local resp, err = httpc:request_uri(url, 305 | { 306 | method = "POST", 307 | body = jws, 308 | headers = headers 309 | } 310 | ) 311 | 312 | if err then 313 | return nil, nil, err 314 | end 315 | log(ngx_DEBUG, "acme request: ", url, " response: ", resp.body) 316 | 317 | local body 318 | if resp.headers['Content-Type']:sub(1, 16) == "application/json" then 319 | body = json.decode(resp.body) 320 | elseif resp.headers['Content-Type']:sub(1, 24) == "application/problem+json" then 321 | body = json.decode(resp.body) 322 | if body.type == 'urn:ietf:params:acme:error:badNonce' and resp.headers["Replay-Nonce"] then 323 | if not nonce then 324 | log(ngx_WARN, "bad nonce: recoverable error, retrying") 325 | return self:post(url, payload, headers, resp.headers["Replay-Nonce"]) 326 | elseif not TEST_TRY_NONCE_INFINITELY then 327 | return nil, nil, "bad nonce: failed again, bailing out" 328 | end 329 | else 330 | return nil, nil, body.detail or body.type 331 | end 332 | else 333 | body = resp.body 334 | end 335 | 336 | return body, resp.headers, err 337 | end 338 | 339 | function _M:new_account() 340 | if self.account_kid then 341 | return self.account_kid, nil 342 | end 343 | 344 | local payload = { 345 | termsOfServiceAgreed = true, 346 | } 347 | 348 | if self.conf.account_email then 349 | payload['contact'] = { 350 | "mailto:" .. self.conf.account_email, 351 | } 352 | end 353 | 354 | if self.eab_required and (not self.eab_kid or not self.eab_hmac_key) then 355 | if not self.eab_handler then 356 | return nil, "eab_handler undefined while EAB is required by CA" 357 | end 358 | local eab_kid, eab_hmac_key, err = self.eab_handler(self.conf.account_email) 359 | if err then 360 | return nil, "eab_handler returned an error: " .. err 361 | end 362 | self.eab_kid = eab_kid 363 | self.eab_hmac_key = decode_base64url(eab_hmac_key) 364 | end 365 | 366 | local _, headers, err = self:post(self.directory["newAccount"], payload) 367 | 368 | if err then 369 | return nil, "failed to create account: " .. err 370 | end 371 | 372 | self.account_kid = headers["location"] 373 | 374 | return self.account_kid, nil 375 | end 376 | 377 | function _M:new_nonce() 378 | local httpc = new_httpc() 379 | local resp, err = httpc:request_uri(self.directory["newNonce"], 380 | { 381 | method = "HEAD" 382 | } 383 | ) 384 | 385 | if resp and resp.headers then 386 | -- TODO: Expect status code 204 387 | -- TODO: Expect Cache-Control: no-store 388 | -- TODO: Expect content size 0 389 | return resp.headers["replay-nonce"] 390 | else 391 | return nil, "failed to fetch new nonce: " .. err 392 | end 393 | end 394 | 395 | function _M:new_order(...) 396 | local domains = {...} 397 | if domains.n == 0 then 398 | return nil, nil, "at least one domains should be provided" 399 | end 400 | 401 | local identifiers = {} 402 | for i, domain in ipairs(domains) do 403 | identifiers[i] = { 404 | type = "dns", 405 | value = domain 406 | } 407 | end 408 | 409 | local body, headers, err = self:post(self.directory["newOrder"], 410 | { 411 | identifiers = identifiers, 412 | } 413 | ) 414 | 415 | if err then 416 | return nil, nil, err 417 | end 418 | 419 | return body, headers, nil 420 | end 421 | 422 | local function watch_order_status(self, order_url, target) 423 | local order_status, err 424 | for _, t in pairs(wait_backoff_series) do 425 | ngx.sleep(t) 426 | -- POST-as-GET request with empty payload 427 | order_status, _, err = self:post(order_url) 428 | log(ngx_DEBUG, "check order: ", json.encode(order_status), " err: ", err) 429 | if order_status then 430 | if order_status.status == target then 431 | break 432 | elseif order_status.status == "invalid" then 433 | local errors = {} 434 | for _, authz in ipairs(order_status.authorizations) do 435 | local authz_status, _, err = self:post(authz) 436 | if err then 437 | log(ngx_WARN, "error fetching authorization final status:", err) 438 | else 439 | for _, c in ipairs(authz_status.challenges) do 440 | log(ngx_DEBUG, "authorization status: ", json.encode(c)) 441 | local err_msg = c['type'] .. ": " .. c['status'] 442 | if c['error'] and c['error']['detail'] then 443 | err_msg = err_msg .. ": " .. c['error']['detail'] 444 | end 445 | errors[#errors+1] = err_msg 446 | end 447 | end 448 | end 449 | return nil, "challenge invalid: " .. table.concat(errors, "; ") 450 | end 451 | end 452 | end 453 | 454 | if not order_status then 455 | return nil, "could not get order status" 456 | end 457 | 458 | if order_status.status ~= target then 459 | return nil, "failed to wait for order status, got " .. (order_status.status or "nil") 460 | end 461 | 462 | return order_status 463 | end 464 | 465 | 466 | local rel_alternate_pattern = '<(.+)>;%s*rel="alternate"' 467 | local function parse_alternate_link(headers) 468 | local link_header = headers["Link"] 469 | if type(link_header) == "string" then 470 | return link_header:match(rel_alternate_pattern) 471 | elseif link_header then 472 | for _, link in pairs(link_header) do 473 | local m = link:match(rel_alternate_pattern) 474 | if m then 475 | return m 476 | end 477 | end 478 | end 479 | end 480 | 481 | function _M:finalize(finalize_url, order_url, csr) 482 | local payload = { 483 | csr = encode_base64url(csr) 484 | } 485 | 486 | local resp, _, err = self:post(finalize_url, payload) 487 | 488 | if err then 489 | return nil, "failed to send finalize request: " .. err 490 | end 491 | 492 | -- Wait until the order is valid: ready to download 493 | if not resp.certificate and resp.status and resp.status == "valid" then 494 | log(ngx_DEBUG, json.encode(resp)) 495 | return nil, "no certificate object returned " .. (resp.detail or "") 496 | end 497 | 498 | local order_status, err = watch_order_status(self, order_url, "valid") 499 | if not order_status or not order_status.certificate then 500 | return nil, "error checking finalize: " .. err 501 | end 502 | 503 | -- POST-as-GET request with empty payload 504 | local body, headers, err = self:post(order_status.certificate) 505 | if err then 506 | return nil, "failed to fetch certificate: " .. err 507 | end 508 | 509 | local cert_content_type = headers["content-type"] 510 | if cert_content_type and string.sub(cert_content_type, 1, 33):lower() ~= "application/pem-certificate-chain" then 511 | return nil, "wrong content type, got " .. cert_content_type 512 | end 513 | 514 | local preferred_chain = self.conf.preferred_chain 515 | if not preferred_chain then 516 | return body 517 | end 518 | 519 | local ok, err = util.check_chain_root_issuer(body, preferred_chain) 520 | if not ok then 521 | log(ngx_DEBUG, "configured preferred chain issuer CN \"", preferred_chain, "\" not found ", 522 | "in default chain, downloading alternate chain: ", err) 523 | local alternate_link = parse_alternate_link(headers) 524 | if not alternate_link then 525 | log(ngx_WARN, "failed to fetch alternate chain because no alternate link is found, ", 526 | "fallback to default chain") 527 | else 528 | local body_alternate, _, err = self:post(alternate_link) 529 | 530 | if err then 531 | log(ngx_WARN, "failed to fetch alternate chain, fallback to default: ", err) 532 | else 533 | local ok, err = util.check_chain_root_issuer(body_alternate, preferred_chain) 534 | if ok then 535 | log(ngx_DEBUG, "alternate chain is selected") 536 | return body_alternate 537 | end 538 | log(ngx_WARN, "configured preferred chain issuer CN \"", preferred_chain, "\" also not found ", 539 | "in alternate chain, fallback to default chain: ", err) 540 | end 541 | end 542 | end 543 | 544 | return body 545 | end 546 | 547 | -- create certificate workflow, used in new cert or renewal 548 | function _M:order_certificate(domain_key, ...) 549 | -- create new-order request 550 | local order_body, order_headers, err = self:new_order(...) 551 | if err then 552 | return nil, "failed to create new order: " .. err 553 | end 554 | 555 | log(ngx_DEBUG, "new order: ", json.encode(order_body)) 556 | 557 | -- setup challenges 558 | local finalize_url = order_body.finalize 559 | local order_url = order_headers["location"] 560 | local authzs = order_body.authorizations or {} 561 | local registered_challenges = {} 562 | local registered_challenge_count = 0 563 | local has_valid_challenge = false 564 | 565 | for _, authz in ipairs(authzs) do 566 | -- POST-as-GET request with empty payload 567 | local challenges, _, err = self:post(authz) 568 | if err then 569 | return nil, "failed to fetch authz: " .. err 570 | end 571 | 572 | if not challenges.challenges then 573 | log(ngx_WARN, "fetching challenges returns an error: ", err) 574 | goto nextchallenge 575 | end 576 | for _, challenge in ipairs(challenges.challenges) do 577 | local typ = challenge.type 578 | if challenge.status ~= 'pending' then 579 | if challenge.status == 'valid' then 580 | has_valid_challenge = true 581 | end 582 | log(ngx_DEBUG, "challenge ", typ, ": ", challenge.token, " is ", challenge.status, ", skipping") 583 | elseif self.challenge_handlers[typ] then 584 | local err = self.challenge_handlers[typ]:register_challenge( 585 | challenge.token, 586 | challenge.token .. "." .. self.account_thumbprint, 587 | {...} 588 | ) 589 | if err then 590 | return nil, "error registering challenge: " .. err 591 | end 592 | registered_challenges[registered_challenge_count + 1] = challenge.token 593 | registered_challenge_count = registered_challenge_count + 1 594 | log(ngx_DEBUG, "register challenge ", typ, ": ", challenge.token) 595 | if self.conf.challenge_start_callback then 596 | while not self.conf.challenge_start_callback(typ, challenge.token) do 597 | ngx.sleep(1) 598 | end 599 | end 600 | -- signal server to start challenge check 601 | -- needs to be empty json body rather than empty string 602 | -- https://tools.ietf.org/html/rfc8555#section-7.5.1 603 | local _, _, err = self:post(challenge.url, {}) 604 | if err then 605 | return nil, "error start challenge check: " .. err 606 | end 607 | end 608 | end 609 | ::nextchallenge:: 610 | end 611 | 612 | if registered_challenge_count == 0 and not has_valid_challenge then 613 | return nil, "no challenge is registered and no challenge is valid" 614 | end 615 | 616 | -- Wait until the order is ready 617 | local order_status, err = watch_order_status(self, order_url, "ready") 618 | if not order_status then 619 | return nil, "error checking challenge: " .. err 620 | end 621 | 622 | local domain_pkey, err = openssl.pkey.new(domain_key) 623 | if err then 624 | return nil, "failed to load domain pkey: " .. err 625 | end 626 | 627 | local csr, err = util.create_csr(domain_pkey, ...) 628 | if err then 629 | return nil, "failed to create csr: " .. err 630 | end 631 | 632 | local cert, err = self:finalize(finalize_url, order_url, csr) 633 | if err then 634 | return nil, err 635 | end 636 | 637 | log(ngx_DEBUG, "order is completed: ", order_url) 638 | 639 | for _, token in ipairs(registered_challenges) do 640 | for _, ch in pairs(self.challenge_handlers) do 641 | ch:cleanup_challenge(token, {...}) 642 | end 643 | end 644 | 645 | return cert, nil 646 | end 647 | 648 | function _M:serve_http_challenge() 649 | if self.challenge_handlers["http-01"] then 650 | self.challenge_handlers["http-01"]:serve_challenge() 651 | else 652 | log(ngx_ERR, "http-01 handler is not enabled") 653 | ngx.exit(500) 654 | end 655 | end 656 | 657 | function _M:serve_tls_alpn_challenge() 658 | if self.challenge_handlers["tls-alpn-01"] then 659 | self.challenge_handlers["tls-alpn-01"]:serve_challenge() 660 | else 661 | log(ngx_ERR, "tls-alpn-01 handler is not enabled") 662 | ngx.exit(500) 663 | end 664 | end 665 | 666 | return _M 667 | -------------------------------------------------------------------------------- /lib/resty/acme/dns_provider/cloudflare.lua: -------------------------------------------------------------------------------- 1 | local http = require("resty.http") 2 | local cjson = require("cjson") 3 | 4 | local _M = {} 5 | local mt = {__index = _M} 6 | 7 | function _M.new(token) 8 | if not token or token == "" then 9 | return nil, "api token is needed" 10 | end 11 | 12 | local self = setmetatable({ 13 | endpoint = "https://api.cloudflare.com/client/v4", 14 | httpc = nil, 15 | token = token, 16 | zone = nil, 17 | headers = { 18 | ["Content-Type"] = "application/json", 19 | ["Authorization"] = "Bearer " .. token, 20 | } 21 | }, mt) 22 | 23 | self.httpc = http.new() 24 | return self 25 | end 26 | 27 | local function get_zone_id(self, fqdn) 28 | local url = self.endpoint .. "/zones" 29 | local resp, err = self.httpc:request_uri(url, 30 | { 31 | method = "GET", 32 | headers = self.headers 33 | } 34 | ) 35 | if err then 36 | return nil, err 37 | end 38 | 39 | if resp and resp.status == 200 then 40 | -- { 41 | -- "result": 42 | -- [{ 43 | -- "id":"12345abcde", 44 | -- "name":"domain.com", 45 | -- ... 46 | -- }], 47 | -- "result_info":{"page":1,"per_page":20,"total_pages":1,"count":1,"total_count":1}, 48 | -- "success":true, 49 | -- "errors":[], 50 | -- "messages":[] 51 | -- } 52 | local body = cjson.decode(resp.body) 53 | if not body then 54 | return nil, "json decode error" 55 | end 56 | 57 | ngx.log(ngx.DEBUG, "[cloudflare] find zone ", fqdn, " in ", resp.body) 58 | 59 | for _, zone in ipairs(body.result) do 60 | local start, _, err = fqdn:find(zone.name, 1, true) 61 | if err then 62 | return nil, err 63 | end 64 | if start then 65 | self.zone = zone.name 66 | ngx.log(ngx.DEBUG, "[cloudflare] zone id is ", zone.id, " for domain ", fqdn) 67 | return zone.id 68 | end 69 | end 70 | else 71 | return nil, "get_zone_id: cloudflare returned non 200 status: " .. resp.status .. " body: " .. resp.body 72 | end 73 | 74 | return nil, "no matched dns zone found" 75 | end 76 | 77 | function _M:post_txt_record(fqdn, content) 78 | local zone_id, err = get_zone_id(self, fqdn) 79 | if err then 80 | return nil, "post_txt_record: " .. err 81 | end 82 | local url = self.endpoint .. "/zones/" .. zone_id .. "/dns_records" 83 | local body = { 84 | ["type"] = "TXT", 85 | ["name"] = fqdn, 86 | ["content"] = content 87 | } 88 | local resp, err = self.httpc:request_uri(url, 89 | { 90 | method = "POST", 91 | headers = self.headers, 92 | body = cjson.encode(body) 93 | } 94 | ) 95 | if err then 96 | return nil, err 97 | end 98 | 99 | if resp.status == 400 then 100 | ngx.log(ngx.INFO, "[cloudflare] ignoring possibly fine error: ", resp.body) 101 | return true 102 | elseif resp.status ~= 200 then 103 | return false, "post_txt_record: cloudflare returned non 200 status: " .. resp.status .. " body: " .. resp.body 104 | end 105 | 106 | return true 107 | end 108 | 109 | local function get_record_ids(self, zone_id, fqdn) 110 | local url = self.endpoint .. "/zones/" .. zone_id .. "/dns_records" 111 | local resp, err = self.httpc:request_uri(url, 112 | { 113 | method = "GET", 114 | headers = self.headers 115 | } 116 | ) 117 | if err then 118 | return nil, err 119 | end 120 | 121 | if resp and resp.status == 200 then 122 | -- { 123 | -- "result": 124 | -- [{ 125 | -- "id":"12345abcdefghti", 126 | -- "zone_id":"12345abcde", 127 | -- "zone_name":"domain.com", 128 | -- "name":"_acme-challenge.domain.com", 129 | -- "type":"TXT", 130 | -- "content":"record_content", 131 | -- ... 132 | -- }], 133 | -- "success":true, 134 | -- "errors":[], 135 | -- "messages":[], 136 | -- "result_info":{"page":1,"per_page":100,"count":1,"total_count":1,"total_pages":1} 137 | -- } 138 | local body = cjson.decode(resp.body) 139 | if not body then 140 | return nil, "json decode error" 141 | end 142 | 143 | local ids = {} 144 | for _, record in ipairs(body.result) do 145 | if fqdn == record.name then 146 | ids[#ids+1] = record.id 147 | end 148 | end 149 | return ids 150 | end 151 | 152 | return nil, "no matched dns record found" 153 | end 154 | 155 | function _M:delete_txt_record(fqdn) 156 | local zone_id, err = get_zone_id(self, fqdn) 157 | if err then 158 | return nil, err 159 | end 160 | 161 | local record_ids, err = get_record_ids(self, zone_id, fqdn) 162 | if err then 163 | return nil, err 164 | end 165 | 166 | for _, record_id in ipairs(record_ids) do 167 | local url = self.endpoint .. "/zones/" .. zone_id .. "/dns_records/" .. record_id 168 | local resp, err = self.httpc:request_uri(url, 169 | { 170 | method = "DELETE", 171 | headers = self.headers 172 | } 173 | ) 174 | if err then 175 | return nil, err 176 | end 177 | 178 | if resp.status ~= 200 then 179 | return nil, "delete_txt_record: cloudflare returned non 200 status: " .. resp.status .. " body: " .. resp.body 180 | end 181 | end 182 | 183 | return true 184 | end 185 | 186 | return _M 187 | -------------------------------------------------------------------------------- /lib/resty/acme/dns_provider/dnspod-intl.lua: -------------------------------------------------------------------------------- 1 | local http = require("resty.http") 2 | local cjson = require("cjson") 3 | 4 | local _M = {} 5 | local mt = {__index = _M} 6 | 7 | function _M.new(token) 8 | if not token or token == "" then 9 | return nil, "api token is needed" 10 | end 11 | 12 | local self = setmetatable({ 13 | endpoint = "https://api.dnspod.com/", 14 | httpc = nil, 15 | token = token, 16 | ttl = 600, 17 | headers = { 18 | ["Content-Type"] = "application/json", 19 | ["User-Agent"] = "lua-resty-acme/0.0.0 (noreply@github.com)", 20 | } 21 | }, mt) 22 | 23 | self.httpc = http.new() 24 | return self 25 | end 26 | 27 | local function request(self, uri, body) 28 | body = body or {} 29 | body.login_token = self.token 30 | body.lang = "en" 31 | body.error_on_empty = "no" 32 | 33 | local url = self.endpoint .. "/" .. uri 34 | 35 | local resp, err = self.httpc:request_uri(url, 36 | { 37 | method = "POST", 38 | headers = self.headers, 39 | body = cjson.encode(body) 40 | } 41 | ) 42 | if err then 43 | return nil, err 44 | end 45 | 46 | return resp 47 | end 48 | 49 | local function get_base_domain(domain) 50 | local parts = {} 51 | for part in domain:gmatch("([^.]+)") do 52 | table.insert(parts, part) 53 | end 54 | 55 | local num_parts = #parts 56 | if num_parts <= 2 then 57 | return "@", domain 58 | else 59 | local base_domain = parts[num_parts-1] .. "." .. parts[num_parts] 60 | table.remove(parts, num_parts) 61 | table.remove(parts, num_parts - 1) 62 | local subdomain = table.concat(parts, ".") 63 | return subdomain, base_domain 64 | end 65 | end 66 | 67 | function _M:post_txt_record(fqdn, content) 68 | local sub, base = get_base_domain(fqdn) 69 | 70 | ngx.log(ngx.DEBUG, "[dnspod-intl] base domain is ", base, " subdomain is ", sub) 71 | 72 | local resp, err = request(self, "Record.Create", { 73 | domain = base, 74 | sub_domain = sub, 75 | record_type = "TXT", 76 | record_line = "default", 77 | value = content, 78 | ttl = self.ttl 79 | }) 80 | 81 | if err then 82 | return nil, "post_txt_record: " .. err 83 | end 84 | 85 | if resp.status ~= 200 then 86 | return nil, "post_txt_record: dnspod returned non 200 status: " .. resp.status .. " body: " .. resp.body 87 | end 88 | 89 | return true 90 | end 91 | 92 | local function get_record_id(self, fqdn) 93 | local sub, base = get_base_domain(fqdn) 94 | 95 | ngx.log(ngx.DEBUG, "[dnspod-intl] base domain is ", base, " subdomain is ", sub) 96 | 97 | local resp, err = request(self, "Record.List", { 98 | domain = base, 99 | sub_domain = sub, 100 | }) 101 | 102 | if err then 103 | return nil, "get_record_id: " .. err 104 | end 105 | 106 | local body = cjson.decode(resp.body) 107 | 108 | local records = {} 109 | 110 | for _, record in ipairs(body.records) do 111 | if record.type == "TXT" then 112 | records[#records+1] = record.id 113 | end 114 | end 115 | 116 | return records 117 | end 118 | 119 | function _M:delete_txt_record(fqdn) 120 | local record_ids, err = get_record_id(self, fqdn) 121 | if err then 122 | return nil, "get_record_id: " .. err 123 | end 124 | local _, base = get_base_domain(fqdn) 125 | for _, rec in ipairs(record_ids) do 126 | local resp, err = request(self, "Record.Remove", { 127 | domain = base, 128 | record_id = rec, 129 | }) 130 | 131 | if err then 132 | return nil, err 133 | end 134 | 135 | if resp.status ~= 200 then 136 | return nil, "delete_txt_record: dnspod returned non 200 status: " .. resp.status .. " body: " .. resp.body 137 | end 138 | end 139 | 140 | return true 141 | end 142 | 143 | return _M 144 | -------------------------------------------------------------------------------- /lib/resty/acme/dns_provider/dynv6.lua: -------------------------------------------------------------------------------- 1 | local http = require("resty.http") 2 | local cjson = require("cjson") 3 | 4 | local _M = {} 5 | local mt = {__index = _M} 6 | 7 | function _M.new(token) 8 | if not token or token == "" then 9 | return nil, "api token is needed" 10 | end 11 | 12 | local self = setmetatable({ 13 | endpoint = "https://dynv6.com/api/v2", 14 | httpc = nil, 15 | token = token, 16 | zone = nil, 17 | headers = { 18 | ["Content-Type"] = "application/json", 19 | ["Authorization"] = "Bearer " .. token, 20 | } 21 | }, mt) 22 | 23 | self.httpc = http.new() 24 | return self 25 | end 26 | 27 | local function get_zone_id(self, fqdn) 28 | local url = self.endpoint .. "/zones" 29 | local resp, err = self.httpc:request_uri(url, 30 | { 31 | method = "GET", 32 | headers = self.headers 33 | } 34 | ) 35 | if err then 36 | return nil, err 37 | end 38 | 39 | if resp and resp.status == 200 then 40 | -- [{ 41 | -- "name":"domain.dynv6.net", 42 | -- "ipv4address":"", 43 | -- "ipv6prefix":"", 44 | -- "id":1, 45 | -- "createdAt":"2022-08-14T17:32:57+02:00", 46 | -- "updatedAt":"2022-08-14T17:32:57+02:00" 47 | -- }] 48 | local body = cjson.decode(resp.body) 49 | if not body then 50 | return nil, "json decode error" 51 | end 52 | 53 | for _, zone in ipairs(body) do 54 | local start, _, err = fqdn:find(zone.name, 1, true) 55 | if err then 56 | return nil, err 57 | end 58 | if start then 59 | self.zone = zone.name 60 | return zone.id 61 | end 62 | end 63 | end 64 | 65 | return nil, "no matched dns zone found" 66 | end 67 | 68 | function _M:post_txt_record(fqdn, content) 69 | local zone_id, err = get_zone_id(self, fqdn) 70 | if err then 71 | return nil, "post_txt_record: " .. err 72 | end 73 | local url = self.endpoint .. "/zones/" .. zone_id .. "/records" 74 | local body = { 75 | ["type"] = "TXT", 76 | ["name"] = fqdn:gsub("." .. self.zone, ""), 77 | ["data"] = content 78 | } 79 | local resp, err = self.httpc:request_uri(url, 80 | { 81 | method = "POST", 82 | headers = self.headers, 83 | body = cjson.encode(body) 84 | } 85 | ) 86 | if err then 87 | return nil, err 88 | end 89 | 90 | if resp.status ~= 200 then 91 | return nil, "post_txt_record: dynv6 returned non 200 status: " .. resp.status .. " body: " .. resp.body 92 | end 93 | 94 | return true 95 | end 96 | 97 | local function get_record_ids(self, zone_id, fqdn) 98 | local url = self.endpoint .. "/zones/" .. zone_id .. "/records" 99 | local resp, err = self.httpc:request_uri(url, 100 | { 101 | method = "GET", 102 | headers = self.headers 103 | } 104 | ) 105 | if err then 106 | return nil, err 107 | end 108 | 109 | if resp and resp.status == 200 then 110 | -- [{ 111 | -- "type":"TXT", 112 | -- "name":"_acme-challenge", 113 | -- "data":"record_content", 114 | -- "priority":null, 115 | -- "flags":null, 116 | -- "tag":null, 117 | -- "weight":null, 118 | -- "port":null, 119 | -- "id":1, 120 | -- "zoneID":1 121 | -- }] 122 | local body = cjson.decode(resp.body) 123 | if not body then 124 | return nil, "json decode error" 125 | end 126 | 127 | local ids = {} 128 | for _, record in ipairs(body) do 129 | local start, _, err = fqdn:find(record.name, 1, true) 130 | if err then 131 | return nil, err 132 | end 133 | if start then 134 | ids[#ids+1] = record.id 135 | end 136 | end 137 | return ids 138 | end 139 | 140 | return nil, "no matched dns record found" 141 | end 142 | 143 | function _M:delete_txt_record(fqdn) 144 | local zone_id, err = get_zone_id(self, fqdn) 145 | if err then 146 | return nil, "delete_txt_record: " .. err 147 | end 148 | 149 | local record_ids, err = get_record_ids(self, zone_id, fqdn) 150 | if err then 151 | return nil, "delete_txt_record: " .. err 152 | end 153 | 154 | for _, record_id in ipairs(record_ids) do 155 | local url = self.endpoint .. "/zones/" .. zone_id .. "/records/" .. record_id 156 | local resp, err = self.httpc:request_uri(url, 157 | { 158 | method = "DELETE", 159 | headers = self.headers 160 | } 161 | ) 162 | if err then 163 | return nil, err 164 | end 165 | 166 | if resp.status ~= 200 then 167 | return nil, "delete_txt_record: dynv6 returned non 200 status: " .. resp.status .. " body: " .. resp.body 168 | end 169 | end 170 | 171 | return true 172 | end 173 | 174 | return _M 175 | -------------------------------------------------------------------------------- /lib/resty/acme/eab/zerossl-com.lua: -------------------------------------------------------------------------------- 1 | local http = require("resty.http") 2 | local json = require("cjson") 3 | 4 | local api_uri = "https://api.zerossl.com/acme/eab-credentials-email" 5 | 6 | local function handle(account_email) 7 | local httpc = http.new() 8 | local resp, err = httpc:request_uri(api_uri, 9 | { 10 | method = "POST", 11 | body = "email=" .. ngx.escape_uri(account_email), 12 | headers = { 13 | ['Content-Type'] = "application/x-www-form-urlencoded", 14 | } 15 | } 16 | ) 17 | if err then 18 | return nil, nil, err 19 | end 20 | 21 | local body = json.decode(resp.body) 22 | 23 | if not body['success'] then 24 | return nil, nil, "zerossl.com API error: " .. resp.body 25 | end 26 | 27 | if not body['eab_kid'] or not body['eab_hmac_key'] then 28 | return nil, nil, "zerossl.com API response missing eab_kid or eab_hmac_key: " .. resp.body 29 | end 30 | 31 | return body['eab_kid'], body['eab_hmac_key'] 32 | end 33 | 34 | return { 35 | handle = handle, 36 | } 37 | -------------------------------------------------------------------------------- /lib/resty/acme/openssl.lua: -------------------------------------------------------------------------------- 1 | local ok, ret = pcall(require, "resty.openssl") 2 | 3 | if ok then 4 | local version = require("resty.openssl.version") 5 | ngx.log(ngx.DEBUG, "[acme] using ffi, OpenSSL version linked: ", string.format("%x", version.version_num)) 6 | 7 | return { 8 | pkey = require("resty.openssl.pkey"), 9 | x509 = require("resty.openssl.x509"), 10 | name = require("resty.openssl.x509.name"), 11 | altname = require("resty.openssl.x509.altname"), 12 | csr = require("resty.openssl.x509.csr"), 13 | digest = require("resty.openssl.digest"), 14 | hmac = require("resty.openssl.hmac"), 15 | } 16 | end 17 | 18 | ngx.log(ngx.INFO, "[acme] resty.openssl doesn't load: ", ret) 19 | 20 | local ok, _ = pcall(require, "openssl.pkey") 21 | if ok then 22 | ngx.log(ngx.DEBUG, "[acme] using luaossl") 23 | local tb = { 24 | pkey = require("openssl.pkey"), 25 | x509 = require("openssl.x509"), 26 | name = require("openssl.x509.name"), 27 | altname = require("openssl.x509.altname"), 28 | csr = require("openssl.x509.csr"), 29 | digest = require("openssl.digest"), 30 | hmac = require("openssl.hmac"), 31 | } 32 | 33 | local bn = require("openssl.bignum") 34 | bn.to_binary = bn.toBinary 35 | 36 | tb.pkey.to_PEM = tb.pkey.toPEM 37 | tb.pkey.get_parameters = tb.pkey.getParameters 38 | 39 | tb.csr.set_pubkey = tb.csr.setPublicKey 40 | tb.csr.set_subject_name = tb.csr.setSubject 41 | tb.csr.set_subject_alt = tb.csr.setSubjectAlt 42 | 43 | tb.x509.set_lifetime = tb.x509.setLifetime 44 | end 45 | 46 | error("no openssl binding is usable or installed, requires either lua-resty-openssl or luaossl") 47 | 48 | -------------------------------------------------------------------------------- /lib/resty/acme/storage/README.md: -------------------------------------------------------------------------------- 1 | ## resty.acme.storage 2 | 3 | An storage can be easily plug-and-use as long as it implement the following interface: 4 | 5 | ```lua 6 | local _M = {} 7 | local mt = {__index = _M} 8 | 9 | function _M.new(conf) 10 | local self = setmetatable({}, mt) 11 | return self, err 12 | end 13 | 14 | -- set the key regardless of it's existence 15 | function _M:set(k, v, ttl) 16 | return err 17 | end 18 | 19 | -- set the key only if the key doesn't exist 20 | function _M:add(k, v, ttl) 21 | return err 22 | end 23 | 24 | function _M:delete(k) 25 | return err 26 | end 27 | 28 | function _M:get(k) 29 | -- if key not exist, return nil, nil 30 | return value, err 31 | end 32 | 33 | function _M:list(prefix) 34 | local keys = { "key1", "key2" } 35 | return keys, err 36 | end 37 | 38 | return _M 39 | ``` 40 | -------------------------------------------------------------------------------- /lib/resty/acme/storage/consul.lua: -------------------------------------------------------------------------------- 1 | local http = require "resty.http" 2 | local cjson = require "cjson.safe" 3 | 4 | local _M = {} 5 | local mt = {__index = _M} 6 | 7 | local function valid_consul_key(key) 8 | local newstr, _ = ngx.re.gsub(key, [=[[/]]=], "-") 9 | return newstr 10 | end 11 | 12 | function _M.new(conf) 13 | conf = conf or {} 14 | local base_url = conf.https and "https://" or "http://" 15 | base_url = base_url .. (conf.host or "127.0.0.1") 16 | base_url = base_url .. ":" .. (conf.port or "8500") 17 | 18 | local prefix = conf.kv_path 19 | if not prefix then 20 | prefix = "/acme" 21 | elseif prefix:sub(1, 1) ~= "/" then 22 | prefix = "/" .. prefix 23 | end 24 | base_url = base_url .. "/v1/kv" .. prefix .. "/" 25 | 26 | local self = 27 | setmetatable( 28 | { 29 | timeout = conf.timeout or 2000, 30 | base_url = base_url, 31 | }, 32 | mt 33 | ) 34 | self.headers = { 35 | ["X-Consul-Token"] = conf.token, 36 | } 37 | return self, nil 38 | end 39 | 40 | local function api(self, method, uri, payload) 41 | -- consul don't keepalive, we create a new instance for every request 42 | local client = http:new() 43 | client:set_timeout(self.timeout) 44 | 45 | local res, err = client:request_uri(self.base_url .. uri, { 46 | method = method, 47 | headers = self.headers, 48 | body = payload, 49 | }) 50 | if err then 51 | return nil, err 52 | end 53 | client:close() 54 | 55 | -- return "soft error" for not found 56 | if res.status == 404 then 57 | return nil, nil 58 | end 59 | 60 | -- "true" "false" is also valid through cjson 61 | local decoded, err = cjson.decode(res.body) 62 | if not decoded then 63 | return nil, "unable to decode response body " .. (err or 'nil') 64 | end 65 | return decoded, err 66 | end 67 | 68 | local function set_cas(self, k, v, cas, ttl) 69 | local params = {} 70 | if ttl then 71 | table.insert(params, string.format("flags=%d", (ngx.now() + ttl) * 1000)) 72 | end 73 | if cas then 74 | table.insert(params, string.format("cas=%d", cas)) 75 | end 76 | local uri = valid_consul_key(k) 77 | if #params > 0 then 78 | uri = uri .. "?" .. table.concat(params, "&") 79 | end 80 | local res, err = api(self, "PUT", uri, v) 81 | if not res or err then 82 | return err or "consul returned false" 83 | end 84 | end 85 | 86 | function _M:add(k, v, ttl) 87 | -- update_time is called in get() 88 | -- we don't delete key automatically 89 | local vget, err = self:get(k) 90 | if err then 91 | return "error reading key " .. err 92 | end 93 | if vget then 94 | return "exists" 95 | end 96 | -- do cas for prevent race condition 97 | return set_cas(self, k, v, 0, ttl) 98 | end 99 | 100 | function _M:set(k, v, ttl) 101 | ngx.update_time() 102 | return set_cas(self, k, v, nil, ttl) 103 | end 104 | 105 | function _M:delete(k, cas) 106 | local uri = valid_consul_key(k) 107 | if cas then 108 | uri = uri .. string.format("?cas=%d", cas) 109 | end 110 | local res, err = api(self, "DELETE", uri) 111 | if not res or err then 112 | return err or "delete key failed" 113 | end 114 | end 115 | 116 | function _M:get(k) 117 | local res, err = api(self, 'GET', valid_consul_key(k)) 118 | ngx.update_time() 119 | if err then 120 | return nil, err 121 | elseif not res or not res[1] or not res[1]["Value"] then 122 | return nil, nil 123 | elseif res[1]["Flags"] and res[1]["Flags"] > 0 and res[1]["Flags"] < ngx.now() * 1000 then 124 | err = self:delete(k, res[1]["ModifyIndex"]) 125 | if err then 126 | return nil, "error cleanup expired key ".. err 127 | end 128 | return nil, nil 129 | end 130 | if res[1]["Value"] == ngx.null then 131 | return nil, err 132 | end 133 | return ngx.decode_base64(res[1]["Value"]), err 134 | end 135 | 136 | local empty_table = {} 137 | function _M:list(prefix) 138 | local res, err = api(self, 'GET', '?keys') 139 | if err then 140 | return nil, err 141 | elseif not res then 142 | return empty_table, nil 143 | end 144 | local ret = {} 145 | local prefix_length = #prefix 146 | for _, key in ipairs(res) do 147 | local key, _ = ngx.re.match(key, [[([^/]+)$]], "jo") 148 | if key then 149 | key = key[1] 150 | if key:sub(1, prefix_length) == prefix then 151 | table.insert(ret, key) 152 | end 153 | end 154 | end 155 | return ret, nil 156 | end 157 | 158 | return _M 159 | -------------------------------------------------------------------------------- /lib/resty/acme/storage/etcd.lua: -------------------------------------------------------------------------------- 1 | local etcd = require "resty.etcd" 2 | 3 | local _M = {} 4 | local mt = {__index = _M} 5 | 6 | function _M.new(conf) 7 | conf = conf or {} 8 | local self = setmetatable({}, mt) 9 | 10 | if conf.protocol and conf.protocol ~= "v3" then 11 | return nil, "only v3 protocol is supported" 12 | end 13 | 14 | local options = { 15 | http_host = conf.http_host or "http://127.0.0.1:4001", 16 | key_prefix = conf.key_prefix or "", 17 | timeout = conf.timeout or 60, 18 | ssl_verify = conf.ssl_verify, 19 | protocol = "v3", 20 | } 21 | 22 | local client, err = etcd.new(options) 23 | if err then 24 | return nil, err 25 | end 26 | 27 | self.client = client 28 | return self, nil 29 | end 30 | 31 | local function grant(self, ttl) 32 | local res, err = self.client:grant(ttl) 33 | if err then 34 | return nil, err 35 | end 36 | return res.body.ID 37 | end 38 | 39 | -- set the key regardless of it's existence 40 | function _M:set(k, v, ttl) 41 | k = "/" .. k 42 | 43 | local lease_id, err 44 | if ttl then 45 | lease_id, err = grant(self, ttl) 46 | if err then 47 | return err 48 | end 49 | end 50 | 51 | local _, err = self.client:set(k, v, { lease = lease_id }) 52 | if err then 53 | return err 54 | end 55 | end 56 | 57 | -- set the key only if the key doesn't exist 58 | -- Note: the key created by etcd:setnx can't be attached to a lease later, it seems to be a bug 59 | function _M:add(k, v, ttl) 60 | k = "/" .. k 61 | 62 | local lease_id, err 63 | if ttl then 64 | lease_id, err = grant(self, ttl) 65 | if err then 66 | return err 67 | end 68 | end 69 | 70 | 71 | local compare = { 72 | { 73 | key = k, 74 | target = "CREATE", 75 | create_revision = 0, 76 | } 77 | } 78 | 79 | local success = { 80 | { 81 | requestPut = { 82 | key = k, 83 | value = v, 84 | lease = lease_id, 85 | } 86 | } 87 | } 88 | 89 | local v, err = self.client:txn(compare, success) 90 | if err then 91 | return nil, err 92 | elseif v and v.body and not v.body.succeeded then 93 | return "exists" 94 | end 95 | end 96 | 97 | function _M:delete(k) 98 | k = "/" .. k 99 | local _, err = self.client:delete(k) 100 | if err then 101 | return err 102 | end 103 | end 104 | 105 | function _M:get(k) 106 | k = "/" .. k 107 | local res, err = self.client:get(k) 108 | if err then 109 | return nil, err 110 | elseif res and res.body.kvs == nil then 111 | return nil, nil 112 | elseif res.status ~= 200 then 113 | return nil, "etcd returned status " .. res.status 114 | end 115 | local node = res.body.kvs[1] 116 | if not node then -- would this ever happen? 117 | return nil, nil 118 | end 119 | return node.value 120 | end 121 | 122 | local empty_table = {} 123 | function _M:list(prefix) 124 | local res, err = self.client:readdir("/" .. prefix) 125 | if err then 126 | return nil, err 127 | elseif not res or not res.body or not res.body.kvs then 128 | return empty_table, nil 129 | end 130 | local ret = {} 131 | -- offset 1 to strip leading "/" in original key 132 | local prefix_length = #prefix + 1 133 | for _, node in ipairs(res.body.kvs) do 134 | local key = node.key 135 | if key then 136 | -- start from 2 to strip leading "/" 137 | if key:sub(2, prefix_length) == prefix then 138 | table.insert(ret, key:sub(2)) 139 | end 140 | end 141 | end 142 | return ret, nil 143 | end 144 | 145 | return _M 146 | -------------------------------------------------------------------------------- /lib/resty/acme/storage/file.lua: -------------------------------------------------------------------------------- 1 | local ok, lfs = pcall(require, 'lfs_ffi') 2 | if not ok then 3 | local _ 4 | _, lfs = pcall(require, 'lfs') 5 | end 6 | 7 | local _M = {} 8 | local mt = {__index = _M} 9 | 10 | local TTL_SEPERATOR = '::' 11 | local TTL_PATTERN = "(%d+)" .. TTL_SEPERATOR .. "(.+)" 12 | 13 | function _M.new(conf) 14 | local dir = conf and conf.dir 15 | dir = dir or os.getenv("TMPDIR") or '/tmp' 16 | 17 | local self = 18 | setmetatable( 19 | { 20 | dir = dir 21 | }, 22 | mt 23 | ) 24 | return self 25 | end 26 | 27 | local function regulate_filename(dir, s) 28 | -- TODO: not windows friendly 29 | return dir .. "/" .. ngx.encode_base64(s) 30 | end 31 | 32 | local function exists(f) 33 | -- TODO: check for existence, not just able to open or not 34 | local f, err = io.open(f, "rb") 35 | if f then 36 | f:close() 37 | end 38 | return err == nil 39 | end 40 | 41 | local function split_ttl(s) 42 | local _, _, ttl, value = string.find(s, TTL_PATTERN) 43 | 44 | return tonumber(ttl), value 45 | end 46 | 47 | local function check_expiration(f) 48 | if not exists(f) then 49 | return 50 | end 51 | 52 | local file, err = io.open(f, "rb") 53 | if err then 54 | return nil, err 55 | end 56 | 57 | local output, err = file:read("*a") 58 | file:close() 59 | 60 | if err then 61 | return nil, err 62 | end 63 | 64 | local ttl, value = split_ttl(output) 65 | 66 | -- ttl is nil meaning the file is corrupted or in legacy format 67 | -- ttl = 0 means the key never expires 68 | if not ttl or (ttl > 0 and ngx.time() - ttl >= 0) then 69 | os.remove(f) 70 | else 71 | return value 72 | end 73 | end 74 | 75 | function _M:add(k, v, ttl) 76 | local f = regulate_filename(self.dir, k) 77 | 78 | local check = check_expiration(f) 79 | if check then 80 | return "exists" 81 | end 82 | 83 | return self:set(k, v, ttl) 84 | end 85 | 86 | function _M:set(k, v, ttl) 87 | local f = regulate_filename(self.dir, k) 88 | 89 | -- remove old keys if it's expired 90 | check_expiration(f) 91 | 92 | if ttl then 93 | ttl = math.floor(ttl + ngx.time()) 94 | else 95 | ttl = 0 96 | end 97 | 98 | local file, err = io.open(f, "wb") 99 | if err then 100 | return err 101 | end 102 | local _, err = file:write(ttl .. TTL_SEPERATOR .. v) 103 | if err then 104 | return err 105 | end 106 | file:close() 107 | end 108 | 109 | function _M:delete(k) 110 | local f = regulate_filename(self.dir, k) 111 | if not exists(f) then 112 | return nil, nil 113 | end 114 | local _, err = os.remove(f) 115 | if err then 116 | return err 117 | end 118 | end 119 | 120 | function _M:get(k) 121 | local f = regulate_filename(self.dir, k) 122 | 123 | local value, err = check_expiration(f) 124 | if err then 125 | return nil, err 126 | elseif value then 127 | return value, nil 128 | else 129 | return nil 130 | end 131 | end 132 | 133 | function _M:list(prefix) 134 | if not lfs then 135 | return {}, "lfs_ffi needed for file:list" 136 | end 137 | 138 | local files = {} 139 | 140 | local prefix_len = prefix and #prefix or 0 141 | 142 | for file in lfs.dir(self.dir) do 143 | file = ngx.decode_base64(file) 144 | if not file then 145 | goto nextfile 146 | end 147 | if prefix_len == 0 or string.sub(file, 1, prefix_len) == prefix then 148 | table.insert(files, file) 149 | end 150 | ::nextfile:: 151 | end 152 | return files, nil 153 | end 154 | 155 | return _M 156 | -------------------------------------------------------------------------------- /lib/resty/acme/storage/redis.lua: -------------------------------------------------------------------------------- 1 | local redis = require "resty.redis" 2 | local util = require "resty.acme.util" 3 | local fmt = string.format 4 | local log = util.log 5 | local ngx_ERR = ngx.ERR 6 | local unpack = unpack 7 | 8 | local _M = {} 9 | local mt = {__index = _M} 10 | 11 | function _M.new(conf) 12 | conf = conf or {} 13 | local self = 14 | setmetatable( 15 | { 16 | host = conf.host or '127.0.0.1', 17 | port = conf.port or 6379, 18 | database = conf.database, 19 | auth = conf.auth, 20 | ssl = conf.ssl or false, 21 | ssl_verify = conf.ssl_verify or false, 22 | ssl_server_name = conf.ssl_server_name, 23 | namespace = conf.namespace or "", 24 | scan_count = conf.scan_count or 10, 25 | username = conf.username, 26 | password = conf.password, 27 | }, 28 | mt 29 | ) 30 | return self, nil 31 | end 32 | 33 | local function op(self, op, ...) 34 | local ok, err 35 | local client = redis:new() 36 | client:set_timeouts(1000, 1000, 1000) -- 1 sec 37 | 38 | local sock_opts = { 39 | ssl = self.ssl, 40 | ssl_verify = self.ssl_verify, 41 | server_name = self.ssl_server_name, 42 | } 43 | ok, err = client:connect(self.host, self.port, sock_opts) 44 | if not ok then 45 | return nil, err 46 | end 47 | 48 | if self.username and self.password then 49 | local _, err = client:auth(self.username, self.password) 50 | if err then 51 | return nil, "authentication failed " .. err 52 | end 53 | elseif self.password then 54 | local _, err = client:auth(self.password) 55 | if err then 56 | return nil, "authentication failed " .. err 57 | end 58 | elseif self.auth then 59 | local _, err = client:auth(self.auth) 60 | if err then 61 | return nil, "authentication failed " .. err 62 | end 63 | end 64 | 65 | if self.database then 66 | ok, err = client:select(self.database) 67 | if not ok then 68 | return nil, "can't select database " .. err 69 | end 70 | end 71 | 72 | ok, err = client[op](client, ...) 73 | client:close() 74 | return ok, err 75 | end 76 | 77 | local function remove_namespace(namespace, keys) 78 | if namespace == "" then 79 | return keys 80 | else 81 | -- 82 | local len = #namespace 83 | local start = len + 1 84 | for k, v in ipairs(keys) do 85 | if v:sub(1, len) == namespace then 86 | keys[k] = v:sub(start) 87 | else 88 | local msg = fmt("found a key '%s', expected to be prefixed with namespace '%s'", 89 | v, namespace) 90 | log(ngx_ERR, msg) 91 | end 92 | end 93 | 94 | return keys 95 | end 96 | end 97 | 98 | function _M:add(k, v, ttl) 99 | k = self.namespace .. k 100 | local ok, err 101 | if ttl then 102 | ok, err = op(self, 'set', k, v, "nx", "px", math.floor(ttl * 1000)) 103 | else 104 | ok, err = op(self, 'set', k, v, "nx") 105 | end 106 | if err then 107 | return err 108 | elseif ok == ngx.null then 109 | return "exists" 110 | end 111 | end 112 | 113 | function _M:set(k, v, ttl) 114 | k = self.namespace .. k 115 | local err, _ 116 | if ttl then 117 | _, err = op(self, 'set', k, v, "px", math.floor(ttl * 1000)) 118 | else 119 | _, err = op(self, 'set', k, v) 120 | end 121 | if err then 122 | return err 123 | end 124 | end 125 | 126 | function _M:delete(k) 127 | k = self.namespace .. k 128 | local _, err = op(self, 'del', k) 129 | if err then 130 | return err 131 | end 132 | end 133 | 134 | function _M:get(k) 135 | k = self.namespace .. k 136 | local res, err = op(self, 'get', k) 137 | if res == ngx.null then 138 | return nil, err 139 | end 140 | return res, err 141 | end 142 | 143 | local empty_table = {} 144 | function _M:list(prefix) 145 | prefix = prefix or "" 146 | prefix = self.namespace .. prefix 147 | 148 | local cursor = "0" 149 | local data = {} 150 | local res, err 151 | 152 | repeat 153 | res, err = op(self, 'scan', cursor, 'match', prefix .. "*", 'count', self.scan_count) 154 | 155 | if not res or res == ngx.null then 156 | return empty_table, err 157 | end 158 | 159 | local keys 160 | cursor, keys = unpack(res) 161 | 162 | for i=1,#keys do 163 | data[#data+1] = keys[i] 164 | end 165 | 166 | until cursor == "0" 167 | 168 | return remove_namespace(self.namespace, data), err 169 | end 170 | 171 | return _M 172 | -------------------------------------------------------------------------------- /lib/resty/acme/storage/shm.lua: -------------------------------------------------------------------------------- 1 | local _M = {} 2 | local mt = {__index = _M} 3 | 4 | local table_remove = table.remove 5 | 6 | function _M.new(conf) 7 | if not conf or not conf.shm_name then 8 | return nil, "conf.shm_name must be provided" 9 | end 10 | 11 | if not ngx.shared[conf.shm_name] then 12 | return nil, "shm " .. conf.shm_name .. " is not defined" 13 | end 14 | 15 | local self = 16 | setmetatable( 17 | { 18 | shm = ngx.shared[conf.shm_name] 19 | }, 20 | mt 21 | ) 22 | return self 23 | end 24 | 25 | function _M:add(k, v, ttl) 26 | local _, err = self.shm:add(k, v, ttl) 27 | return err 28 | end 29 | 30 | function _M:set(k, v, ttl) 31 | local _, err = self.shm:set(k, v, ttl) 32 | return err 33 | end 34 | 35 | function _M:delete(k) 36 | local _, err = self.shm:delete(k) 37 | return err 38 | end 39 | 40 | function _M:get(k) 41 | return self.shm:get(k) 42 | end 43 | 44 | function _M:list(prefix) 45 | local keys = self.shm:get_keys(0) 46 | if prefix then 47 | local prefix_length = #prefix 48 | for i=#keys, 1, -1 do 49 | if keys[i]:sub(1, prefix_length) ~= prefix then 50 | table_remove(keys, i) 51 | end 52 | end 53 | end 54 | return keys, nil 55 | end 56 | 57 | return _M 58 | -------------------------------------------------------------------------------- /lib/resty/acme/storage/vault.lua: -------------------------------------------------------------------------------- 1 | local http = require "resty.http" 2 | local cjson = require "cjson.safe" 3 | 4 | local _M = {} 5 | local mt = {__index = _M} 6 | local auth 7 | 8 | local function valid_vault_key(key) 9 | local newstr, _ = ngx.re.gsub(key, [=[[/]]=], "-") 10 | return newstr 11 | end 12 | 13 | function _M.new(conf) 14 | conf = conf or {} 15 | 16 | local prefix = conf.kv_path 17 | if not prefix then 18 | prefix = "/acme" 19 | elseif prefix:sub(1, 1) ~= "/" then 20 | prefix = "/" .. prefix 21 | end 22 | local mount, err = ngx.re.match(prefix, "(/[^/]+)") 23 | if err then 24 | return err 25 | end 26 | mount = mount[0] 27 | local path = prefix:sub(#mount+1) 28 | local metadata_url = "/v1" .. mount .. "/metadata" .. path .. "/" 29 | local data_url = "/v1" .. mount .. "/data" .. path .. "/" 30 | 31 | local tls_verify = conf.tls_verify 32 | if tls_verify == nil then 33 | tls_verify = true 34 | end 35 | 36 | local self = 37 | setmetatable( 38 | { 39 | host = conf.host or "127.0.0.1", 40 | port = conf.port or 8200, 41 | auth_method = string.lower(conf.auth_method or "token"), 42 | auth_path = conf.auth_path or "kubernetes", 43 | auth_role = conf.auth_role, 44 | jwt_path = conf.jwt_path or "/var/run/secrets/kubernetes.io/serviceaccount/token", 45 | https = conf.https, 46 | tls_verify = tls_verify, 47 | tls_server_name = conf.tls_server_name, 48 | timeout = conf.timeout or 2000, 49 | data_url = data_url, 50 | metadata_url = metadata_url, 51 | namespace = conf.namespace, 52 | }, 53 | mt 54 | ) 55 | 56 | local token, err = auth(self, conf) 57 | 58 | if err then 59 | return nil, err 60 | end 61 | 62 | self.headers = { 63 | ["X-Vault-Token"] = token 64 | } 65 | 66 | if self.https then 67 | if not self.tls_server_name then 68 | self.tls_server_name = self.host 69 | end 70 | self.headers["Host"] = self.tls_server_name 71 | end 72 | return self, nil 73 | end 74 | 75 | local function api(self, method, uri, payload) 76 | local _, err 77 | -- vault don't keepalive, we create a new instance for every request 78 | local client = http:new() 79 | client:set_timeout(self.timeout) 80 | 81 | local payload = payload and cjson.encode(payload) 82 | 83 | _, err = client:connect(self.host, self.port) 84 | if err then 85 | return nil, err 86 | end 87 | if self.https then 88 | local _, err = client:ssl_handshake(nil, self.tls_server_name, self.tls_verify) 89 | if err then 90 | return nil, "unable to SSL handshake with vault server: " .. err 91 | end 92 | end 93 | 94 | if self.namespace then 95 | self.headers["X-Vault-Namespace"] = self.namespace 96 | end 97 | 98 | local res, err = client:request({ 99 | path = uri, 100 | method = method, 101 | headers = self.headers, 102 | body = payload, 103 | }) 104 | if err then 105 | return nil, err 106 | end 107 | 108 | -- return "soft error" for not found and successful delete 109 | if res.status == 404 or res.status == 204 then 110 | client:close() 111 | return nil, nil 112 | end 113 | 114 | local body, err = res:read_body() 115 | if err then 116 | client:close() 117 | return nil, "unable to read response body: " .. err 118 | end 119 | 120 | -- "true" "false" is also valid through cjson 121 | local decoded, err = cjson.decode(body) 122 | if not decoded then 123 | client:close() 124 | return nil, "unable to decode response body: " .. (err or 'nil') .. "body: " .. (body or 'nil') 125 | end 126 | 127 | client:close() 128 | if decoded.errors then 129 | return nil, "errors from vault: " .. cjson.encode(decoded.errors) 130 | end 131 | 132 | return decoded, err 133 | end 134 | 135 | function auth(self, conf) 136 | if self.auth_method == "token" then 137 | return conf.token, nil 138 | elseif self.auth_method ~= "kubernetes" then 139 | return nil, "Unknown authentication method" 140 | end 141 | 142 | local file, err = io.open(self.jwt_path, "r") 143 | 144 | if err then 145 | return nil, err 146 | end 147 | 148 | local token = file:read("*all") 149 | file:close() 150 | 151 | local response, err = api(self, "POST", "/v1/auth/" .. self.auth_path .. "/login", { 152 | role = self.auth_role, 153 | jwt = token 154 | }) 155 | 156 | if err then 157 | return nil, err 158 | end 159 | 160 | if not response["auth"] or not response["auth"]["client_token"] then 161 | return nil, "Could not authenticate" 162 | end 163 | 164 | return response["auth"]["client_token"] 165 | end 166 | 167 | local function set_cas(self, k, v, cas, ttl) 168 | if ttl then 169 | if ttl > 0 and ttl < 1 then 170 | ngx.log(ngx.WARN, "vault doesn't support ttl less than 1s, will use 1s") 171 | ttl = 1 172 | end 173 | -- first update the metadata 174 | local _, err = api(self, "POST", self.metadata_url .. valid_vault_key(k), { 175 | delete_version_after = string.format("%dms", ttl * 1000) 176 | }) 177 | -- vault doesn't seem return any useful info in this api ? 178 | if err then 179 | return err 180 | end 181 | end 182 | 183 | local payload = { 184 | data = { 185 | value = v, 186 | note = "managed by lua-resty-acme", 187 | } 188 | } 189 | if cas then 190 | payload.options = { 191 | cas = cas, 192 | } 193 | end 194 | local res, err = api(self, "POST", self.data_url .. valid_vault_key(k), payload) 195 | if not res or err then 196 | return err or "set key failed" 197 | end 198 | return nil 199 | end 200 | 201 | local function get(self, k) 202 | local res, err = api(self, 'GET', self.data_url .. valid_vault_key(k)) 203 | if err then 204 | return nil, err 205 | elseif not res or not res["data"] or not res["data"]["data"] 206 | or not res["data"]["data"]["value"] then 207 | return nil, nil 208 | end 209 | return res['data'], nil 210 | end 211 | 212 | function _M:add(k, v, ttl) 213 | -- we don't delete key automatically 214 | local vget, err = get(self, k) 215 | if err then 216 | return "error reading key " .. err 217 | end 218 | local revision 219 | -- if there's no 'data' meaning all versions are gone, then we are good 220 | if vget then 221 | if vget['data'] then 222 | return "exists" 223 | end 224 | revision = vget['metadata'] and vget['metadata']['version'] or 0 225 | end 226 | ngx.update_time() 227 | -- do cas for prevent race condition 228 | return set_cas(self, k, v, revision, ttl) 229 | end 230 | 231 | function _M:set(k, v, ttl) 232 | ngx.update_time() 233 | return set_cas(self, k, v, nil, ttl) 234 | end 235 | 236 | function _M:delete(k, cas) 237 | -- delete metadata will delete all versions of secret as well 238 | local _, err = api(self, "DELETE", self.metadata_url .. valid_vault_key(k)) 239 | if err then 240 | return "delete key failed" 241 | end 242 | end 243 | 244 | function _M:get(k) 245 | local v, err = get(self, k) 246 | if err then 247 | return nil, err 248 | end 249 | return v and v["data"]["value"], err 250 | end 251 | 252 | local empty_table = {} 253 | function _M:list(prefix) 254 | local res, err = api(self, 'LIST', self.metadata_url) 255 | if err then 256 | return nil, err 257 | elseif not res or not res['data'] or not res['data']['keys'] then 258 | return empty_table, nil 259 | end 260 | local ret = {} 261 | local prefix_length = #prefix 262 | for _, key in ipairs(res['data']['keys']) do 263 | local key, _ = ngx.re.match(key, [[([^/]+)$]], "jo") 264 | if key then 265 | key = key[1] 266 | if key:sub(1, prefix_length) == prefix then 267 | table.insert(ret, key) 268 | end 269 | end 270 | end 271 | return ret, nil 272 | end 273 | 274 | return _M 275 | -------------------------------------------------------------------------------- /lib/resty/acme/util.lua: -------------------------------------------------------------------------------- 1 | local reverse = string.reverse 2 | local find = string.find 3 | local sub = string.sub 4 | local openssl = require("resty.acme.openssl") 5 | 6 | -- https://tools.ietf.org/html/rfc8555 Page 10 7 | -- Binary fields in the JSON objects used by acme are encoded using 8 | -- base64url encoding described in Section 5 of [RFC4648] according to 9 | -- the profile specified in JSON Web Signature in Section 2 of 10 | -- [RFC7515]. This encoding uses a URL safe character set. Trailing 11 | -- '=' characters MUST be stripped. Encoded values that include 12 | -- trailing '=' characters MUST be rejected as improperly encoded. 13 | local base64 = require("ngx.base64") 14 | local encode_base64url = base64.encode_base64url 15 | -- -- Fallback if resty.core is not available 16 | -- encode_base64url = function (s) 17 | -- return ngx.encode_base64(s):gsub("/", "_"):gsub("+", "-"):gsub("[= ]", "") 18 | -- end 19 | local decode_base64url = base64.decode_base64url 20 | local errlog = require("ngx.errlog") 21 | 22 | -- https://tools.ietf.org/html/rfc7638 23 | local function thumbprint(pkey) 24 | local params = pkey:get_parameters() 25 | if not params then 26 | return nil, "could not extract account key parameters." 27 | end 28 | 29 | local jwk_ordered = 30 | string.format( 31 | '{"e":"%s","kty":"%s","n":"%s"}', 32 | encode_base64url(params.e:to_binary()), 33 | "RSA", 34 | encode_base64url(params.n:to_binary()) 35 | ) 36 | local digest = openssl.digest.new("SHA256"):final(jwk_ordered) 37 | return encode_base64url(digest), nil 38 | end 39 | 40 | local function create_csr(domain_pkey, ...) 41 | local domains = {...} 42 | 43 | 44 | local subject = openssl.name.new() 45 | local _, err = subject:add("CN", domains[1]) 46 | if err then 47 | return nil, "failed to add subject name: " .. err 48 | end 49 | 50 | local alt, err 51 | -- add subject name to altname as well, some implementaions 52 | -- like ZeroSSL requires that 53 | if #{...} > 0 then 54 | alt, err = openssl.altname.new() 55 | if err then 56 | return nil, err 57 | end 58 | 59 | for _, domain in pairs(domains) do 60 | _, err = alt:add("DNS", domain) 61 | if err then 62 | return nil, "failed to add altname: " .. err 63 | end 64 | end 65 | end 66 | 67 | local csr = openssl.csr.new() 68 | _, err = csr:set_subject_name(subject) 69 | if err then 70 | return nil, "failed to set_subject_name: " .. err 71 | end 72 | if alt then 73 | _, err = csr:set_subject_alt_name(alt) 74 | if err then 75 | return nil, "failed to set_subject_alt: " .. err 76 | end 77 | end 78 | 79 | _, err = csr:set_pubkey(domain_pkey) 80 | if err then 81 | return nil, "failed to set_pubkey: " .. err 82 | end 83 | 84 | _, err = csr:sign(domain_pkey) 85 | if err then 86 | return nil, "failed to sign: " .. err 87 | end 88 | 89 | return csr:tostring("DER"), nil 90 | end 91 | 92 | local function create_pkey(bits, typ, curve) 93 | bits = bits or 4096 94 | typ = typ or 'RSA' 95 | local pkey = openssl.pkey.new({ 96 | bits = bits, 97 | type = typ, 98 | curve = curve, 99 | }) 100 | 101 | return pkey:to_PEM('private') 102 | end 103 | 104 | local pem_cert_header = "-----BEGIN CERTIFICATE-----" 105 | local pem_cert_footer = "-----END CERTIFICATE-----" 106 | local function check_chain_root_issuer(chain_pem, issuer_name) 107 | -- we find the last pem cert block in the chain file 108 | local pos = 0 109 | local pem 110 | while true do 111 | local newpos = chain_pem:find(pem_cert_header, pos + 1, true) 112 | if not newpos then 113 | local endpos = chain_pem:find(pem_cert_footer, pos+1, true) 114 | if endpos then 115 | pem = chain_pem:sub(pos, endpos+#pem_cert_footer-1) 116 | end 117 | break 118 | end 119 | pos = newpos 120 | end 121 | 122 | if pem then 123 | local cert, err = openssl.x509.new(pem) 124 | if err then 125 | return false, err 126 | end 127 | local name, err = cert:get_issuer_name() 128 | if err then 129 | return false, err 130 | end 131 | local cn,_, err = name:find("CN") 132 | if err then 133 | return false, err 134 | end 135 | if cn then 136 | if cn.blob == issuer_name then 137 | return true 138 | else 139 | return false, "current chain root issuer common name is \"" .. cn.blob .. "\"" 140 | end 141 | end 142 | end 143 | 144 | return false, "cert not found in PEM chain" 145 | end 146 | 147 | local function log(lvl, ...) 148 | -- log to error logs with our custom prefix, stack level 149 | -- and separator 150 | local n = select("#", ...) 151 | local t = { ... } 152 | local info = debug.getinfo(2, "Sl") 153 | 154 | -- kong: kong/pdk/log 155 | local short_src = info.short_src 156 | if short_src then 157 | local rev_src = reverse(short_src) 158 | local idx = find(rev_src, "/", nil, true) 159 | if idx then 160 | short_src = sub(short_src, #rev_src - idx + 2) 161 | end 162 | end 163 | 164 | local prefix = string.format("[acme] %s:%d: ", short_src, info.currentline) 165 | local buf = { prefix } 166 | 167 | for i = 1, n do 168 | buf[i + 1] = tostring(t[i]) 169 | end 170 | 171 | local msg = table.concat(buf, "") 172 | 173 | errlog.raw_log(lvl, msg) 174 | end 175 | 176 | return { 177 | encode_base64url = encode_base64url, 178 | decode_base64url = decode_base64url, 179 | thumbprint = thumbprint, 180 | create_csr = create_csr, 181 | create_pkey = create_pkey, 182 | check_chain_root_issuer = check_chain_root_issuer, 183 | log = log, 184 | } 185 | -------------------------------------------------------------------------------- /lua-resty-acme-0.15.0-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-acme" 2 | version = "0.15.0-1" 3 | source = { 4 | url = "git+https://github.com/fffonion/lua-resty-acme.git", 5 | tag = "0.15.0" 6 | } 7 | description = { 8 | summary = "Automatic Let's Encrypt certificate serving and Lua implementation of ACME procotol", 9 | detailed = [[ 10 | Automatic Let's Encrypt certificate serving (RSA + ECC) and Lua implementation of the ACME protocol. 11 | This library consits of two parts: 12 | 13 | - `resty.acme.autossl`: automatic lifecycle management of Let's Encrypt certificates 14 | - `resty.acme.client`: Lua implementation of ACME v2 protocol 15 | ]], 16 | homepage = "https://github.com/fffonion/lua-resty-acme", 17 | license = "BSD" 18 | } 19 | build = { 20 | type = "builtin", 21 | modules = { 22 | ["resty.acme.autossl"] = "lib/resty/acme/autossl.lua", 23 | ["resty.acme.challenge.dns-01"] = "lib/resty/acme/challenge/dns-01.lua", 24 | ["resty.acme.challenge.http-01"] = "lib/resty/acme/challenge/http-01.lua", 25 | ["resty.acme.challenge.tls-alpn-01"] = "lib/resty/acme/challenge/tls-alpn-01.lua", 26 | ["resty.acme.client"] = "lib/resty/acme/client.lua", 27 | ["resty.acme.dns_provider.cloudflare"] = "lib/resty/acme/dns_provider/cloudflare.lua", 28 | ["resty.acme.dns_provider.dynv6"] = "lib/resty/acme/dns_provider/dynv6.lua", 29 | ["resty.acme.eab.zerossl-com"] = "lib/resty/acme/eab/zerossl-com.lua", 30 | ["resty.acme.openssl"] = "lib/resty/acme/openssl.lua", 31 | ["resty.acme.storage.consul"] = "lib/resty/acme/storage/consul.lua", 32 | ["resty.acme.storage.etcd"] = "lib/resty/acme/storage/etcd.lua", 33 | ["resty.acme.storage.file"] = "lib/resty/acme/storage/file.lua", 34 | ["resty.acme.storage.redis"] = "lib/resty/acme/storage/redis.lua", 35 | ["resty.acme.storage.shm"] = "lib/resty/acme/storage/shm.lua", 36 | ["resty.acme.storage.vault"] = "lib/resty/acme/storage/vault.lua", 37 | ["resty.acme.util"] = "lib/resty/acme/util.lua" 38 | } 39 | } 40 | 41 | dependencies = { 42 | "lua-resty-http >= 0.15-0", 43 | "lua-resty-openssl >= 0.7.0", 44 | -- "luafilesystem ~> 1", 45 | } 46 | -------------------------------------------------------------------------------- /scripts/prepare_new_release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -v 2 | 3 | new_v=$1 4 | if [[ -z $1 ]]; then 5 | echo "Usage: $0 version" 6 | exit 1 7 | fi 8 | 9 | if [[ $(uname) == "Darwin" ]]; then 10 | SED=gsed 11 | else 12 | SED=sed 13 | fi 14 | 15 | git reset 16 | old_rockspec=$(ls *.rockspec -r1|grep -v dev|grep -v "$new_v"|head -n1) 17 | old_v=$(echo $old_rockspec | cut -d '-' -f4) 18 | if [[ -z "$old_v" ]]; then 19 | echo "Unknown old version" 20 | exit 1 21 | fi 22 | 23 | echo "Creating new release $new_v from $old_v" 24 | git branch -D release/${new_v} 25 | git checkout -b release/${new_v} 26 | 27 | # rockspec 28 | new_rockspec="${old_rockspec/$old_v/$new_v}" 29 | cp "$old_rockspec" "$new_rockspec" 30 | git rm "$old_rockspec" 31 | $SED -i "s/$old_v/$new_v/g" "$new_rockspec" 32 | git add "$new_rockspec" 33 | 34 | # file 35 | $SED -i "s/_VERSION = '$old_v'/_VERSION = '$new_v'/g" lib/resty/acme/client.lua 36 | git add -u 37 | 38 | # changelog 39 | git commit -m "release: $new_v" 40 | git tag "$new_v" 41 | git-chglog --output CHANGELOG.md 42 | git add -u 43 | git tag -d "$new_v" 44 | git commit -m "release: $new_v" --amend 45 | 46 | -------------------------------------------------------------------------------- /t/autossl.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et fdm=marker: 2 | 3 | use Test::Nginx::Socket::Lua 'no_plan'; 4 | use Cwd qw(cwd); 5 | 6 | 7 | my $pwd = cwd(); 8 | 9 | our $HttpConfig = qq{ 10 | lua_package_path "$pwd/lib/?.lua;$pwd/lib/?/init.lua;$pwd/../lib/?.lua;$pwd/../lib/?/init.lua;;"; 11 | }; 12 | 13 | our $TestConfig = q{ 14 | location /t { 15 | content_by_lua_block { 16 | require("resty.acme.autossl").init({ 17 | tos_accepted = true, 18 | account_email = "youemail@youdomain.com", 19 | domain_whitelist = { "example.com" }, 20 | storage_adapter = "redis", 21 | storage_config = { 22 | namespace = ngx.var.uri:sub(4), 23 | } 24 | }) 25 | } 26 | } 27 | }; 28 | 29 | run_tests(); 30 | 31 | __DATA__ 32 | === TEST 1: should fail if namespace is prefixed with update_lock: 33 | --- http_config eval: $::HttpConfig 34 | --- config eval: $::TestConfig 35 | --- request 36 | GET /t/update_lock%3A 37 | --- error_code: 500 38 | --- error_log 39 | namespace can't be prefixed with reserved word: update_lock: 40 | 41 | === TEST 2: should fail if namespace is prefixed with domain: 42 | --- http_config eval: $::HttpConfig 43 | --- config eval: $::TestConfig 44 | --- request 45 | GET /t/domain%3Aaaa 46 | --- error_code: 500 47 | --- error_log 48 | namespace can't be prefixed with reserved word: domain: 49 | 50 | === TEST 3: should fail if namespace is prefixed with account_key: 51 | --- http_config eval: $::HttpConfig 52 | --- config eval: $::TestConfig 53 | --- request 54 | GET /t/account_key%3Abbb 55 | --- error_code: 500 56 | --- error_log 57 | namespace can't be prefixed with reserved word: account_key: 58 | 59 | === TEST 4: should fail if namespace is prefixed with failure_lock: 60 | --- http_config eval: $::HttpConfig 61 | --- config eval: $::TestConfig 62 | --- request 63 | GET /t/failure_lock%3Accc 64 | --- error_code: 500 65 | --- error_log 66 | namespace can't be prefixed with reserved word: failure_lock: 67 | 68 | === TEST 5: should fail if namespace is prefixed with failed_attempts: 69 | --- http_config eval: $::HttpConfig 70 | --- config eval: $::TestConfig 71 | --- request 72 | GET /t/failed_attempts%3Addd 73 | --- error_code: 500 74 | --- error_log 75 | namespace can't be prefixed with reserved word: failed_attempts: 76 | 77 | === TEST 6: should success if namespace is suffixed with reserved word 78 | --- http_config eval: $::HttpConfig 79 | --- config eval: $::TestConfig 80 | --- request 81 | GET /t/xxxupdate_lock%3A 82 | --- error_code: 200 83 | --- no_error_log 84 | [error] 85 | 86 | === TEST 7: should success if namespace is infixed with reserved word 87 | --- http_config eval: $::HttpConfig 88 | --- config eval: $::TestConfig 89 | --- request 90 | GET /t/xxxupdate_lock%3Ayyy 91 | --- error_code: 200 92 | --- no_error_log 93 | [error] 94 | 95 | === TEST 8: should success with other normal namespace 96 | --- http_config eval: $::HttpConfig 97 | --- config eval: $::TestConfig 98 | --- request 99 | GET /t/normalname 100 | --- error_code: 200 101 | --- no_error_log 102 | [error] 103 | -------------------------------------------------------------------------------- /t/e2e.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et fdm=marker: 2 | 3 | use Test::Nginx::Socket::Lua 'no_plan'; 4 | use Cwd qw(cwd); 5 | 6 | 7 | my $pwd = cwd(); 8 | 9 | env_to_nginx("TEST_TRY_NONCE_INFINITELY=1"); 10 | 11 | $ENV{'tm'} = time; 12 | 13 | sub ::make_http_config{ 14 | my ($key_types, $key_path, $challenges, $shm_name, $storage, $blocking) = @_; 15 | $shm_name ||= "acme"; 16 | $storage ||= "shm"; 17 | $blocking ||= "false"; 18 | return qq{ 19 | lua_package_path "$pwd/lib/?.lua;$pwd/lib/?/init.lua;$pwd/../lib/?.lua;$pwd/../lib/?/init.lua;;"; 20 | lua_package_cpath "$pwd/luajit/lib/?.so;/usr/local/openresty-debug/lualib/?.so;/usr/local/openresty/lualib/?.so;;"; 21 | 22 | lua_shared_dict $shm_name 16m; 23 | 24 | init_by_lua_block { 25 | -- patch localhost to resolve to 127.0.0.1 :facepalm: 26 | -- why resolver in github actions doesn't work? 27 | local old_tcp = ngx.socket.tcp 28 | local old_tcp_connect 29 | 30 | -- need to do the extra check here: https://github.com/openresty/lua-nginx-module/issues/860 31 | local function strip_nils(first, second) 32 | if second then 33 | return first, second 34 | elseif first then 35 | return first 36 | end 37 | end 38 | 39 | local function resolve_connect(f, sock, host, port, opts) 40 | if host == "localhost" then 41 | host = "127.0.0.1" 42 | end 43 | 44 | return f(sock, host, strip_nils(port, opts)) 45 | end 46 | 47 | local function tcp_resolve_connect(sock, host, port, opts) 48 | return resolve_connect(old_tcp_connect, sock, host, port, opts) 49 | end 50 | 51 | _G.ngx.socket.tcp = function(...) 52 | local sock = old_tcp(...) 53 | 54 | if not old_tcp_connect then 55 | old_tcp_connect = sock.connect 56 | end 57 | 58 | sock.connect = tcp_resolve_connect 59 | 60 | return sock 61 | end 62 | 63 | require("resty.acme.autossl").init({ 64 | tos_accepted = true, 65 | domain_key_types = { $key_types }, 66 | account_key_path = "$key_path", 67 | account_email = "travis\@youdomain.com", 68 | domain_whitelist = setmetatable({}, { __index = function() 69 | return true 70 | end}), 71 | enabled_challenge_handlers = { $challenges }, 72 | storage_adapter = "$storage", 73 | -- bump up this slightly in test 74 | challenge_start_delay = 3, 75 | blocking = $blocking, 76 | }, { 77 | api_uri = "https://localhost:14000/dir", 78 | }) 79 | } 80 | init_worker_by_lua_block { 81 | require("resty.acme.autossl").init_worker() 82 | } 83 | 84 | lua_ssl_trusted_certificate ../../fixtures/pebble.minica.pem; 85 | } 86 | }; 87 | 88 | sub ::make_main_config{ 89 | my ($key_types, $key_path, $challenges) = @_; 90 | my $common_config = make_http_config($key_types, $key_path, $challenges, "acme_stream", "file"); 91 | return qq{ 92 | stream { 93 | $common_config 94 | 95 | map \$ssl_preread_alpn_protocols \$backend { 96 | ~\\bacme-tls/1\\b 127.0.0.1:5201; 97 | default 127.0.0.1:5101; 98 | } 99 | 100 | server { 101 | listen 5001; 102 | ssl_preread on; 103 | proxy_pass \$backend; 104 | } 105 | 106 | server { 107 | listen 5201 ssl; 108 | ssl_certificate /tmp/default.pem; 109 | ssl_certificate_key /tmp/default.key; 110 | 111 | ssl_certificate_by_lua_block { 112 | require("resty.acme.autossl").serve_tls_alpn_challenge() 113 | } 114 | 115 | content_by_lua_block { 116 | ngx.exit(0) 117 | } 118 | } 119 | } 120 | } 121 | } 122 | 123 | 124 | run_tests(); 125 | 126 | __DATA__ 127 | === TEST 1: http-01 challenge 128 | --- http_config eval: ::make_http_config("'rsa'", "/tmp/account.key", "'http-01'") 129 | --- config 130 | listen 5002; 131 | listen 5001 ssl; 132 | ssl_certificate /tmp/default.pem; 133 | ssl_certificate_key /tmp/default.key; 134 | 135 | ssl_certificate_by_lua_block { 136 | require("resty.acme.autossl").ssl_certificate() 137 | } 138 | 139 | location /.well-known { 140 | content_by_lua_block { 141 | require("resty.acme.autossl").serve_http_challenge() 142 | } 143 | } 144 | 145 | location ~ /t/(.+) { 146 | set $domain $1; 147 | content_by_lua_block { 148 | local ngx_pipe = require "ngx.pipe" 149 | local opts = { 150 | merge_stderr = true, 151 | buffer_size = 256000, 152 | } 153 | local out 154 | for i=0,15,1 do 155 | local proc = ngx_pipe.spawn({'bash', '-c', "echo q |openssl s_client -host 127.0.0.1 -servername ".. ngx.var.domain .. " -port 5001|openssl x509 -noout -text && sleep 0.1"}, opts) 156 | local data, err, partial = proc:stdout_read_all() 157 | if ngx.re.match(data, ngx.var.domain) then 158 | local f = io.open("/tmp/test1", "w") 159 | f:write(data) 160 | f:close() 161 | ngx.say(data) 162 | break 163 | end 164 | ngx.sleep(2) 165 | end 166 | ngx.say(out or "timeout") 167 | } 168 | } 169 | --- request eval 170 | "GET /t/e2e-test1-$ENV{'tm'}" 171 | --- response_body_like eval 172 | "Pebble Intermediate.+CN\\s*=\\s*e2e-test1.+rsaEncryption" 173 | --- no_error_log 174 | [warn] 175 | [error] 176 | 177 | === TEST 2: http-01 challenge with RSA + ECC dual certs 178 | --- http_config eval: ::make_http_config("'rsa', 'ecc'", "/tmp/account.key", "'http-01'") 179 | --- config 180 | listen 5002; 181 | listen 5001 ssl; 182 | ssl_certificate /tmp/default.pem; 183 | ssl_certificate_key /tmp/default.key; 184 | ssl_certificate /tmp/default-ecc.pem; 185 | ssl_certificate_key /tmp/default-ecc.key; 186 | 187 | ssl_certificate_by_lua_block { 188 | require("resty.acme.autossl").ssl_certificate() 189 | } 190 | 191 | location /.well-known { 192 | content_by_lua_block { 193 | require("resty.acme.autossl").serve_http_challenge() 194 | } 195 | } 196 | 197 | location ~ /t/(.+) { 198 | set $domain $1; 199 | content_by_lua_block { 200 | local ngx_pipe = require "ngx.pipe" 201 | local opts = { 202 | merge_stderr = true, 203 | buffer_size = 256000, 204 | } 205 | local out 206 | for i=0,15,1 do 207 | local proc = ngx_pipe.spawn({'bash', '-c', "echo q |openssl s_client -host 127.0.0.1 -servername ".. ngx.var.domain .. " -max_protocol TLSv1.2 -port 5001 -cipher ECDHE-RSA-AES128-GCM-SHA256|openssl x509 -noout -text && sleep 0.1"}, opts) 208 | local data, err, partial = proc:stdout_read_all() 209 | if ngx.re.match(data, ngx.var.domain) then 210 | local proc2 = ngx_pipe.spawn({'bash', '-c', "echo q |openssl s_client -host 127.0.0.1 -servername ".. ngx.var.domain .. " -port 5001 -max_protocol TLSv1.2 -cipher ECDHE-ECDSA-AES128-GCM-SHA256|openssl x509 -noout -text && sleep 0.1"}, opts) 211 | local data2, err, partial = proc2:stdout_read_all() 212 | ngx.log(ngx.INFO, data, data2) 213 | local f = io.open("/tmp/test2.1", "w") 214 | f:write(data) 215 | f:close() 216 | if ngx.re.match(data2, ngx.var.domain) then 217 | local f = io.open("/tmp/test2.2", "w") 218 | f:write(data2) 219 | f:close() 220 | ngx.say(data) 221 | ngx.say(data2) 222 | break 223 | end 224 | end 225 | ngx.sleep(2) 226 | end 227 | ngx.say(out or "timeout") 228 | } 229 | } 230 | --- request eval 231 | "GET /t/e2e-test2-$ENV{'tm'}" 232 | --- response_body_like eval 233 | "Pebble Intermediate.+CN\\s*=\\s*e2e-test2.+rsaEncryption.+Pebble Intermediate.+CN\\s*=\\s*e2e-test2.+id-ecPublicKey 234 | " 235 | --- no_error_log 236 | [warn] 237 | [error] 238 | --- error_log 239 | set ecc key 240 | 241 | === TEST 3: tls-alpn-01 242 | --- http_config eval: ::make_http_config("'rsa'", "/tmp/account.key", "'tls-alpn-01'", "acme", "file") 243 | --- main_config eval: ::make_main_config("'rsa'", "/tmp/account.key", "'tls-alpn-01'") 244 | --- config 245 | listen 5002; 246 | listen 5101 ssl; 247 | ssl_certificate /tmp/default.pem; 248 | ssl_certificate_key /tmp/default.key; 249 | 250 | ssl_certificate_by_lua_block { 251 | require("resty.acme.autossl").ssl_certificate() 252 | } 253 | 254 | location ~ /t/(.+) { 255 | set $domain $1; 256 | content_by_lua_block { 257 | local ngx_pipe = require "ngx.pipe" 258 | local opts = { 259 | merge_stderr = true, 260 | buffer_size = 256000, 261 | } 262 | local out 263 | for i=0,15,1 do 264 | local proc = ngx_pipe.spawn({'bash', '-c', "echo q |openssl s_client -host 127.0.0.1 -servername ".. ngx.var.domain .. " -port 5101|openssl x509 -noout -text && sleep 0.1"}, opts) 265 | local data, err, partial = proc:stdout_read_all() 266 | if ngx.re.match(data, ngx.var.domain) then 267 | local f = io.open("/tmp/test3", "w") 268 | f:write(data) 269 | f:close() 270 | ngx.say(data) 271 | break 272 | end 273 | ngx.sleep(2) 274 | end 275 | ngx.say(out or "timeout") 276 | } 277 | } 278 | --- request eval 279 | "GET /t/e2e-test3-$ENV{'tm'}" 280 | --- response_body_like eval 281 | "Pebble Intermediate.+CN\\s*=\\s*e2e-test3.+rsaEncryption" 282 | --- no_error_log 283 | [warn]@we allow warn here since we are using plain FFI mode for resty.openssl.ssl 284 | [error] 285 | 286 | === TEST 4: blocking mode 287 | --- http_config eval: ::make_http_config("'rsa'", "/tmp/account.key", "'http-01'", "acme", "file", "true") 288 | --- config 289 | listen 5002; 290 | listen 5001 ssl; 291 | ssl_certificate /tmp/default.pem; 292 | ssl_certificate_key /tmp/default.key; 293 | 294 | ssl_certificate_by_lua_block { 295 | require("resty.acme.autossl").ssl_certificate() 296 | } 297 | 298 | location /.well-known { 299 | content_by_lua_block { 300 | require("resty.acme.autossl").serve_http_challenge() 301 | } 302 | } 303 | 304 | location ~ /t/(.+) { 305 | set $domain $1; 306 | content_by_lua_block { 307 | local ngx_pipe = require "ngx.pipe" 308 | local opts = { 309 | merge_stderr = true, 310 | buffer_size = 256000, 311 | } 312 | local out 313 | local proc = ngx_pipe.spawn({'bash', '-c', "echo q |openssl s_client -host 127.0.0.1 -servername ".. ngx.var.domain .. " -port 5001|openssl x509 -noout -text && sleep 0.1"}, opts) 314 | local data, err, partial = proc:stdout_read_all() 315 | if ngx.re.match(data, ngx.var.domain) then 316 | local f = io.open("/tmp/test1", "w") 317 | f:write(data) 318 | f:close() 319 | ngx.say(data) 320 | end 321 | ngx.say(data or "error") 322 | } 323 | } 324 | --- request eval 325 | "GET /t/e2e-test1-$ENV{'tm'}" 326 | --- response_body_like eval 327 | "Pebble Intermediate.+CN\\s*=\\s*e2e-test1.+rsaEncryption" 328 | --- no_error_log 329 | [warn] 330 | [error] 331 | 332 | -------------------------------------------------------------------------------- /t/fixtures/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | pebble: 4 | image: letsencrypt/pebble:latest 5 | command: pebble -config /test/config/pebble-config.json -strict -dnsserver 10.30.50.3:8053 6 | ports: 7 | - 14000:14000 # HTTPS ACME API 8 | - 15000:15000 # HTTPS Management API 9 | environment: 10 | - PEBBLE_VA_NOSLEEP=1 11 | networks: 12 | acmenet: 13 | ipv4_address: 10.30.50.2 14 | challtestsrv: 15 | image: letsencrypt/pebble-challtestsrv:latest 16 | command: pebble-challtestsrv -defaultIPv6 "" -defaultIPv4 10.30.50.1 17 | ports: 18 | - 8055:8055 # HTTP Management API 19 | networks: 20 | acmenet: 21 | ipv4_address: 10.30.50.3 22 | 23 | consul: 24 | image: hashicorp/consul 25 | ports: 26 | - "127.0.0.1:8500:8500" 27 | command: agent -server -bootstrap-expect=1 -client=0.0.0.0 28 | healthcheck: 29 | test: ["CMD", "consul", "members"] 30 | interval: 10s 31 | timeout: 5s 32 | retries: 3 33 | 34 | vault: 35 | image: hashicorp/vault 36 | user: root 37 | cap_add: 38 | - IPC_LOCK 39 | environment: 40 | - VAULT_DEV_ROOT_TOKEN_ID=root 41 | - VAULT_LOCAL_CONFIG={"listener":{"tcp":{"tls_key_file":"/tmp/key.pem","tls_cert_file":"/tmp/cert.pem","address":"0.0.0.0:8210"}}} 42 | volumes: 43 | - /tmp/key.pem:/tmp/key.pem 44 | - /tmp/cert.pem:/tmp/cert.pem 45 | ports: 46 | - "127.0.0.1:8200:8200" 47 | - "127.0.0.1:8210:8210" 48 | command: server -dev 49 | healthcheck: 50 | test: ["CMD", "vault", "status", "-address", "http://127.0.0.1:8200"] 51 | interval: 10s 52 | timeout: 5s 53 | retries: 3 54 | 55 | etcd: 56 | image: quay.io/coreos/etcd:v3.4.33 57 | volumes: 58 | - /usr/share/ca-certificates/:/etc/ssl/certs 59 | ports: 60 | - "4001:4001" 61 | - "2380:2380" 62 | - "2379:2379" 63 | environment: 64 | - HOST_IP=${HOST_IP} 65 | command: > 66 | etcd 67 | -name etcd0 68 | -advertise-client-urls http://${HOST_IP}:2379,http://${HOST_IP}:4001 69 | -listen-client-urls http://0.0.0.0:2379,http://0.0.0.0:4001 70 | -initial-advertise-peer-urls http://${HOST_IP}:2380 71 | -listen-peer-urls http://0.0.0.0:2380 72 | -initial-cluster-token etcd-cluster-1 73 | -initial-cluster etcd0=http://${HOST_IP}:2380 74 | -initial-cluster-state new 75 | healthcheck: 76 | test: ["CMD", "etcdctl", "endpoint", "health"] 77 | interval: 10s 78 | timeout: 5s 79 | retries: 3 80 | 81 | dummy: 82 | image: ubuntu 83 | command: tail -f /dev/null 84 | depends_on: 85 | consul: 86 | condition: service_healthy 87 | vault: 88 | condition: service_healthy 89 | etcd: 90 | condition: service_healthy 91 | 92 | networks: 93 | acmenet: 94 | driver: bridge 95 | ipam: 96 | driver: default 97 | config: 98 | - subnet: 10.30.50.0/24 99 | -------------------------------------------------------------------------------- /t/fixtures/multiple_certs.pem: -------------------------------------------------------------------------------- 1 | # this is not chain, we put multiple certs together to test 2 | # util.check_chain_root_issuer is indeed using the last cert 3 | # in file 4 | -----BEGIN CERTIFICATE----- 5 | MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG 6 | A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv 7 | b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw 8 | MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i 9 | YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT 10 | aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ 11 | jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp 12 | xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp 13 | 1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG 14 | snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ 15 | U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8 16 | 9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E 17 | BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B 18 | AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz 19 | yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE 20 | 38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP 21 | AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad 22 | DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME 23 | HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== 24 | -----END CERTIFICATE----- 25 | -----BEGIN CERTIFICATE----- 26 | MIIDBzCCAe+gAwIBAgIUJ+FXF8zL+pdK8Nl68Eq0aQlZKNMwDQYJKoZIhvcNAQEL 27 | BQAwEzERMA8GA1UEAwwIdGVzdC5jb20wHhcNMjAxMjE1MTAwNjIyWhcNMzAxMjEz 28 | MTAwNjIyWjATMREwDwYDVQQDDAh0ZXN0LmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD 29 | ggEPADCCAQoCggEBAMEQQC0nyiHOekSs6sTwLBrdiWYvDWC5OQylQZY2pWsBYtWH 30 | 3rkkt98rRNC3cxLSPwH+AAJrJCnRl4ZIxUrtNF8zPW/NexAaarKMLq8LHnVD+cf5 31 | uLzK9xZNt5s8aTQOF8TuHH2Zq/jdfJ9MnAJf1noZ4Oz5IZqOtgJ+1oCDZJc4ZlL1 32 | KO5tfDsWZOsRdow6F7wlK1xtCfcakcncL7Yh4xbZYQXnNSliGZF0/+SIqYIGhv2f 33 | EBng0yOW6FrXtrxhj/7TplAd2v5ziCsdcqqA+YFu4e6PzFybNErUgNZ8ZsokmP56 34 | uU13oKYLIsEf11EmKEX1bwvEvvu+T/V/IB38YV8CAwEAAaNTMFEwHQYDVR0OBBYE 35 | FM8D9Qnrg9JPEN5lkpDpkz44TOh8MB8GA1UdIwQYMBaAFM8D9Qnrg9JPEN5lkpDp 36 | kz44TOh8MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAI/ODar1 37 | fVkJ50rLToICvp2zZkLSsZlL13Gy4+FUUl0sctSRbXF6yPZGa3u6/HeF5AWnrFNX 38 | eZUVuJgyYa2gmz0K+HGbSrbNFb4Cpnhe7Y722SpSDEj3ybOI3EBeRT3WcwpSsGKa 39 | Kfx8NY08J440cn3oNAbZ9XrZOHhyvjkCEr9+ieg1MvMtNg5NbTpHj6Riuvuvvs3s 40 | CaOJ1dN5a59hHHvt76lb6Ah3cwJ98CRAObp1bElgL//Tl9faAHAFIpGopvq41Jnn 41 | rBd/GtvM6J/LHznZ9eOvMq+uBMyAhzpmi6Ih4SGnwN/i8StRbNvpIUIq2rO6IvCZ 42 | 61xzxPhcY6bB2KI= 43 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /t/fixtures/pebble.minica.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDCTCCAfGgAwIBAgIIJOLbes8sTr4wDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE 3 | AxMVbWluaWNhIHJvb3QgY2EgMjRlMmRiMCAXDTE3MTIwNjE5NDIxMFoYDzIxMTcx 4 | MjA2MTk0MjEwWjAgMR4wHAYDVQQDExVtaW5pY2Egcm9vdCBjYSAyNGUyZGIwggEi 5 | MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC5WgZNoVJandj43kkLyU50vzCZ 6 | alozvdRo3OFiKoDtmqKPNWRNO2hC9AUNxTDJco51Yc42u/WV3fPbbhSznTiOOVtn 7 | Ajm6iq4I5nZYltGGZetGDOQWr78y2gWY+SG078MuOO2hyDIiKtVc3xiXYA+8Hluu 8 | 9F8KbqSS1h55yxZ9b87eKR+B0zu2ahzBCIHKmKWgc6N13l7aDxxY3D6uq8gtJRU0 9 | toumyLbdzGcupVvjbjDP11nl07RESDWBLG1/g3ktJvqIa4BWgU2HMh4rND6y8OD3 10 | Hy3H8MY6CElL+MOCbFJjWqhtOxeFyZZV9q3kYnk9CAuQJKMEGuN4GU6tzhW1AgMB 11 | AAGjRTBDMA4GA1UdDwEB/wQEAwIChDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB 12 | BQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsFAAOCAQEAF85v 13 | d40HK1ouDAtWeO1PbnWfGEmC5Xa478s9ddOd9Clvp2McYzNlAFfM7kdcj6xeiNhF 14 | WPIfaGAi/QdURSL/6C1KsVDqlFBlTs9zYfh2g0UXGvJtj1maeih7zxFLvet+fqll 15 | xseM4P9EVJaQxwuK/F78YBt0tCNfivC6JNZMgxKF59h0FBpH70ytUSHXdz7FKwix 16 | Mfn3qEb9BXSk0Q3prNV5sOV3vgjEtB4THfDxSz9z3+DepVnW3vbbqwEbkXdk3j82 17 | 2muVldgOUgTwK8eT+XdofVdntzU/kzygSAtAQwLJfn51fS1GvEcYGBc1bDryIqmF 18 | p9BI7gVKtWSZYegicA== 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /t/fixtures/prepare_env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | export HOST_IP="$(hostname -I | awk '{print $1}')" 5 | 6 | openssl req -x509 -newkey rsa:4096 -keyout /tmp/key.pem -out /tmp/cert.pem -days 1 -nodes -subj '/CN=some.vault' 7 | chmod 777 /tmp/key.pem /tmp/cert.pem 8 | 9 | echo "Prepare containers" 10 | pushd "$SCRIPT_DIR" 11 | docker compose up -d || ( 12 | docker compose logs vault; 13 | docker compose logs etcd; 14 | docker compose logs consul; 15 | exit 1 16 | ) 17 | popd 18 | 19 | 20 | echo "Prepare vault for JWT auth" 21 | curl 'https://localhost:8210/v1/sys/auth/kubernetes.test' -k -X POST -H 'X-Vault-Token: root' -H 'Content-Type: application/json; charset=utf-8' --data-raw '{"path":"kubernetes.test","type":"jwt","config":{}}' 22 | curl 'https://localhost:8210/v1/auth/kubernetes.test/config' -k -X PUT -H 'X-Vault-Token: root' -H 'content-type: application/json; charset=utf-8' --data-raw '{"jwt_validation_pubkeys":["-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtMCbmrsltFKqStOoxl8V\nK5ZlrIMb8d+W62yoXW1DKdg+cPNq0vGD94cxl9NjjRzlSR/NVZq6Q34c1lkbenPw\nf3CYfmbQupOKTJKhBdn9sFCCbW0gi6gQv0BaU3Pa8iGfVcZPctAtdbwmNKVd26hW\nmvnoJYhyewhY+j3ooLdnmh55cZU9w1VO0PaSf2zGSmCUeIao77jWcnkEauK2RrYv\nq5yB6w54Q71+lp2jZil9e4IJP/WqcS1CtmKgiWLoZuWNJXDWaa8LbcgQfsxudn3X\nsgHaYnAdZJOaCsDS/ablKmUOLIiI3TBM6dkUlBUMK9OgAsu+wBdX521rK3u+NNVX\n3wIDAQAB\n-----END PUBLIC KEY-----"],"default_role":"root","namespace_in_state":false,"provider_config":{}}' 23 | curl 'https://localhost:8210/v1/auth/kubernetes.test/role/root' -k -X POST -H 'X-Vault-Token: root' -H 'content-type: application/json; charset=utf-8' --data-raw '{"token_policies":["acme"],"role_type":"jwt","user_claim":"kubernetes.io/serviceaccount/service-account.uid","bound_subject":"system:serviceaccount:kong:gateway-kong"}' 24 | curl 'https://localhost:8210/v1/sys/policies/acl/acme' -k -X PUT -H 'X-Vault-Token: root' -H 'Content-Type: application/json; charset=utf-8' --data-raw '{"name":"acme","policy":"path \"secret/*\" {\n capabilities = [\"create\", \"read\", \"update\", \"delete\"]\n}"}' 25 | 26 | # on macOS use host.docker.internal 27 | if [[ "$OSTYPE" == 'darwin'* ]]; then 28 | host_ip=$(docker run -it --rm alpine ping host.docker.internal -c1|grep -oE "\d+\.\d+\.\d+\.\d+"|head -n1) 29 | # update the default ip in resolver 30 | curl --request POST --data '{"ip":"'$host_ip'"}' http://localhost:8055/set-default-ipv4 31 | fi 32 | 33 | echo "Generate certs" 34 | openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out /tmp/account.key 35 | openssl req -newkey rsa:2048 -nodes -keyout /tmp/default.key -x509 -days 365 -out /tmp/default.pem -subj "/" 36 | 37 | openssl ecparam -name prime256v1 -genkey -out /tmp/default-ecc.key 38 | openssl req -new -sha256 -key /tmp/default-ecc.key -subj "/" -out temp.csr 39 | openssl x509 -req -sha256 -days 365 -in temp.csr -signkey /tmp/default-ecc.key -out /tmp/default-ecc.pem 40 | -------------------------------------------------------------------------------- /t/fixtures/serviceaccount.jwt: -------------------------------------------------------------------------------- 1 | eyJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1MTYyMzkwMjIsImV4cCI6OTk5OTk5OTk5OSwiaXNzIjoia3ViZXJuZXRlcy9zZXJ2aWNlYWNjb3VudCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvbmFtZXNwYWNlIjoia29uZyIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJnYXRld2F5LWtvbmctdG9rZW4tNWw5YmIiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZ2F0ZXdheS1rb25nIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiZWJkOTlkYmUtMWJlZC00NDlmLWIzNzktMjBmNWY1ZDY5MjJhIiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50Omtvbmc6Z2F0ZXdheS1rb25nIn0.qitzPG8qtC-tTj4z6kdUo6g8sWO_94KIhFrafeuaziM3hdi_5bzKiCBKJkxiimN05VQ_4IoSgn0pZrwU8nwPu9Vzv9lRmI1guXiPifJeJzKu1vyZR_9OqnKb-RqRGlVmjibosuTfXe5de6MtCM5cm6NLfLUtVTeLRdFHkT4ZLvU1IlR0CnsD0szgjh8AjvJvXfWddvJ8EFShvlrsvCS_1SolTgF5Fkl4b7iQA5ToaOetC9Rq6XQNp2Qp2-Vw5pCIJ6kpAtU7FNc9Ufq6tuVIqvBxDDpanKIQJ5j_90Ytlh-xOlug8ObaRN2UqRZdogMPHsrb36U3iTnr7gaNuraIzw -------------------------------------------------------------------------------- /t/openssl.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et fdm=marker: 2 | 3 | use Test::Nginx::Socket::Lua 'no_plan'; 4 | use Cwd qw(cwd); 5 | 6 | 7 | my $pwd = cwd(); 8 | 9 | our $HttpConfig = qq{ 10 | lua_package_path "$pwd/lib/?.lua;$pwd/lib/?/init.lua;$pwd/../lib/?.lua;$pwd/../lib/?/init.lua;;"; 11 | }; 12 | 13 | run_tests(); 14 | 15 | __DATA__ 16 | === TEST 1: Load ffi openssl library 17 | --- http_config eval: $::HttpConfig 18 | --- config 19 | location =/t { 20 | content_by_lua_block { 21 | assert(require("resty.acme.openssl")) 22 | } 23 | } 24 | --- request 25 | GET /t 26 | --- error_log 27 | using ffi, OpenSSL version linked: 28 | --- no_error_log 29 | [error] 30 | -------------------------------------------------------------------------------- /t/storage/consul.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et fdm=marker: 2 | 3 | use Test::Nginx::Socket::Lua 'no_plan'; 4 | use Cwd qw(cwd); 5 | 6 | 7 | my $pwd = cwd(); 8 | 9 | our $HttpConfig = qq{ 10 | lua_package_path "$pwd/lib/?.lua;$pwd/lib/?/init.lua;$pwd/../lib/?.lua;$pwd/../lib/?/init.lua;;"; 11 | init_by_lua_block { 12 | _G.test_lib = require("resty.acme.storage.consul") 13 | _G.test_cfg = nil 14 | _G.test_ttl = 0.1 15 | } 16 | }; 17 | 18 | run_tests(); 19 | 20 | __DATA__ 21 | === TEST 1: Consul set key 22 | --- http_config eval: $::HttpConfig 23 | --- config 24 | location =/t { 25 | content_by_lua_block { 26 | local st = test_lib.new(test_cfg) 27 | local err = st:set("key1", "2") 28 | ngx.say(err) 29 | local err = st:set("key1", "new value") 30 | ngx.say(err) 31 | } 32 | } 33 | --- request 34 | GET /t 35 | --- response_body_like eval 36 | "nil 37 | " 38 | --- no_error_log 39 | [error] 40 | 41 | === TEST 2: Consul get key 42 | --- http_config eval: $::HttpConfig 43 | --- config 44 | location =/t { 45 | content_by_lua_block { 46 | local st = test_lib.new(test_cfg) 47 | local err = st:set("key2", "3") 48 | ngx.say(err) 49 | local v, err = st:get("key2") 50 | ngx.say(err) 51 | ngx.say(v) 52 | } 53 | } 54 | --- request 55 | GET /t 56 | --- response_body_like eval 57 | "nil 58 | nil 59 | 3 60 | " 61 | --- no_error_log 62 | [error] 63 | 64 | === TEST 3: Consul delete key 65 | --- http_config eval: $::HttpConfig 66 | --- config 67 | location =/t { 68 | content_by_lua_block { 69 | local st = test_lib.new(test_cfg) 70 | local err = st:set("key3", "3") 71 | ngx.say(err) 72 | local v, err = st:get("key3") 73 | ngx.say(err) 74 | ngx.say(v) 75 | local err = st:delete("key3") 76 | ngx.say(err) 77 | 78 | -- now the key should be deleted 79 | local v, err = st:get("key3") 80 | ngx.say(err) 81 | ngx.say(v) 82 | 83 | -- delete again with no error 84 | local err = st:delete("key3") 85 | ngx.say(err) 86 | } 87 | } 88 | --- request 89 | GET /t 90 | --- response_body_like eval 91 | "nil 92 | nil 93 | 3 94 | nil 95 | nil 96 | nil 97 | nil 98 | " 99 | --- no_error_log 100 | [error] 101 | 102 | === TEST 4: Consul list keys 103 | --- http_config eval: $::HttpConfig 104 | --- config 105 | location =/t { 106 | content_by_lua_block { 107 | local st = test_lib.new(test_cfg) 108 | local err = st:set("prefix1", "bb--") 109 | ngx.say(err) 110 | local err = st:set("pref-x2", "aa--") 111 | ngx.say(err) 112 | local err = st:set("prefix3", "aa--") 113 | ngx.say(err) 114 | 115 | local keys, err = st:list("prefix") 116 | ngx.say(err) 117 | table.sort(keys) 118 | for _, p in ipairs(keys) do ngx.say(p) end 119 | 120 | local keys, err = st:list("nonexistent") 121 | ngx.say(#keys) 122 | } 123 | } 124 | --- request 125 | GET /t 126 | --- response_body_like eval 127 | "nil 128 | nil 129 | nil 130 | nil 131 | prefix1 132 | prefix3 133 | 0 134 | " 135 | --- no_error_log 136 | [error] 137 | 138 | === TEST 5: Consul set ttl 139 | --- http_config eval: $::HttpConfig 140 | --- config 141 | location =/t { 142 | content_by_lua_block { 143 | local st = test_lib.new(test_cfg) 144 | local err = st:set("setttl", "bb--", test_ttl) 145 | ngx.say(err) 146 | local v, err = st:get("setttl") 147 | ngx.say(err) 148 | ngx.say(v) 149 | ngx.sleep(test_ttl) 150 | local v, err = st:get("setttl") 151 | ngx.say(err) 152 | ngx.say(v) 153 | } 154 | } 155 | --- request 156 | GET /t 157 | --- response_body_like eval 158 | "nil 159 | nil 160 | bb-- 161 | nil 162 | nil 163 | " 164 | --- no_error_log 165 | [error] 166 | 167 | === TEST 6: Consul add ttl 168 | --- http_config eval: $::HttpConfig 169 | --- config 170 | location =/t { 171 | content_by_lua_block { 172 | local st = test_lib.new(test_cfg) 173 | local err = st:add("addttl", "bb--", test_ttl) 174 | ngx.say(err) 175 | local v, err = st:get("addttl") 176 | ngx.say(err) 177 | ngx.say(v) 178 | ngx.sleep(test_ttl) 179 | local v, err = st:get("addttl") 180 | ngx.say(err) 181 | ngx.say(v) 182 | } 183 | } 184 | --- request 185 | GET /t 186 | --- response_body_like eval 187 | "nil 188 | nil 189 | bb-- 190 | nil 191 | nil 192 | " 193 | --- no_error_log 194 | [error] 195 | 196 | === TEST 7: Consul add only set when key not exist 197 | --- http_config eval: $::HttpConfig 198 | --- config 199 | location =/t { 200 | content_by_lua_block { 201 | local st = test_lib.new(test_cfg) 202 | local err = st:set("prefix1", "bb--", test_ttl) 203 | ngx.say(err) 204 | local err = st:add("prefix1", "aa--") 205 | ngx.say(err) 206 | local v, err = st:get("prefix1") 207 | ngx.say(err) 208 | ngx.say(v) 209 | ngx.sleep(test_ttl) 210 | local err = st:add("prefix1", "aa--", test_ttl) 211 | ngx.say(err) 212 | local v, err = st:get("prefix1") 213 | ngx.say(err) 214 | ngx.say(v) 215 | ngx.sleep(test_ttl) 216 | local err = st:add("prefix1", "aa--", test_ttl) 217 | ngx.say(err) 218 | } 219 | } 220 | --- request 221 | GET /t 222 | --- response_body_like eval 223 | "nil 224 | exists 225 | nil 226 | bb-- 227 | nil 228 | nil 229 | aa-- 230 | nil 231 | " 232 | --- no_error_log 233 | [error] 234 | 235 | 236 | -------------------------------------------------------------------------------- /t/storage/etcd.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et fdm=marker: 2 | 3 | use Test::Nginx::Socket::Lua 'no_plan'; 4 | use Cwd qw(cwd); 5 | 6 | 7 | my $pwd = cwd(); 8 | 9 | our $HttpConfig = qq{ 10 | lua_package_path "/home/wow/.luarocks/share/lua/5.1/?.lua;/home/wow/.luarocks/share/lua/5.1/?/init.lua;$pwd/lib/?.lua;$pwd/lib/?/init.lua;$pwd/../lib/?.lua;$pwd/../lib/?/init.lua;;"; 11 | init_by_lua_block { 12 | _G.test_lib = require("resty.acme.storage.etcd") 13 | _G.test_cfg = nil 14 | _G.test_ttl = 1 15 | } 16 | }; 17 | 18 | run_tests(); 19 | 20 | __DATA__ 21 | === TEST 1: Etcd set key 22 | --- http_config eval: $::HttpConfig 23 | --- config 24 | location =/t { 25 | content_by_lua_block { 26 | local st = test_lib.new(test_cfg) 27 | local key = "key1_" .. ngx.now() 28 | local err = st:set(key, "2") 29 | ngx.say(err) 30 | local err = st:set(key, "new value") 31 | ngx.say(err) 32 | } 33 | } 34 | --- request 35 | GET /t 36 | --- response_body_like eval 37 | "nil 38 | " 39 | --- no_error_log 40 | [error] 41 | 42 | === TEST 2: Etcd get key 43 | --- http_config eval: $::HttpConfig 44 | --- config 45 | location =/t { 46 | content_by_lua_block { 47 | local st = test_lib.new(test_cfg) 48 | local key = "key2_" .. ngx.now() 49 | local err = st:set(key, "3") 50 | ngx.say(err) 51 | local v, err = st:get(key) 52 | ngx.say(err) 53 | ngx.say(v) 54 | } 55 | } 56 | --- request 57 | GET /t 58 | --- response_body_like eval 59 | "nil 60 | nil 61 | 3 62 | " 63 | --- no_error_log 64 | [error] 65 | 66 | === TEST 3: Etcd delete key 67 | --- http_config eval: $::HttpConfig 68 | --- config 69 | location =/t { 70 | content_by_lua_block { 71 | local st = test_lib.new(test_cfg) 72 | local key = "key3_" .. ngx.now() 73 | local err = st:set(key, "3") 74 | ngx.say(err) 75 | local v, err = st:get(key) 76 | ngx.say(err) 77 | ngx.say(v) 78 | local err = st:delete(key) 79 | ngx.say(err) 80 | 81 | -- now the key should be deleted 82 | local v, err = st:get(key) 83 | ngx.say(err) 84 | ngx.say(v) 85 | 86 | -- delete again with no error 87 | local err = st:delete(key) 88 | ngx.say(err) 89 | } 90 | } 91 | --- request 92 | GET /t 93 | --- response_body_like eval 94 | "nil 95 | nil 96 | 3 97 | nil 98 | nil 99 | nil 100 | nil 101 | " 102 | --- no_error_log 103 | [error] 104 | 105 | === TEST 4: Etcd list keys 106 | --- http_config eval: $::HttpConfig 107 | --- config 108 | location =/t { 109 | content_by_lua_block { 110 | local st = test_lib.new(test_cfg) 111 | local prefix = "prefix4_" .. ngx.now() 112 | local err = st:set(prefix .. "prefix1", "bb--") 113 | ngx.say(err) 114 | local err = st:set("pref-x2", "aa--") 115 | ngx.say(err) 116 | local err = st:set(prefix .. "prefix3", "aa--") 117 | ngx.say(err) 118 | 119 | local keys, err = st:list(prefix) 120 | ngx.say(err) 121 | table.sort(keys) 122 | for _, p in ipairs(keys) do ngx.say(p) end 123 | 124 | local keys, err = st:list("nonexistent") 125 | ngx.say(#keys) 126 | } 127 | } 128 | --- request 129 | GET /t 130 | --- response_body_like eval 131 | "nil 132 | nil 133 | nil 134 | nil 135 | prefix4.+prefix1 136 | prefix4.+prefix3 137 | 0 138 | " 139 | --- no_error_log 140 | [error] 141 | 142 | === TEST 5: Etcd set ttl 143 | --- http_config eval: $::HttpConfig 144 | --- config 145 | location =/t { 146 | content_by_lua_block { 147 | local st = test_lib.new(test_cfg) 148 | local key = "key5_" .. ngx.now() 149 | local err = st:set(key, "bb--", test_ttl) 150 | ngx.say(err) 151 | local v, err = st:get(key) 152 | ngx.say(err) 153 | ngx.say(v) 154 | for i=1, 5 do 155 | ngx.sleep(1) 156 | local v, err = st:get(key) 157 | if err then 158 | ngx.say(err) 159 | ngx.exit(0) 160 | elseif not v then 161 | ngx.say(nil) 162 | ngx.exit(0) 163 | end 164 | end 165 | ngx.say("still exists") 166 | } 167 | } 168 | --- request 169 | GET /t 170 | --- response_body_like eval 171 | "nil 172 | nil 173 | bb-- 174 | nil 175 | " 176 | --- no_error_log 177 | [error] 178 | 179 | === TEST 6: Etcd add ttl 180 | --- http_config eval: $::HttpConfig 181 | --- config 182 | location =/t { 183 | content_by_lua_block { 184 | local st = test_lib.new(test_cfg) 185 | local key = "key6_" .. ngx.now() 186 | local err = st:add(key, "bb--", test_ttl) 187 | ngx.say(err) 188 | local v, err = st:get(key) 189 | ngx.say(err) 190 | ngx.say(v) 191 | for i=1, 5 do 192 | ngx.sleep(1) 193 | local v, err = st:get(key) 194 | if err then 195 | ngx.say(err) 196 | ngx.exit(0) 197 | elseif not v then 198 | ngx.say(nil) 199 | ngx.exit(0) 200 | end 201 | end 202 | ngx.say("still exists") 203 | } 204 | } 205 | --- request 206 | GET /t 207 | --- response_body_like eval 208 | "nil 209 | nil 210 | bb-- 211 | nil 212 | " 213 | --- no_error_log 214 | [error] 215 | 216 | === TEST 7: Etcd add only set when key not exist 217 | --- http_config eval: $::HttpConfig 218 | --- config 219 | location =/t { 220 | content_by_lua_block { 221 | local st = test_lib.new(test_cfg) 222 | local key = "key7_" .. ngx.now() 223 | local err = st:set(key, "bb--", test_ttl) 224 | ngx.say(err) 225 | local err = st:add(key, "aa--") 226 | ngx.say(err) 227 | local v, err = st:get(key) 228 | ngx.say(err) 229 | ngx.say(v) 230 | -- note: etcd evit expired node not immediately 231 | for i=1, 5 do 232 | ngx.sleep(1) 233 | local v, err = st:get(key) 234 | if err then 235 | ngx.say(err) 236 | break 237 | elseif not v then 238 | ngx.say("key evicted") 239 | break 240 | end 241 | end 242 | local err = st:add(key, "aa--", test_ttl) 243 | ngx.say(err) 244 | local v, err = st:get(key) 245 | ngx.say(err) 246 | ngx.say(v) 247 | } 248 | } 249 | --- request 250 | GET /t 251 | --- response_body_like eval 252 | "nil 253 | exists 254 | nil 255 | bb-- 256 | key evicted 257 | nil 258 | nil 259 | aa-- 260 | " 261 | --- no_error_log 262 | [error] 263 | 264 | 265 | -------------------------------------------------------------------------------- /t/storage/file.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et fdm=marker: 2 | 3 | use Test::Nginx::Socket::Lua 'no_plan'; 4 | use Cwd qw(cwd); 5 | 6 | 7 | my $pwd = cwd(); 8 | 9 | our $HttpConfig = qq{ 10 | lua_package_path "$pwd/lib/?.lua;$pwd/lib/?/init.lua;$pwd/../lib/?.lua;$pwd/../lib/?/init.lua;;"; 11 | init_by_lua_block { 12 | _G.test_lib = require("resty.acme.storage.file") 13 | _G.test_cfg = nil 14 | _G.test_ttl = 1 15 | } 16 | }; 17 | 18 | run_tests(); 19 | 20 | __DATA__ 21 | === TEST 1: File set key 22 | --- http_config eval: $::HttpConfig 23 | --- config 24 | location =/t { 25 | content_by_lua_block { 26 | local st = test_lib.new(test_cfg) 27 | local err = st:set("key1", "2") 28 | ngx.say(err) 29 | local err = st:set("key1", "new value") 30 | ngx.say(err) 31 | } 32 | } 33 | --- request 34 | GET /t 35 | --- response_body_like eval 36 | "nil 37 | " 38 | --- no_error_log 39 | [error] 40 | 41 | === TEST 2: File get key 42 | --- http_config eval: $::HttpConfig 43 | --- config 44 | location =/t { 45 | content_by_lua_block { 46 | local st = test_lib.new(test_cfg) 47 | local err = st:set("key2", "3") 48 | ngx.say(err) 49 | local v, err = st:get("key2") 50 | ngx.say(err) 51 | ngx.say(v) 52 | } 53 | } 54 | --- request 55 | GET /t 56 | --- response_body_like eval 57 | "nil 58 | nil 59 | 3 60 | " 61 | --- no_error_log 62 | [error] 63 | 64 | === TEST 3: File delete key 65 | --- http_config eval: $::HttpConfig 66 | --- config 67 | location =/t { 68 | content_by_lua_block { 69 | local st = test_lib.new(test_cfg) 70 | local err = st:set("key3", "3") 71 | ngx.say(err) 72 | local v, err = st:get("key3") 73 | ngx.say(err) 74 | ngx.say(v) 75 | local err = st:delete("key3") 76 | ngx.say(err) 77 | 78 | -- now the key should be deleted 79 | local v, err = st:get("key3") 80 | ngx.say(err) 81 | ngx.say(v) 82 | 83 | -- delete again with no error 84 | local err = st:delete("key3") 85 | ngx.say(err) 86 | } 87 | } 88 | --- request 89 | GET /t 90 | --- response_body_like eval 91 | "nil 92 | nil 93 | 3 94 | nil 95 | nil 96 | nil 97 | nil 98 | " 99 | --- no_error_log 100 | [error] 101 | 102 | === TEST 4: File list keys 103 | --- http_config eval: $::HttpConfig 104 | --- config 105 | location =/t { 106 | content_by_lua_block { 107 | local st = test_lib.new(test_cfg) 108 | local err = st:set("prefix1", "bb--") 109 | ngx.say(err) 110 | local err = st:set("pref-x2", "aa--") 111 | ngx.say(err) 112 | local err = st:set("prefix3", "aa--") 113 | ngx.say(err) 114 | 115 | local keys, err = st:list("prefix") 116 | ngx.say(err) 117 | table.sort(keys) 118 | for _, p in ipairs(keys) do ngx.say(p) end 119 | 120 | local keys, err = st:list("nonexistent") 121 | ngx.say(#keys) 122 | } 123 | } 124 | --- request 125 | GET /t 126 | --- response_body_like eval 127 | "nil 128 | nil 129 | nil 130 | nil 131 | prefix1 132 | prefix3 133 | 0 134 | " 135 | --- no_error_log 136 | [error] 137 | 138 | === TEST 5: File set ttl 139 | --- http_config eval: $::HttpConfig 140 | --- config 141 | location =/t { 142 | content_by_lua_block { 143 | local st = test_lib.new(test_cfg) 144 | local err = st:set("setttl", "bb--", test_ttl) 145 | ngx.say(err) 146 | local v, err = st:get("setttl") 147 | ngx.say(err) 148 | ngx.say(v) 149 | ngx.sleep(test_ttl) 150 | local v, err = st:get("setttl") 151 | ngx.say(err) 152 | ngx.say(v) 153 | } 154 | } 155 | --- request 156 | GET /t 157 | --- response_body_like eval 158 | "nil 159 | nil 160 | bb-- 161 | nil 162 | nil 163 | " 164 | --- no_error_log 165 | [error] 166 | 167 | === TEST 6: File add ttl 168 | --- http_config eval: $::HttpConfig 169 | --- config 170 | location =/t { 171 | content_by_lua_block { 172 | local st = test_lib.new(test_cfg) 173 | local err = st:add("addttl", "bb--", test_ttl) 174 | ngx.say(err) 175 | local v, err = st:get("addttl") 176 | ngx.say(err) 177 | ngx.say(v) 178 | ngx.sleep(test_ttl) 179 | local v, err = st:get("addttl") 180 | ngx.say(err) 181 | ngx.say(v) 182 | } 183 | } 184 | --- request 185 | GET /t 186 | --- response_body_like eval 187 | "nil 188 | nil 189 | bb-- 190 | nil 191 | nil 192 | " 193 | --- no_error_log 194 | [error] 195 | 196 | === TEST 7: File add only set when key not exist 197 | --- http_config eval: $::HttpConfig 198 | --- config 199 | location =/t { 200 | content_by_lua_block { 201 | local st = test_lib.new(test_cfg) 202 | local err = st:set("prefix1", "bb--", test_ttl) 203 | ngx.say(err) 204 | local err = st:add("prefix1", "aa--") 205 | ngx.say(err) 206 | local v, err = st:get("prefix1") 207 | ngx.say(err) 208 | ngx.say(v) 209 | ngx.sleep(test_ttl) 210 | local err = st:add("prefix1", "aa--", test_ttl) 211 | ngx.say(err) 212 | local v, err = st:get("prefix1") 213 | ngx.say(err) 214 | ngx.say(v) 215 | ngx.sleep(test_ttl) 216 | local err = st:add("prefix1", "aa--", test_ttl) 217 | ngx.say(err) 218 | } 219 | } 220 | --- request 221 | GET /t 222 | --- response_body_like eval 223 | "nil 224 | exists 225 | nil 226 | bb-- 227 | nil 228 | nil 229 | aa-- 230 | nil 231 | " 232 | --- no_error_log 233 | [error] 234 | 235 | 236 | -------------------------------------------------------------------------------- /t/storage/redis.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et fdm=marker: 2 | 3 | use Test::Nginx::Socket::Lua 'no_plan'; 4 | use Cwd qw(cwd); 5 | 6 | 7 | my $pwd = cwd(); 8 | 9 | our $HttpConfig = qq{ 10 | lua_package_path "$pwd/lib/?.lua;$pwd/lib/?/init.lua;$pwd/../lib/?.lua;$pwd/../lib/?/init.lua;;"; 11 | init_by_lua_block { 12 | _G.test_lib = require("resty.acme.storage.redis") 13 | _G.test_cfg = nil 14 | _G.test_ttl = 0.1 15 | } 16 | }; 17 | 18 | run_tests(); 19 | 20 | __DATA__ 21 | === TEST 1: Redis set key 22 | --- http_config eval: $::HttpConfig 23 | --- config 24 | location =/t { 25 | content_by_lua_block { 26 | local st = test_lib.new(test_cfg) 27 | local err = st:set("key1", "2") 28 | ngx.say(err) 29 | local err = st:set("key1", "new value") 30 | ngx.say(err) 31 | } 32 | } 33 | --- request 34 | GET /t 35 | --- response_body_like eval 36 | "nil 37 | " 38 | --- no_error_log 39 | [error] 40 | 41 | === TEST 2: Redis get key 42 | --- http_config eval: $::HttpConfig 43 | --- config 44 | location =/t { 45 | content_by_lua_block { 46 | local st = test_lib.new(test_cfg) 47 | local err = st:set("key2", "3") 48 | ngx.say(err) 49 | local v, err = st:get("key2") 50 | ngx.say(err) 51 | ngx.say(v) 52 | } 53 | } 54 | --- request 55 | GET /t 56 | --- response_body_like eval 57 | "nil 58 | nil 59 | 3 60 | " 61 | --- no_error_log 62 | [error] 63 | 64 | === TEST 3: Redis delete key 65 | --- http_config eval: $::HttpConfig 66 | --- config 67 | location =/t { 68 | content_by_lua_block { 69 | local st = test_lib.new(test_cfg) 70 | local err = st:set("key3", "3") 71 | ngx.say(err) 72 | local v, err = st:get("key3") 73 | ngx.say(err) 74 | ngx.say(v) 75 | local err = st:delete("key3") 76 | ngx.say(err) 77 | 78 | -- now the key should be deleted 79 | local v, err = st:get("key3") 80 | ngx.say(err) 81 | ngx.say(v) 82 | 83 | -- delete again with no error 84 | local err = st:delete("key3") 85 | ngx.say(err) 86 | } 87 | } 88 | --- request 89 | GET /t 90 | --- response_body_like eval 91 | "nil 92 | nil 93 | 3 94 | nil 95 | nil 96 | nil 97 | nil 98 | " 99 | --- no_error_log 100 | [error] 101 | 102 | === TEST 4: Redis list keys 103 | --- http_config eval: $::HttpConfig 104 | --- config 105 | location =/t { 106 | content_by_lua_block { 107 | local st = test_lib.new(test_cfg) 108 | local err = st:set("prefix1", "bb--") 109 | ngx.say(err) 110 | local err = st:set("pref-x2", "aa--") 111 | ngx.say(err) 112 | local err = st:set("prefix3", "aa--") 113 | ngx.say(err) 114 | 115 | local keys, err = st:list("prefix") 116 | ngx.say(err) 117 | table.sort(keys) 118 | for _, p in ipairs(keys) do ngx.say(p) end 119 | 120 | local keys, err = st:list("nonexistent") 121 | ngx.say(#keys) 122 | } 123 | } 124 | --- request 125 | GET /t 126 | --- response_body_like eval 127 | "nil 128 | nil 129 | nil 130 | nil 131 | prefix1 132 | prefix3 133 | 0 134 | " 135 | --- no_error_log 136 | [error] 137 | 138 | === TEST 5: Redis set ttl 139 | --- http_config eval: $::HttpConfig 140 | --- config 141 | location =/t { 142 | content_by_lua_block { 143 | local st = test_lib.new(test_cfg) 144 | local err = st:set("setttl", "bb--", test_ttl) 145 | ngx.say(err) 146 | local v, err = st:get("setttl") 147 | ngx.say(err) 148 | ngx.say(v) 149 | ngx.sleep(test_ttl) 150 | local v, err = st:get("setttl") 151 | ngx.say(err) 152 | ngx.say(v) 153 | } 154 | } 155 | --- request 156 | GET /t 157 | --- response_body_like eval 158 | "nil 159 | nil 160 | bb-- 161 | nil 162 | nil 163 | " 164 | --- no_error_log 165 | [error] 166 | 167 | === TEST 6: Redis add ttl 168 | --- http_config eval: $::HttpConfig 169 | --- config 170 | location =/t { 171 | content_by_lua_block { 172 | local st = test_lib.new(test_cfg) 173 | local err = st:add("addttl", "bb--", test_ttl) 174 | ngx.say(err) 175 | local v, err = st:get("addttl") 176 | ngx.say(err) 177 | ngx.say(v) 178 | ngx.sleep(test_ttl) 179 | local v, err = st:get("addttl") 180 | ngx.say(err) 181 | ngx.say(v) 182 | } 183 | } 184 | --- request 185 | GET /t 186 | --- response_body_like eval 187 | "nil 188 | nil 189 | bb-- 190 | nil 191 | nil 192 | " 193 | --- no_error_log 194 | [error] 195 | 196 | === TEST 7: Redis add only set when key not exist 197 | --- http_config eval: $::HttpConfig 198 | --- config 199 | location =/t { 200 | content_by_lua_block { 201 | local st = test_lib.new(test_cfg) 202 | local err = st:set("prefix1", "bb--", test_ttl) 203 | ngx.say(err) 204 | local err = st:add("prefix1", "aa--") 205 | ngx.say(err) 206 | local v, err = st:get("prefix1") 207 | ngx.say(err) 208 | ngx.say(v) 209 | ngx.sleep(test_ttl) 210 | local err = st:add("prefix1", "aa--", test_ttl) 211 | ngx.say(err) 212 | local v, err = st:get("prefix1") 213 | ngx.say(err) 214 | ngx.say(v) 215 | ngx.sleep(test_ttl) 216 | local err = st:add("prefix1", "aa--", test_ttl) 217 | ngx.say(err) 218 | } 219 | } 220 | --- request 221 | GET /t 222 | --- response_body_like eval 223 | "nil 224 | exists 225 | nil 226 | bb-- 227 | nil 228 | nil 229 | aa-- 230 | nil 231 | " 232 | --- no_error_log 233 | [error] 234 | 235 | === TEST 8: Redis namespace set key 236 | --- http_config eval: $::HttpConfig 237 | --- config 238 | location =/t { 239 | content_by_lua_block { 240 | local st = test_lib.new({namespace = "foo"}) 241 | local err = st:set("key1", "2") 242 | ngx.say(err) 243 | local err = st:set("key1", "new value") 244 | ngx.say(err) 245 | } 246 | } 247 | --- request 248 | GET /t 249 | --- response_body_like eval 250 | "nil 251 | " 252 | --- no_error_log 253 | [error] 254 | 255 | === TEST 9: Redis namespace get key 256 | --- http_config eval: $::HttpConfig 257 | --- config 258 | location =/t { 259 | content_by_lua_block { 260 | local st = test_lib.new({namespace = "foo"}) 261 | local err = st:set("key2", "3") 262 | ngx.say(err) 263 | local v, err = st:get("key2") 264 | ngx.say(err) 265 | ngx.say(v) 266 | } 267 | } 268 | --- request 269 | GET /t 270 | --- response_body_like eval 271 | "nil 272 | nil 273 | 3 274 | " 275 | --- no_error_log 276 | [error] 277 | 278 | === TEST 10: Redis namespace delete key 279 | --- http_config eval: $::HttpConfig 280 | --- config 281 | location =/t { 282 | content_by_lua_block { 283 | local st = test_lib.new({namespace = "foo"}) 284 | local err = st:set("key3", "3") 285 | ngx.say(err) 286 | local v, err = st:get("key3") 287 | ngx.say(err) 288 | ngx.say(v) 289 | local err = st:delete("key3") 290 | ngx.say(err) 291 | 292 | -- now the key should be deleted 293 | local v, err = st:get("key3") 294 | ngx.say(err) 295 | ngx.say(v) 296 | 297 | -- delete again with no error 298 | local err = st:delete("key3") 299 | ngx.say(err) 300 | } 301 | } 302 | --- request 303 | GET /t 304 | --- response_body_like eval 305 | "nil 306 | nil 307 | 3 308 | nil 309 | nil 310 | nil 311 | nil 312 | " 313 | --- no_error_log 314 | [error] 315 | 316 | === TEST 11: Redis namespace list keys 317 | --- http_config eval: $::HttpConfig 318 | --- config 319 | location =/t { 320 | content_by_lua_block { 321 | local st = test_lib.new({namespace = "foo"}) 322 | local err = st:set("prefix1", "bb--") 323 | ngx.say(err) 324 | local err = st:set("pref-x2", "aa--") 325 | ngx.say(err) 326 | local err = st:set("prefix3", "aa--") 327 | ngx.say(err) 328 | 329 | local keys, err = st:list("prefix") 330 | ngx.say(err) 331 | table.sort(keys) 332 | for _, p in ipairs(keys) do ngx.say(p) end 333 | 334 | local keys, err = st:list("nonexistent") 335 | ngx.say(#keys) 336 | } 337 | } 338 | --- request 339 | GET /t 340 | --- response_body_like eval 341 | "nil 342 | nil 343 | nil 344 | nil 345 | prefix1 346 | prefix3 347 | 0 348 | " 349 | --- no_error_log 350 | [error] 351 | 352 | === TEST 12: Redis namespace add only set when key not exist 353 | --- http_config eval: $::HttpConfig 354 | --- config 355 | location =/t { 356 | content_by_lua_block { 357 | local st = test_lib.new({namespace = "foo"}) 358 | local err = st:set("prefix1", "bb--", test_ttl) 359 | ngx.say(err) 360 | local err = st:add("prefix1", "aa--") 361 | ngx.say(err) 362 | local v, err = st:get("prefix1") 363 | ngx.say(err) 364 | ngx.say(v) 365 | ngx.sleep(test_ttl) 366 | local err = st:add("prefix1", "aa--", test_ttl) 367 | ngx.say(err) 368 | local v, err = st:get("prefix1") 369 | ngx.say(err) 370 | ngx.say(v) 371 | ngx.sleep(test_ttl) 372 | local err = st:add("prefix1", "aa--", test_ttl) 373 | ngx.say(err) 374 | } 375 | } 376 | --- request 377 | GET /t 378 | --- response_body_like eval 379 | "nil 380 | exists 381 | nil 382 | bb-- 383 | nil 384 | nil 385 | aa-- 386 | nil 387 | " 388 | --- no_error_log 389 | [error] 390 | 391 | === TEST 13: Redis namespace isolation 392 | --- http_config eval: $::HttpConfig 393 | --- config 394 | location =/t { 395 | content_by_lua_block { 396 | local st1 = test_lib.new({namespace = "foo"}) 397 | local st2 = test_lib.new({namespace = "bar"}) 398 | local err = st1:set("isolation", "1") 399 | ngx.say(err) 400 | local v, err = st1:get("isolation") 401 | ngx.say(err) 402 | ngx.say(v) 403 | local v, err = st2:get("isolation") 404 | ngx.say(err) 405 | ngx.say(v) 406 | 407 | local err = st2:set("isolation", "2") 408 | ngx.say(err) 409 | local v, err = st2:get("isolation") 410 | ngx.say(err) 411 | ngx.say(v) 412 | local v, err = st1:get("isolation") 413 | ngx.say(err) 414 | ngx.say(v) 415 | } 416 | } 417 | --- request 418 | GET /t 419 | --- response_body_like eval 420 | "nil 421 | nil 422 | 1 423 | nil 424 | nil 425 | nil 426 | nil 427 | 2 428 | nil 429 | 1 430 | " 431 | --- no_error_log 432 | [error] 433 | 434 | === TEST 14: Redis list keys with multiple scan calls 435 | --- http_config eval: $::HttpConfig 436 | --- config 437 | location =/t { 438 | content_by_lua_block { 439 | local st = test_lib.new(test_cfg) 440 | for i=1,50 do 441 | local err = st:set(string.format("test14:%02d", i), string.format("value%02d", i)) 442 | ngx.say(err) 443 | end 444 | 445 | local keys, err = st:list("test14") 446 | ngx.say(err) 447 | table.sort(keys) 448 | for _, p in ipairs(keys) do ngx.say(p) end 449 | } 450 | } 451 | --- request 452 | GET /t 453 | --- response_body_like eval 454 | "nil 455 | nil 456 | nil 457 | nil 458 | nil 459 | nil 460 | nil 461 | nil 462 | nil 463 | nil 464 | nil 465 | nil 466 | nil 467 | nil 468 | nil 469 | nil 470 | nil 471 | nil 472 | nil 473 | nil 474 | nil 475 | nil 476 | nil 477 | nil 478 | nil 479 | nil 480 | nil 481 | nil 482 | nil 483 | nil 484 | nil 485 | nil 486 | nil 487 | nil 488 | nil 489 | nil 490 | nil 491 | nil 492 | nil 493 | nil 494 | nil 495 | nil 496 | nil 497 | nil 498 | nil 499 | nil 500 | nil 501 | nil 502 | nil 503 | nil 504 | nil 505 | test14:01 506 | test14:02 507 | test14:03 508 | test14:04 509 | test14:05 510 | test14:06 511 | test14:07 512 | test14:08 513 | test14:09 514 | test14:10 515 | test14:11 516 | test14:12 517 | test14:13 518 | test14:14 519 | test14:15 520 | test14:16 521 | test14:17 522 | test14:18 523 | test14:19 524 | test14:20 525 | test14:21 526 | test14:22 527 | test14:23 528 | test14:24 529 | test14:25 530 | test14:26 531 | test14:27 532 | test14:28 533 | test14:29 534 | test14:30 535 | test14:31 536 | test14:32 537 | test14:33 538 | test14:34 539 | test14:35 540 | test14:36 541 | test14:37 542 | test14:38 543 | test14:39 544 | test14:40 545 | test14:41 546 | test14:42 547 | test14:43 548 | test14:44 549 | test14:45 550 | test14:46 551 | test14:47 552 | test14:48 553 | test14:49 554 | test14:50 555 | " 556 | --- no_error_log 557 | [error] 558 | 559 | === TEST 15: Redis auth works with username and password 560 | --- http_config eval: $::HttpConfig 561 | --- config 562 | location =/t { 563 | content_by_lua_block { 564 | local st = test_lib.new({ username = "default", password = "passdefault", port = 6380 }) 565 | local err = st:set("key2", "3") 566 | ngx.say(err) 567 | local v, err = st:get("key2") 568 | ngx.say(err) 569 | ngx.say(v) 570 | } 571 | } 572 | --- request 573 | GET /t 574 | --- response_body_like eval 575 | "nil 576 | nil 577 | 3 578 | " 579 | --- no_error_log 580 | [error] 581 | 582 | === TEST 16: Redis auth works with single auth (backwards compatibility) 583 | --- http_config eval: $::HttpConfig 584 | --- config 585 | location =/t { 586 | content_by_lua_block { 587 | local st = test_lib.new({auth = "passdefault", port = 6380 }) 588 | local err = st:set("key2", "3") 589 | ngx.say(err) 590 | local v, err = st:get("key2") 591 | ngx.say(err) 592 | ngx.say(v) 593 | } 594 | } 595 | --- request 596 | GET /t 597 | --- response_body_like eval 598 | "nil 599 | nil 600 | 3 601 | " 602 | --- no_error_log 603 | [error] 604 | 605 | === TEST 17: Redis auth works with just password 606 | --- http_config eval: $::HttpConfig 607 | --- config 608 | location =/t { 609 | content_by_lua_block { 610 | local st = test_lib.new({ password = "passdefault", port = 6380 }) 611 | local err = st:set("key2", "3") 612 | ngx.say(err) 613 | local v, err = st:get("key2") 614 | ngx.say(err) 615 | ngx.say(v) 616 | } 617 | } 618 | --- request 619 | GET /t 620 | --- response_body_like eval 621 | "nil 622 | nil 623 | 3 624 | " 625 | --- no_error_log 626 | [error] 627 | 628 | === TEST 18: Redis auth fails with just username with error "NOAUTH Authentication required" 629 | --- http_config eval: $::HttpConfig 630 | --- config 631 | location =/t { 632 | content_by_lua_block { 633 | local st = test_lib.new({ username = "default", port = 6380 }) 634 | local err = st:set("key2", "3") 635 | ngx.say(err) 636 | local v, err = st:get("key2") 637 | ngx.say(err) 638 | ngx.say(v) 639 | } 640 | } 641 | --- request 642 | GET /t 643 | --- response_body_like eval 644 | "NOAUTH Authentication required" 645 | --- no_error_log 646 | [error] 647 | 648 | === TEST 19: Redis auth fails with wrong username 649 | --- http_config eval: $::HttpConfig 650 | --- config 651 | location =/t { 652 | content_by_lua_block { 653 | local st = test_lib.new({ username = "kong", port = 6380 }) 654 | local err = st:set("key2", "3") 655 | ngx.say(err) 656 | local v, err = st:get("key2") 657 | ngx.say(err) 658 | ngx.say(v) 659 | } 660 | } 661 | --- request 662 | GET /t 663 | --- response_body_like eval 664 | "NOAUTH Authentication required" 665 | --- no_error_log 666 | [error] 667 | 668 | === TEST 20: Redis auth fails with wrong password and no username with error "authentication failed WRONGPASS" 669 | --- http_config eval: $::HttpConfig 670 | --- config 671 | location =/t { 672 | content_by_lua_block { 673 | local st = test_lib.new({ password = "wrongpass", port = 6380 }) 674 | local err = st:set("key2", "3") 675 | ngx.say(err) 676 | local v, err = st:get("key2") 677 | ngx.say(err) 678 | ngx.say(v) 679 | } 680 | } 681 | --- request 682 | GET /t 683 | --- response_body_like eval 684 | "authentication failed WRONGPASS" 685 | --- no_error_log 686 | [error] 687 | 688 | === TEST 21: Redis auth fails with wrong password and correct username with error "authentication failed WRONGPASS" 689 | --- http_config eval: $::HttpConfig 690 | --- config 691 | location =/t { 692 | content_by_lua_block { 693 | local st = test_lib.new({ username = "default", password = "wrongpass", port = 6380 }) 694 | local err = st:set("key2", "3") 695 | ngx.say(err) 696 | local v, err = st:get("key2") 697 | ngx.say(err) 698 | ngx.say(v) 699 | } 700 | } 701 | --- request 702 | GET /t 703 | --- response_body_like eval 704 | "authentication failed WRONGPASS" 705 | --- no_error_log 706 | [error] 707 | 708 | === TEST 22: Redis auth fails with correct password and wrong username with error "authentication failed WRONGPASS" 709 | --- http_config eval: $::HttpConfig 710 | --- config 711 | location =/t { 712 | content_by_lua_block { 713 | local st = test_lib.new({ username = "kong", password = "passdefault", port = 6380 }) 714 | local err = st:set("key2", "3") 715 | ngx.say(err) 716 | local v, err = st:get("key2") 717 | ngx.say(err) 718 | ngx.say(v) 719 | } 720 | } 721 | --- request 722 | GET /t 723 | --- response_body_like eval 724 | "authentication failed WRONGPASS" 725 | --- no_error_log 726 | [error] 727 | -------------------------------------------------------------------------------- /t/storage/shm.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et fdm=marker: 2 | 3 | use Test::Nginx::Socket::Lua 'no_plan'; 4 | use Cwd qw(cwd); 5 | 6 | 7 | my $pwd = cwd(); 8 | 9 | our $HttpConfig = qq{ 10 | lua_package_path "$pwd/lib/?.lua;$pwd/lib/?/init.lua;$pwd/../lib/?.lua;$pwd/../lib/?/init.lua;;"; 11 | lua_shared_dict acme_shared 1m; 12 | init_by_lua_block { 13 | _G.test_lib = require("resty.acme.storage.shm") 14 | _G.test_cfg = { shm_name = "acme_shared" } 15 | _G.test_ttl = 0.1 16 | } 17 | }; 18 | 19 | run_tests(); 20 | 21 | __DATA__ 22 | === TEST 1: Redis set key 23 | --- http_config eval: $::HttpConfig 24 | --- config 25 | location =/t { 26 | content_by_lua_block { 27 | local st = test_lib.new(test_cfg) 28 | local err = st:set("key1", "2") 29 | ngx.say(err) 30 | local err = st:set("key1", "new value") 31 | ngx.say(err) 32 | } 33 | } 34 | --- request 35 | GET /t 36 | --- response_body_like eval 37 | "nil 38 | " 39 | --- no_error_log 40 | [error] 41 | 42 | === TEST 2: Redis get key 43 | --- http_config eval: $::HttpConfig 44 | --- config 45 | location =/t { 46 | content_by_lua_block { 47 | local st = test_lib.new(test_cfg) 48 | local err = st:set("key2", "3") 49 | ngx.say(err) 50 | local v, err = st:get("key2") 51 | ngx.say(err) 52 | ngx.say(v) 53 | } 54 | } 55 | --- request 56 | GET /t 57 | --- response_body_like eval 58 | "nil 59 | nil 60 | 3 61 | " 62 | --- no_error_log 63 | [error] 64 | 65 | === TEST 3: Redis delete key 66 | --- http_config eval: $::HttpConfig 67 | --- config 68 | location =/t { 69 | content_by_lua_block { 70 | local st = test_lib.new(test_cfg) 71 | local err = st:set("key3", "3") 72 | ngx.say(err) 73 | local v, err = st:get("key3") 74 | ngx.say(err) 75 | ngx.say(v) 76 | local err = st:delete("key3") 77 | ngx.say(err) 78 | 79 | -- now the key should be deleted 80 | local v, err = st:get("key3") 81 | ngx.say(err) 82 | ngx.say(v) 83 | 84 | -- delete again with no error 85 | local err = st:delete("key3") 86 | ngx.say(err) 87 | } 88 | } 89 | --- request 90 | GET /t 91 | --- response_body_like eval 92 | "nil 93 | nil 94 | 3 95 | nil 96 | nil 97 | nil 98 | nil 99 | " 100 | --- no_error_log 101 | [error] 102 | 103 | === TEST 4: Redis list keys 104 | --- http_config eval: $::HttpConfig 105 | --- config 106 | location =/t { 107 | content_by_lua_block { 108 | local st = test_lib.new(test_cfg) 109 | local err = st:set("prefix1", "bb--") 110 | ngx.say(err) 111 | local err = st:set("pref-x2", "aa--") 112 | ngx.say(err) 113 | local err = st:set("prefix3", "aa--") 114 | ngx.say(err) 115 | 116 | local keys, err = st:list("prefix") 117 | ngx.say(err) 118 | table.sort(keys) 119 | for _, p in ipairs(keys) do ngx.say(p) end 120 | 121 | local keys, err = st:list("nonexistent") 122 | ngx.say(#keys) 123 | } 124 | } 125 | --- request 126 | GET /t 127 | --- response_body_like eval 128 | "nil 129 | nil 130 | nil 131 | nil 132 | prefix1 133 | prefix3 134 | 0 135 | " 136 | --- no_error_log 137 | [error] 138 | 139 | === TEST 5: Redis set ttl 140 | --- http_config eval: $::HttpConfig 141 | --- config 142 | location =/t { 143 | content_by_lua_block { 144 | local st = test_lib.new(test_cfg) 145 | local err = st:set("setttl", "bb--", test_ttl) 146 | ngx.say(err) 147 | local v, err = st:get("setttl") 148 | ngx.say(err) 149 | ngx.say(v) 150 | ngx.sleep(test_ttl) 151 | local v, err = st:get("setttl") 152 | ngx.say(err) 153 | ngx.say(v) 154 | } 155 | } 156 | --- request 157 | GET /t 158 | --- response_body_like eval 159 | "nil 160 | nil 161 | bb-- 162 | nil 163 | nil 164 | " 165 | --- no_error_log 166 | [error] 167 | 168 | === TEST 6: Redis add ttl 169 | --- http_config eval: $::HttpConfig 170 | --- config 171 | location =/t { 172 | content_by_lua_block { 173 | local st = test_lib.new(test_cfg) 174 | local err = st:add("addttl", "bb--", test_ttl) 175 | ngx.say(err) 176 | local v, err = st:get("addttl") 177 | ngx.say(err) 178 | ngx.say(v) 179 | ngx.sleep(test_ttl) 180 | local v, err = st:get("addttl") 181 | ngx.say(err) 182 | ngx.say(v) 183 | } 184 | } 185 | --- request 186 | GET /t 187 | --- response_body_like eval 188 | "nil 189 | nil 190 | bb-- 191 | nil 192 | nil 193 | " 194 | --- no_error_log 195 | [error] 196 | 197 | === TEST 7: Redis add only set when key not exist 198 | --- http_config eval: $::HttpConfig 199 | --- config 200 | location =/t { 201 | content_by_lua_block { 202 | local st = test_lib.new(test_cfg) 203 | local err = st:set("prefix1", "bb--", test_ttl) 204 | ngx.say(err) 205 | local err = st:add("prefix1", "aa--") 206 | ngx.say(err) 207 | local v, err = st:get("prefix1") 208 | ngx.say(err) 209 | ngx.say(v) 210 | ngx.sleep(test_ttl) 211 | local err = st:add("prefix1", "aa--", test_ttl) 212 | ngx.say(err) 213 | local v, err = st:get("prefix1") 214 | ngx.say(err) 215 | ngx.say(v) 216 | ngx.sleep(test_ttl) 217 | local err = st:add("prefix1", "aa--", test_ttl) 218 | ngx.say(err) 219 | } 220 | } 221 | --- request 222 | GET /t 223 | --- response_body_like eval 224 | "nil 225 | exists 226 | nil 227 | bb-- 228 | nil 229 | nil 230 | aa-- 231 | nil 232 | " 233 | --- no_error_log 234 | [error] 235 | 236 | 237 | -------------------------------------------------------------------------------- /t/storage/vault.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et fdm=marker: 2 | 3 | use Test::Nginx::Socket::Lua 'no_plan'; 4 | use Cwd qw(cwd); 5 | 6 | 7 | my $pwd = cwd(); 8 | 9 | our $HttpConfig = qq{ 10 | lua_package_path "$pwd/lib/?.lua;$pwd/lib/?/init.lua;$pwd/../lib/?.lua;$pwd/../lib/?/init.lua;;"; 11 | init_by_lua_block { 12 | _G.test_lib = require("resty.acme.storage.vault") 13 | _G.test_cfg = { 14 | token = "root", 15 | kv_path = "secret/acme" 16 | } 17 | _G.test_ttl = 1 18 | } 19 | }; 20 | 21 | 22 | run_tests(); 23 | 24 | __DATA__ 25 | === TEST 1: Vault set key 26 | --- http_config eval: $::HttpConfig 27 | --- config 28 | location =/t { 29 | content_by_lua_block { 30 | local st = test_lib.new(test_cfg) 31 | local err = st:set("key1", "2") 32 | ngx.say(err) 33 | local err = st:set("key1", "new value") 34 | ngx.say(err) 35 | } 36 | } 37 | --- request 38 | GET /t 39 | --- response_body_like eval 40 | "nil 41 | " 42 | --- no_error_log 43 | [error] 44 | 45 | === TEST 2: Vault get key 46 | --- http_config eval: $::HttpConfig 47 | --- config 48 | location =/t { 49 | content_by_lua_block { 50 | local st = test_lib.new(test_cfg) 51 | local err = st:set("key2", "3") 52 | ngx.say(err) 53 | local v, err = st:get("key2") 54 | ngx.say(err) 55 | ngx.say(v) 56 | } 57 | } 58 | --- request 59 | GET /t 60 | --- response_body_like eval 61 | "nil 62 | nil 63 | 3 64 | " 65 | --- no_error_log 66 | [error] 67 | 68 | === TEST 3: Vault delete key 69 | --- http_config eval: $::HttpConfig 70 | --- config 71 | location =/t { 72 | content_by_lua_block { 73 | local st = test_lib.new(test_cfg) 74 | local err = st:set("key3", "3") 75 | ngx.say(err) 76 | local v, err = st:get("key3") 77 | ngx.say(err) 78 | ngx.say(v) 79 | local err = st:delete("key3") 80 | ngx.say(err) 81 | 82 | -- now the key should be deleted 83 | local v, err = st:get("key3") 84 | ngx.say(err) 85 | ngx.say(v) 86 | 87 | -- delete again with no error 88 | local err = st:delete("key3") 89 | ngx.say(err) 90 | } 91 | } 92 | --- request 93 | GET /t 94 | --- response_body_like eval 95 | "nil 96 | nil 97 | 3 98 | nil 99 | nil 100 | nil 101 | nil 102 | " 103 | --- no_error_log 104 | [error] 105 | 106 | === TEST 4: Vault list keys 107 | --- http_config eval: $::HttpConfig 108 | --- config 109 | location =/t { 110 | content_by_lua_block { 111 | local st = test_lib.new(test_cfg) 112 | local err = st:set("prefix1", "bb--") 113 | ngx.say(err) 114 | local err = st:set("pref-x2", "aa--") 115 | ngx.say(err) 116 | local err = st:set("prefix3", "aa--") 117 | ngx.say(err) 118 | 119 | local keys, err = st:list("prefix") 120 | ngx.say(err) 121 | table.sort(keys) 122 | for _, p in ipairs(keys) do ngx.say(p) end 123 | 124 | local keys, err = st:list("nonexistent") 125 | ngx.say(#keys) 126 | } 127 | } 128 | --- request 129 | GET /t 130 | --- response_body_like eval 131 | "nil 132 | nil 133 | nil 134 | nil 135 | prefix1 136 | prefix3 137 | 0 138 | " 139 | --- no_error_log 140 | [error] 141 | 142 | === TEST 5: Vault set ttl 143 | --- http_config eval: $::HttpConfig 144 | --- config 145 | location =/t { 146 | content_by_lua_block { 147 | local st = test_lib.new(test_cfg) 148 | local err = st:set("setttl", "bb--", test_ttl) 149 | ngx.say(err) 150 | local v, err = st:get("setttl") 151 | ngx.say(err) 152 | ngx.say(v) 153 | ngx.sleep(test_ttl) 154 | local v, err = st:get("setttl") 155 | ngx.say(err) 156 | ngx.say(v) 157 | } 158 | } 159 | --- request 160 | GET /t 161 | --- response_body_like eval 162 | "nil 163 | nil 164 | bb-- 165 | nil 166 | nil 167 | " 168 | --- no_error_log 169 | [error] 170 | 171 | === TEST 6: Vault add ttl 172 | --- http_config eval: $::HttpConfig 173 | --- config 174 | location =/t { 175 | content_by_lua_block { 176 | local st = test_lib.new(test_cfg) 177 | local err = st:add("addttl", "bb--", test_ttl) 178 | ngx.say(err) 179 | local v, err = st:get("addttl") 180 | ngx.say(err) 181 | ngx.say(v) 182 | ngx.sleep(test_ttl) 183 | local v, err = st:get("addttl") 184 | ngx.say(err) 185 | ngx.say(v) 186 | } 187 | } 188 | --- request 189 | GET /t 190 | --- response_body_like eval 191 | "nil 192 | nil 193 | bb-- 194 | nil 195 | nil 196 | " 197 | --- no_error_log 198 | [error] 199 | 200 | === TEST 7: Vault add only set when key not exist 201 | --- http_config eval: $::HttpConfig 202 | --- config 203 | location =/t { 204 | content_by_lua_block { 205 | local st = test_lib.new(test_cfg) 206 | local err = st:set("prefix1", "bb--", test_ttl) 207 | ngx.say(err) 208 | local err = st:add("prefix1", "aa--") 209 | ngx.say(err) 210 | local v, err = st:get("prefix1") 211 | ngx.say(err) 212 | ngx.say(v) 213 | ngx.sleep(test_ttl) 214 | local err = st:add("prefix1", "aa--", test_ttl) 215 | ngx.say(err) 216 | local v, err = st:get("prefix1") 217 | ngx.say(err) 218 | ngx.say(v) 219 | ngx.sleep(test_ttl) 220 | local err = st:add("prefix1", "aa--", test_ttl) 221 | ngx.say(err) 222 | } 223 | } 224 | --- request 225 | GET /t 226 | --- response_body_like eval 227 | "nil 228 | exists 229 | nil 230 | bb-- 231 | nil 232 | nil 233 | aa-- 234 | nil 235 | " 236 | --- no_error_log 237 | [error] 238 | 239 | 240 | -------------------------------------------------------------------------------- /t/storage/vault_kubernetes.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et fdm=marker: 2 | 3 | use Test::Nginx::Socket::Lua 'no_plan'; 4 | use Cwd qw(cwd); 5 | 6 | 7 | my $pwd = cwd(); 8 | 9 | our $HttpConfig = qq{ 10 | lua_package_path "$pwd/lib/?.lua;$pwd/lib/?/init.lua;$pwd/../lib/?.lua;$pwd/../lib/?/init.lua;;"; 11 | init_by_lua_block { 12 | _G.test_lib = require("resty.acme.storage.vault") 13 | } 14 | }; 15 | 16 | 17 | run_tests(); 18 | 19 | __DATA__ 20 | === TEST 1: Vault authentication failed if no token or jwt provided 21 | --- http_config eval: $::HttpConfig 22 | --- config 23 | location =/t { 24 | content_by_lua_block { 25 | local st = test_lib.new({ 26 | https = true, 27 | tls_verify = false, 28 | port = 8210, 29 | kv_path = "secret/acme", 30 | }) 31 | local err = st:set("keyssl1", "1") 32 | ngx.say(err) 33 | } 34 | } 35 | --- request 36 | GET /t 37 | --- response_body_like eval 38 | "errors from vault: \\[\"(?:missing client token|permission denied)\"\\] 39 | " 40 | --- no_error_log 41 | [error] 42 | 43 | 44 | === TEST 2: Vault authenticate using kubernetes 45 | --- http_config eval: $::HttpConfig 46 | --- config 47 | location =/t { 48 | content_by_lua_block { 49 | local st = test_lib.new({ 50 | https = true, 51 | tls_verify = false, 52 | auth_method = "kubernetes", 53 | auth_path = "kubernetes.test", 54 | jwt_path = "t/fixtures/serviceaccount.jwt", 55 | auth_role = "root", 56 | port = 8210, 57 | kv_path = "secret/acme", 58 | }) 59 | local err = st:set("keyssl2", "2") 60 | ngx.say(err) 61 | local v, err = st:get("keyssl2") 62 | ngx.say(err) 63 | ngx.say(v) 64 | } 65 | } 66 | --- request 67 | GET /t 68 | --- response_body_like eval 69 | "nil 70 | nil 71 | 2 72 | " 73 | --- no_error_log 74 | [error] 75 | 76 | 77 | === TEST 3: Vault authenticate using kubernetes (case-insensitivity test) 78 | --- http_config eval: $::HttpConfig 79 | --- config 80 | location =/t { 81 | content_by_lua_block { 82 | local st = test_lib.new({ 83 | https = true, 84 | tls_verify = false, 85 | auth_method = "KuBeRnEtEs", 86 | auth_path = "kubernetes.test", 87 | jwt_path = "t/fixtures/serviceaccount.jwt", 88 | auth_role = "root", 89 | port = 8210, 90 | kv_path = "secret/acme", 91 | }) 92 | local err = st:set("keyssl3", "3") 93 | ngx.say(err) 94 | local v, err = st:get("keyssl3") 95 | ngx.say(err) 96 | ngx.say(v) 97 | } 98 | } 99 | --- request 100 | GET /t 101 | --- response_body_like eval 102 | "nil 103 | nil 104 | 3 105 | " 106 | --- no_error_log 107 | [error] 108 | 109 | -------------------------------------------------------------------------------- /t/storage/vault_tls.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et fdm=marker: 2 | 3 | use Test::Nginx::Socket::Lua 'no_plan'; 4 | use Cwd qw(cwd); 5 | 6 | 7 | my $pwd = cwd(); 8 | 9 | our $HttpConfig = qq{ 10 | lua_package_path "$pwd/lib/?.lua;$pwd/lib/?/init.lua;$pwd/../lib/?.lua;$pwd/../lib/?/init.lua;;"; 11 | init_by_lua_block { 12 | _G.test_lib = require("resty.acme.storage.vault") 13 | } 14 | }; 15 | 16 | 17 | run_tests(); 18 | 19 | __DATA__ 20 | === TEST 1: Vault tls_verify default on 21 | --- http_config eval: $::HttpConfig 22 | --- config 23 | location =/t { 24 | content_by_lua_block { 25 | local st = test_lib.new({ 26 | token = "root", 27 | port = 8210, 28 | https = true, 29 | tls_verify = true, 30 | kv_path = "secret/acme", 31 | }) 32 | local err = st:set("keyssl1", "2") 33 | ngx.say(err) 34 | local st = test_lib.new({ 35 | token = "root", 36 | port = 8210, 37 | https = true, 38 | kv_path = "secret/acme", 39 | }) 40 | local v, err = st:get("keyssl1") 41 | ngx.say(err) 42 | ngx.say(v) 43 | } 44 | } 45 | --- request 46 | GET /t 47 | --- response_body_like eval 48 | "unable to SSL handshake with vault.+ 49 | unable to SSL handshake with vault.+ 50 | nil 51 | " 52 | --- error_log eval 53 | qr/self.signed certificate/ 54 | 55 | === TEST 2: Vault tls_verify off connection ok 56 | --- http_config eval: $::HttpConfig 57 | --- config 58 | location =/t { 59 | content_by_lua_block { 60 | local st = test_lib.new({ 61 | token = "root", 62 | port = 8210, 63 | https = true, 64 | tls_verify = false, 65 | kv_path = "secret/acme", 66 | }) 67 | local err = st:set("keyssl1", "2") 68 | ngx.say(err) 69 | local v, err = st:get("keyssl1") 70 | ngx.say(err) 71 | ngx.say(v) 72 | } 73 | } 74 | --- request 75 | GET /t 76 | --- response_body_like eval 77 | "nil 78 | nil 79 | 2 80 | " 81 | --- no_error_log 82 | [error] 83 | 84 | === TEST 3: Vault tls_verify with trusted certificate 85 | --- http_config eval: $::HttpConfig 86 | --- config 87 | location =/t { 88 | lua_ssl_trusted_certificate /tmp/cert.pem; 89 | content_by_lua_block { 90 | local st = test_lib.new({ 91 | token = "root", 92 | port = 8210, 93 | https = true, 94 | kv_path = "secret/acme", 95 | }) 96 | local err = st:set("keyssl1", "2") 97 | ngx.say(err) 98 | local v, err = st:get("keyssl1") 99 | ngx.say(err) 100 | ngx.say(v) 101 | } 102 | } 103 | --- request 104 | GET /t 105 | --- response_body_like eval 106 | "unable to SSL handshake with vault.+ 107 | unable to SSL handshake with vault.+ 108 | nil 109 | " 110 | --- error_log 111 | certificate does not match 112 | 113 | === TEST 4: Vault tls_verify with trusted certificate and server_name 114 | --- http_config eval: $::HttpConfig 115 | --- config 116 | location =/t { 117 | lua_ssl_trusted_certificate /tmp/cert.pem; 118 | content_by_lua_block { 119 | local st = test_lib.new({ 120 | token = "root", 121 | port = 8210, 122 | https = true, 123 | tls_server_name = "some.vault", 124 | kv_path = "secret/acme", 125 | }) 126 | local err = st:set("keyssl1", "2") 127 | ngx.say(err) 128 | local v, err = st:get("keyssl1") 129 | ngx.say(err) 130 | ngx.say(v) 131 | } 132 | } 133 | --- request 134 | GET /t 135 | --- response_body_like eval 136 | "nil 137 | nil 138 | 2 139 | " 140 | --- no_error_log 141 | [error] 142 | 143 | -------------------------------------------------------------------------------- /t/util.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et fdm=marker: 2 | 3 | use Test::Nginx::Socket::Lua 'no_plan'; 4 | use Cwd qw(cwd); 5 | 6 | 7 | my $pwd = cwd(); 8 | 9 | our $HttpConfig = qq{ 10 | lua_package_path "$pwd/lib/?.lua;$pwd/lib/?/init.lua;$pwd/../lib/?.lua;$pwd/../lib/?/init.lua;;"; 11 | }; 12 | 13 | 14 | run_tests(); 15 | 16 | __DATA__ 17 | === TEST 1: Generates CSR with RSA pkey correctly 18 | --- http_config eval: $::HttpConfig 19 | --- config 20 | location =/t { 21 | content_by_lua_block { 22 | local util = require("resty.acme.util") 23 | local openssl = require("resty.acme.openssl") 24 | local pkey = openssl.pkey.new() 25 | local der, err = util.create_csr(pkey, "dns1.com", "dns2.com", "dns3.com") 26 | if err then 27 | ngx.log(ngx.ERR, err) 28 | return 29 | end 30 | ngx.update_time() 31 | local fname = "ci_" .. math.floor(ngx.now() * 1000) 32 | local f = io.open(fname, "wb") 33 | f:write(der) 34 | f:close() 35 | ngx.say(io.popen("openssl req -inform der -in " .. fname .. " -noout -text", 'r'):read("*a")) 36 | os.remove(fname) 37 | } 38 | } 39 | --- request 40 | GET /t 41 | --- response_body_like eval 42 | ".+CN\\s*=\\s*dns1.com.+rsaEncryption.+2048 bit.+DNS:dns1.com.+DNS:dns2.com.+DNS:dns3.com" 43 | --- no_error_log 44 | [error] 45 | 46 | === TEST 2: Generates CSR with RSA pkey specific bits correctly 47 | --- http_config eval: $::HttpConfig 48 | --- config 49 | location =/t { 50 | content_by_lua_block { 51 | local util = require("resty.acme.util") 52 | local openssl = require("resty.acme.openssl") 53 | local pkey = openssl.pkey.new({ 54 | bits = 4096, 55 | }) 56 | local der, err = util.create_csr(pkey, "dns1.com", "dns2.com", "dns3.com") 57 | if err then 58 | ngx.log(ngx.ERR, err) 59 | return 60 | end 61 | ngx.update_time() 62 | local fname = "ci_" .. math.floor(ngx.now() * 1000) 63 | local f = io.open(fname, "wb") 64 | f:write(der) 65 | f:close() 66 | ngx.say(io.popen("openssl req -inform der -in " .. fname .. " -noout -text", 'r'):read("*a")) 67 | os.remove(fname) 68 | } 69 | } 70 | --- request 71 | GET /t 72 | --- response_body_like eval 73 | ".+CN\\s*=\\s*dns1.com.+rsaEncryption.+4096 bit.+DNS:dns1.com.+DNS:dns2.com.+DNS:dns3.com" 74 | --- no_error_log 75 | [error] 76 | 77 | === TEST 3: Generates CSR with EC pkey correctly 78 | --- http_config eval: $::HttpConfig 79 | --- config 80 | location =/t { 81 | content_by_lua_block { 82 | local util = require("resty.acme.util") 83 | local openssl = require("resty.acme.openssl") 84 | local pkey = openssl.pkey.new({ 85 | type = 'EC', 86 | curve = 'prime256v1', 87 | }) 88 | local der, err = util.create_csr(pkey, "dns1.com", "dns2.com", "dns3.com") 89 | if err then 90 | ngx.log(ngx.ERR, err) 91 | return 92 | end 93 | ngx.update_time() 94 | local fname = "ci_" .. math.floor(ngx.now() * 1000) 95 | local f = io.open(fname, "wb") 96 | f:write(der) 97 | f:close() 98 | -- https://github.com/openssl/openssl/issues/8938 99 | ngx.say(io.popen("openssl asn1parse -inform der -in " .. fname, 'r'):read("*a")) 100 | os.remove(fname) 101 | } 102 | } 103 | --- request 104 | GET /t 105 | --- response_body_like eval 106 | "commonName.+dns1.com.+id-ecPublicKey.+prime.+301E8208646E73312E636F6D8208646E73322E636F6D8208646E73332E636F6D" 107 | --- no_error_log 108 | [error] 109 | 110 | 111 | === TEST 4: Checks root cert issuer CN correctly 112 | --- http_config eval: $::HttpConfig 113 | --- config 114 | location =/t { 115 | content_by_lua_block { 116 | local f = io.open("t/fixtures/multiple_certs.pem") 117 | local p = f:read("*a") 118 | f:close() 119 | local util = require("resty.acme.util") 120 | local ok, err = util.check_chain_root_issuer("", "me") 121 | ngx.say(ok, err) 122 | local ok, err = util.check_chain_root_issuer(p, "me") 123 | ngx.say(ok, err) 124 | local ok, err = util.check_chain_root_issuer(p, "test.com") 125 | ngx.say(ok, err) 126 | } 127 | } 128 | --- request 129 | GET /t 130 | --- response_body eval 131 | "falsecert not found in PEM chain 132 | falsecurrent chain root issuer common name is \"test.com\" 133 | truenil 134 | " 135 | --- no_error_log 136 | [error] --------------------------------------------------------------------------------