Skip to content

Commit f60f359

Browse files
authored
Merge pull request #196 from GUI/no-gnu-date
Eliminate dependency on GNU version of "date"
2 parents 81dae33 + 39fd352 commit f60f359

File tree

11 files changed

+471
-152
lines changed

11 files changed

+471
-152
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# lua-resty-auto-ssl Change Log
22

3+
## 0.13.1 - Unreleased
4+
5+
### Changed
6+
- Eliminate dependency on GNU version of the `date` command line utility to improve compatibility with Alpine Linux, BSDs, and others. Fixes warnings that may have started getting logged in v0.13.0. [#195](https://github.com/GUI/lua-resty-auto-ssl/issues/195)
7+
38
## 0.13.0 - 2019-09-30
49

510
### Upgrade Notes

Dockerfile-test-alpine

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ WORKDIR /app
66
# Runtime dependencies
77
RUN apk add --no-cache \
88
bash \
9-
coreutils \
109
curl \
1110
diffutils \
1211
grep \
@@ -27,6 +26,7 @@ RUN apk add --no-cache \
2726
procps \
2827
redis \
2928
sudo \
29+
tzdata \
3030
wget && \
3131
curl -fsSL -o /tmp/ngrok.tar.gz https://bin.equinox.io/a/naDTyS8Kyxv/ngrok-2.3.34-linux-386.tar.gz && \
3232
tar -xvf /tmp/ngrok.tar.gz -C /usr/local/bin/ && \

Dockerfile-test-ubuntu

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
FROM openresty/openresty:1.15.8.2-1-bionic
22

3+
ENV DEBIAN_FRONTEND noninteractive
4+
35
# Runtime dependencies
46
RUN apt-get update && \
57
apt-get -y install \
@@ -22,7 +24,8 @@ RUN apt-get update && \
2224
lsof \
2325
lua5.2 \
2426
redis-server \
25-
sudo && \
27+
sudo \
28+
tzdata && \
2629
curl -fsSL -o /tmp/ngrok.deb https://bin.equinox.io/a/b2wQezFbsHk/ngrok-2.3.34-linux-amd64.deb && \
2730
dpkg -i /tmp/ngrok.deb || apt-get -fy install && \
2831
rm -f /tmp/ngrok.deb

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ install: check-dependencies
4747
install -m 644 lib/resty/auto-ssl/storage_adapters/file.lua $(INST_LUADIR)/resty/auto-ssl/storage_adapters/file.lua
4848
install -m 644 lib/resty/auto-ssl/storage_adapters/redis.lua $(INST_LUADIR)/resty/auto-ssl/storage_adapters/redis.lua
4949
install -d $(INST_LUADIR)/resty/auto-ssl/utils
50+
install -m 644 lib/resty/auto-ssl/utils/parse_openssl_time.lua $(INST_LUADIR)/resty/auto-ssl/utils/parse_openssl_time.lua
5051
install -m 644 lib/resty/auto-ssl/utils/random_seed.lua $(INST_LUADIR)/resty/auto-ssl/utils/random_seed.lua
5152
install -m 644 lib/resty/auto-ssl/utils/shell_execute.lua $(INST_LUADIR)/resty/auto-ssl/utils/shell_execute.lua
5253
install -m 644 lib/resty/auto-ssl/utils/shuffle_table.lua $(INST_LUADIR)/resty/auto-ssl/utils/shuffle_table.lua
@@ -105,6 +106,7 @@ lint:
105106
luacheck lib spec
106107

107108
test:
109+
luarocks --tree=/tmp/resty-auto-ssl-test-luarocks make ./lua-resty-auto-ssl-git-1.rockspec
108110
rm -rf /tmp/resty-auto-ssl-server-luarocks
109111
luarocks --tree=/tmp/resty-auto-ssl-server-luarocks make ./lua-resty-auto-ssl-git-1.rockspec
110112
luarocks --tree=/tmp/resty-auto-ssl-server-luarocks install dkjson 2.5-2

bin/letsencrypt_hooks

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ clean_challenge() {
3434
deploy_cert() {
3535
local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" TIMESTAMP="${6}"
3636
local EXPIRY
37-
if ! EXPIRY=$(date --date="$(openssl x509 -enddate -noout -in "$CERTFILE"|cut -d= -f 2)" +%s); then
37+
if ! EXPIRY=$(openssl x509 -enddate -noout -in "$CERTFILE"); then
3838
echo "failed to get the expiry date"
3939
fi
4040

lib/resty/auto-ssl/jobs/renewal.lua

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
local lock = require "resty.lock"
2+
local parse_openssl_time = require "resty.auto-ssl.utils.parse_openssl_time"
23
local shell_blocking = require "shell-games"
34
local shuffle_table = require "resty.auto-ssl.utils.shuffle_table"
45
local ssl_provider = require "resty.auto-ssl.ssl_providers.lets_encrypt"
@@ -97,12 +98,16 @@ local function renew_check_cert(auto_ssl_instance, storage, domain)
9798
file:write(cert["fullchain_pem"])
9899
file:close()
99100

100-
local date_result, date_err = shell_blocking.run_raw('date --date="$(openssl x509 -enddate -noout -in "' .. shell_blocking.quote(cert_pem_path) .. '"|cut -d= -f 2)" +%s', { capture = true, stderr = "&1" })
101+
local date_result, date_err = shell_blocking.capture_combined({ "openssl", "x509", "-enddate", "-noout", "-in", cert_pem_path })
101102
if date_err then
102103
ngx.log(ngx.ERR, "auto-ssl: failed to extract expiry date from cert: ", date_err)
103104
else
104-
cert["expiry"] = tonumber(date_result["output"])
105-
if cert["expiry"] then
105+
local expiry, parse_err = parse_openssl_time(date_result["output"])
106+
if parse_err then
107+
ngx.log(ngx.ERR, "auto-ssl: failed to parse expiry date: ", parse_err)
108+
else
109+
cert["expiry"] = expiry
110+
106111
-- Update stored certificate to include expiry information
107112
ngx.log(ngx.NOTICE, "auto-ssl: setting expiration date of ", domain, " to ", cert["expiry"])
108113
local _, set_cert_err = storage:set_cert(domain, cert["fullchain_pem"], cert["privkey_pem"], cert["cert_pem"], cert["expiry"])

lib/resty/auto-ssl/servers/hook.lua

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
local parse_openssl_time = require "resty.auto-ssl.utils.parse_openssl_time"
12
local shell_blocking = require "shell-games"
23

34
-- This server provides an internal-only API for the dehydrated bash hook
@@ -42,7 +43,13 @@ return function(auto_ssl_instance)
4243
assert(params["fullchain"])
4344
assert(params["privkey"])
4445
assert(params["expiry"])
45-
local _, err = storage:set_cert(params["domain"], params["fullchain"], params["privkey"], params["cert"], tonumber(params["expiry"]))
46+
47+
local expiry, parse_err = parse_openssl_time(params["expiry"])
48+
if parse_err then
49+
ngx.log(ngx.ERR, "auto-ssl: failed to parse expiry date: ", parse_err)
50+
end
51+
52+
local _, err = storage:set_cert(params["domain"], params["fullchain"], params["privkey"], params["cert"], expiry)
4653
if err then
4754
ngx.log(ngx.ERR, "auto-ssl: failed to set cert: ", err)
4855
return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
local floor = math.floor
2+
3+
local months = {
4+
Jan = 1,
5+
Feb = 2,
6+
Mar = 3,
7+
Apr = 4,
8+
May = 5,
9+
Jun = 6,
10+
Jul = 7,
11+
Aug = 8,
12+
Sep = 9,
13+
Oct = 10,
14+
Nov = 11,
15+
Dec = 12,
16+
}
17+
18+
-- Parse the time strings that OpenSSL outputs via ASN1_TIME_print:
19+
-- https://www.openssl.org/docs/man1.1.1/man3/ASN1_TIME_print.html
20+
--
21+
-- Relevant pieces of specification:
22+
--
23+
-- > It will be of the format MMM DD HH:MM:SS YYYY [GMT], for example "Feb 3
24+
-- > 00:55:52 2015 GMT"
25+
-- > Does not print out the time zone: it either prints out "GMT" or nothing.
26+
-- > But all certificates complying with RFC5280 et al use GMT anyway.
27+
return function(time_str)
28+
local matches, match_err = ngx.re.match(time_str, [[(?<month>[A-Za-z]{3}) +(?<day>\d{1,2}) +(?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2})(?:\.\d+)? +(?<year>-?\d{4})]], "jo")
29+
if match_err then
30+
return nil, match_err
31+
elseif not matches then
32+
return nil, "could not parse openssl time string: " .. (tostring(time_str) or "")
33+
end
34+
35+
local month = months[matches["month"]]
36+
if not month then
37+
return nil, "could not parse month in openssl time string: " .. (tostring(time_str) or "")
38+
end
39+
40+
local year = tonumber(matches["year"])
41+
local day = tonumber(matches["day"])
42+
local hour = tonumber(matches["hour"])
43+
local minute = tonumber(matches["minute"])
44+
local second = tonumber(matches["second"])
45+
46+
-- Convert the parsed time into a unix epoch timestamp. Since the unix
47+
-- timestamp should always be returned according to UTC, we can't use Lua's
48+
-- "os.time", since it returns values based on local time
49+
-- (http://lua-users.org/lists/lua-l/2012-04/msg00557.html), and workarounds
50+
-- seem tricky (http://lua-users.org/lists/lua-l/2012-04/msg00588.html).
51+
--
52+
-- So instead, manually calculate the days since UTC epoch and output based
53+
-- on this math. The algorithm behind this is based on
54+
-- http://howardhinnant.github.io/date_algorithms.html#civil_from_days
55+
if month <= 2 then
56+
year = year - 1
57+
month = month + 9
58+
else
59+
month = month - 3
60+
end
61+
local era = floor(year / 400)
62+
local yoe = year - era * 400
63+
local doy = floor((153 * month + 2) / 5) + day - 1
64+
local doe = (yoe * 365) + floor(yoe / 4) - floor(yoe / 100) + doy
65+
local days = era * 146097 + doe - 719468
66+
67+
return (days * 86400) + (hour * 3600) + (minute * 60) + second
68+
end

spec/expiry_spec.lua

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
local cjson = require "cjson.safe"
2+
local file = require "pl.file"
3+
local http = require "resty.http"
4+
local server = require "spec.support.server"
5+
local shell_blocking = require "shell-games"
6+
7+
describe("expiry", function()
8+
before_each(server.stop)
9+
after_each(server.stop)
10+
11+
it("stores the expiry date on issuance", function()
12+
server.start()
13+
14+
local httpc = http.new()
15+
local _, connect_err = httpc:connect("127.0.0.1", 9443)
16+
assert.equal(nil, connect_err)
17+
18+
local _, ssl_err = httpc:ssl_handshake(nil, server.ngrok_hostname, true)
19+
assert.equal(nil, ssl_err)
20+
21+
local res, request_err = httpc:request({ path = "/foo" })
22+
assert.equal(nil, request_err)
23+
assert.equal(200, res.status)
24+
25+
local body, body_err = res:read_body()
26+
assert.equal(nil, body_err)
27+
assert.equal("foo", body)
28+
29+
local error_log = server.nginx_error_log_tail:read()
30+
assert.matches("issuing new certificate for " .. server.ngrok_hostname, error_log, nil, true)
31+
assert.Not.matches("auto-ssl: checking certificate renewals for " .. server.ngrok_hostname, error_log, nil, true)
32+
assert.Not.matches("failed to get the expiry date", error_log, nil, true)
33+
34+
local cert_path = server.current_test_dir .. "/auto-ssl/storage/file/" .. ngx.escape_uri(server.ngrok_hostname .. ":latest")
35+
local content = assert(file.read(cert_path))
36+
assert.string(content)
37+
local data = assert(cjson.decode(content))
38+
assert.number(data["expiry"])
39+
assert(data["expiry"] > 0, data["expiry"] .. " is not greater than 0")
40+
end)
41+
42+
it("fills in missing expiry dates in storage from certificate expiration on renewal", function()
43+
server.start({
44+
auto_ssl_pre_new = [[
45+
options["renew_check_interval"] = 1
46+
]],
47+
})
48+
49+
local httpc = http.new()
50+
local _, connect_err = httpc:connect("127.0.0.1", 9443)
51+
assert.equal(nil, connect_err)
52+
53+
local _, ssl_err = httpc:ssl_handshake(nil, server.ngrok_hostname, true)
54+
assert.equal(nil, ssl_err)
55+
56+
local res, request_err = httpc:request({ path = "/foo" })
57+
assert.equal(nil, request_err)
58+
assert.equal(200, res.status)
59+
60+
local body, body_err = res:read_body()
61+
assert.equal(nil, body_err)
62+
assert.equal("foo", body)
63+
64+
local error_log = server.nginx_error_log_tail:read()
65+
assert.matches("issuing new certificate for", error_log, nil, true)
66+
67+
local cert_path = server.current_test_dir .. "/auto-ssl/storage/file/" .. ngx.escape_uri(server.ngrok_hostname .. ":latest")
68+
local content = assert(file.read(cert_path))
69+
assert.string(content)
70+
local data = assert(cjson.decode(content))
71+
local original_expiry = data["expiry"]
72+
assert.number(data["expiry"])
73+
74+
-- Unset the expiration time.
75+
data["expiry"] = nil
76+
assert.Nil(data["expiry"])
77+
78+
assert(file.write(cert_path, assert(cjson.encode(data))))
79+
80+
-- Wait for scheduled renewals to happen.
81+
ngx.sleep(3)
82+
83+
error_log = server.nginx_error_log_tail:read()
84+
assert.matches("auto-ssl: checking certificate renewals for " .. server.ngrok_hostname, error_log, nil, true)
85+
assert.matches("auto-ssl: setting expiration date of " .. server.ngrok_hostname, error_log, nil, true)
86+
assert.matches("auto-ssl: expiry date is more than 30 days out, skipping renewal: " .. server.ngrok_hostname, error_log, nil, true)
87+
88+
content = assert(file.read(cert_path))
89+
assert.string(content)
90+
data = assert(cjson.decode(content))
91+
assert.number(data["expiry"])
92+
assert.equal(original_expiry, data["expiry"])
93+
94+
error_log = server.read_error_log()
95+
assert.Not.matches("[warn]", error_log, nil, true)
96+
assert.Not.matches("[error]", error_log, nil, true)
97+
assert.Not.matches("[alert]", error_log, nil, true)
98+
assert.Not.matches("[emerg]", error_log, nil, true)
99+
end)
100+
101+
it("removes cert if expiration has expired and renewal fails", function()
102+
server.start({
103+
auto_ssl_pre_new = [[
104+
options["renew_check_interval"] = 1
105+
]],
106+
})
107+
108+
local httpc = http.new()
109+
local _, connect_err = httpc:connect("127.0.0.1", 9443)
110+
assert.equal(nil, connect_err)
111+
112+
local _, ssl_err = httpc:ssl_handshake(nil, server.ngrok_hostname, true)
113+
assert.equal(nil, ssl_err)
114+
115+
local res, request_err = httpc:request({ path = "/foo" })
116+
assert.equal(nil, request_err)
117+
assert.equal(200, res.status)
118+
119+
local body, body_err = res:read_body()
120+
assert.equal(nil, body_err)
121+
assert.equal("foo", body)
122+
123+
local error_log = server.nginx_error_log_tail:read()
124+
assert.matches("issuing new certificate for", error_log, nil, true)
125+
126+
local cert_path = server.current_test_dir .. "/auto-ssl/storage/file/" .. ngx.escape_uri(server.ngrok_hostname .. ":latest")
127+
local content = assert(file.read(cert_path))
128+
assert.string(content)
129+
local data = assert(cjson.decode(content))
130+
assert.number(data["expiry"])
131+
132+
-- Set the expiration time to some time in the past.
133+
data["expiry"] = 1000
134+
135+
assert(file.write(cert_path, assert(cjson.encode(data))))
136+
137+
-- Wait for scheduled renewals to happen.
138+
ngx.sleep(3)
139+
140+
error_log = server.nginx_error_log_tail:read()
141+
assert.matches("auto-ssl: checking certificate renewals for " .. server.ngrok_hostname, error_log, nil, true)
142+
assert.matches("Skipping renew!", error_log, nil, true)
143+
144+
-- Since this cert renewal is still valid, it should still remain despite
145+
-- being marked as expired.
146+
content = assert(file.read(cert_path))
147+
assert.string(content)
148+
data = assert(cjson.decode(content))
149+
assert.number(data["expiry"])
150+
151+
-- Copy the cert to an unresolvable domain to verify that failed renewals
152+
-- will be removed.
153+
local unresolvable_cert_path = server.current_test_dir .. "/auto-ssl/storage/file/" .. ngx.escape_uri("unresolvable-sdjfklsdjf.example:latest")
154+
local _, cp_err = shell_blocking.capture_combined({ "cp", "-p", cert_path, unresolvable_cert_path })
155+
assert.equal(nil, cp_err)
156+
157+
-- Wait for scheduled renewals to happen.
158+
ngx.sleep(5)
159+
160+
error_log = server.nginx_error_log_tail:read()
161+
assert.matches("auto-ssl: checking certificate renewals for " .. server.ngrok_hostname, error_log, nil, true)
162+
assert.matches("Skipping renew!", error_log, nil, true)
163+
assert.matches("auto-ssl: checking certificate renewals for unresolvable-sdjfklsdjf.example", error_log, nil, true)
164+
assert.matches("Ignoring because renew was forced!", error_log, nil, true)
165+
assert.matches("Name does not end in a public suffix", error_log, nil, true)
166+
assert.matches("auto-ssl: issuing renewal certificate failed: dehydrated failure", error_log, nil, true)
167+
assert.matches("auto-ssl: existing certificate is expired, deleting: unresolvable-sdjfklsdjf.example", error_log, nil, true)
168+
169+
-- Verify that the valid cert still remains (despite being marked as
170+
-- expired).
171+
content = assert(file.read(cert_path))
172+
assert.string(content)
173+
data = assert(cjson.decode(content))
174+
assert.number(data["expiry"])
175+
176+
-- Verify that the failed renewal gets deleted.
177+
local file_content, file_err = file.read(unresolvable_cert_path)
178+
assert.equal(nil, file_content)
179+
assert.matches("No such file or directory", file_err, nil, true)
180+
181+
error_log = server.read_error_log()
182+
assert.Not.matches("[alert]", error_log, nil, true)
183+
assert.Not.matches("[emerg]", error_log, nil, true)
184+
end)
185+
end)

0 commit comments

Comments
 (0)