├── .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]
--------------------------------------------------------------------------------