Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Eliminate dependency on GNU version of "date" #196

Merged
merged 5 commits into from
Oct 1, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# lua-resty-auto-ssl Change Log

## 0.13.1 - Unreleased

### Changed
- 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)

## 0.13.0 - 2019-09-30

### Upgrade Notes
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile-test-alpine
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ WORKDIR /app
# Runtime dependencies
RUN apk add --no-cache \
bash \
coreutils \
curl \
diffutils \
grep \
Expand All @@ -27,6 +26,7 @@ RUN apk add --no-cache \
procps \
redis \
sudo \
tzdata \
wget && \
curl -fsSL -o /tmp/ngrok.tar.gz https://bin.equinox.io/a/naDTyS8Kyxv/ngrok-2.3.34-linux-386.tar.gz && \
tar -xvf /tmp/ngrok.tar.gz -C /usr/local/bin/ && \
Expand Down
5 changes: 4 additions & 1 deletion Dockerfile-test-ubuntu
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
FROM openresty/openresty:1.15.8.2-1-bionic

ENV DEBIAN_FRONTEND noninteractive

# Runtime dependencies
RUN apt-get update && \
apt-get -y install \
Expand All @@ -22,7 +24,8 @@ RUN apt-get update && \
lsof \
lua5.2 \
redis-server \
sudo && \
sudo \
tzdata && \
curl -fsSL -o /tmp/ngrok.deb https://bin.equinox.io/a/b2wQezFbsHk/ngrok-2.3.34-linux-amd64.deb && \
dpkg -i /tmp/ngrok.deb || apt-get -fy install && \
rm -f /tmp/ngrok.deb
Expand Down
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ install: check-dependencies
install -m 644 lib/resty/auto-ssl/storage_adapters/file.lua $(INST_LUADIR)/resty/auto-ssl/storage_adapters/file.lua
install -m 644 lib/resty/auto-ssl/storage_adapters/redis.lua $(INST_LUADIR)/resty/auto-ssl/storage_adapters/redis.lua
install -d $(INST_LUADIR)/resty/auto-ssl/utils
install -m 644 lib/resty/auto-ssl/utils/parse_openssl_time.lua $(INST_LUADIR)/resty/auto-ssl/utils/parse_openssl_time.lua
install -m 644 lib/resty/auto-ssl/utils/random_seed.lua $(INST_LUADIR)/resty/auto-ssl/utils/random_seed.lua
install -m 644 lib/resty/auto-ssl/utils/shell_execute.lua $(INST_LUADIR)/resty/auto-ssl/utils/shell_execute.lua
install -m 644 lib/resty/auto-ssl/utils/shuffle_table.lua $(INST_LUADIR)/resty/auto-ssl/utils/shuffle_table.lua
Expand Down Expand Up @@ -105,6 +106,7 @@ lint:
luacheck lib spec

test:
luarocks --tree=/tmp/resty-auto-ssl-test-luarocks make ./lua-resty-auto-ssl-git-1.rockspec
rm -rf /tmp/resty-auto-ssl-server-luarocks
luarocks --tree=/tmp/resty-auto-ssl-server-luarocks make ./lua-resty-auto-ssl-git-1.rockspec
luarocks --tree=/tmp/resty-auto-ssl-server-luarocks install dkjson 2.5-2
Expand Down
2 changes: 1 addition & 1 deletion bin/letsencrypt_hooks
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ clean_challenge() {
deploy_cert() {
local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" TIMESTAMP="${6}"
local EXPIRY
if ! EXPIRY=$(date --date="$(openssl x509 -enddate -noout -in "$CERTFILE"|cut -d= -f 2)" +%s); then
if ! EXPIRY=$(openssl x509 -enddate -noout -in "$CERTFILE"); then
echo "failed to get the expiry date"
fi

Expand Down
11 changes: 8 additions & 3 deletions lib/resty/auto-ssl/jobs/renewal.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
local lock = require "resty.lock"
local parse_openssl_time = require "resty.auto-ssl.utils.parse_openssl_time"
local shell_blocking = require "shell-games"
local shuffle_table = require "resty.auto-ssl.utils.shuffle_table"
local ssl_provider = require "resty.auto-ssl.ssl_providers.lets_encrypt"
Expand Down Expand Up @@ -97,12 +98,16 @@ local function renew_check_cert(auto_ssl_instance, storage, domain)
file:write(cert["fullchain_pem"])
file:close()

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" })
local date_result, date_err = shell_blocking.capture_combined({ "openssl", "x509", "-enddate", "-noout", "-in", cert_pem_path })
if date_err then
ngx.log(ngx.ERR, "auto-ssl: failed to extract expiry date from cert: ", date_err)
else
cert["expiry"] = tonumber(date_result["output"])
if cert["expiry"] then
local expiry, parse_err = parse_openssl_time(date_result["output"])
if parse_err then
ngx.log(ngx.ERR, "auto-ssl: failed to parse expiry date: ", parse_err)
else
cert["expiry"] = expiry

-- Update stored certificate to include expiry information
ngx.log(ngx.NOTICE, "auto-ssl: setting expiration date of ", domain, " to ", cert["expiry"])
local _, set_cert_err = storage:set_cert(domain, cert["fullchain_pem"], cert["privkey_pem"], cert["cert_pem"], cert["expiry"])
Expand Down
9 changes: 8 additions & 1 deletion lib/resty/auto-ssl/servers/hook.lua
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
local parse_openssl_time = require "resty.auto-ssl.utils.parse_openssl_time"
local shell_blocking = require "shell-games"

-- This server provides an internal-only API for the dehydrated bash hook
Expand Down Expand Up @@ -42,7 +43,13 @@ return function(auto_ssl_instance)
assert(params["fullchain"])
assert(params["privkey"])
assert(params["expiry"])
local _, err = storage:set_cert(params["domain"], params["fullchain"], params["privkey"], params["cert"], tonumber(params["expiry"]))

local expiry, parse_err = parse_openssl_time(params["expiry"])
if parse_err then
ngx.log(ngx.ERR, "auto-ssl: failed to parse expiry date: ", parse_err)
end

local _, err = storage:set_cert(params["domain"], params["fullchain"], params["privkey"], params["cert"], expiry)
if err then
ngx.log(ngx.ERR, "auto-ssl: failed to set cert: ", err)
return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
Expand Down
68 changes: 68 additions & 0 deletions lib/resty/auto-ssl/utils/parse_openssl_time.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
local floor = math.floor

local months = {
Jan = 1,
Feb = 2,
Mar = 3,
Apr = 4,
May = 5,
Jun = 6,
Jul = 7,
Aug = 8,
Sep = 9,
Oct = 10,
Nov = 11,
Dec = 12,
}

-- Parse the time strings that OpenSSL outputs via ASN1_TIME_print:
-- https://www.openssl.org/docs/man1.1.1/man3/ASN1_TIME_print.html
--
-- Relevant pieces of specification:
--
-- > It will be of the format MMM DD HH:MM:SS YYYY [GMT], for example "Feb 3
-- > 00:55:52 2015 GMT"
-- > Does not print out the time zone: it either prints out "GMT" or nothing.
-- > But all certificates complying with RFC5280 et al use GMT anyway.
return function(time_str)
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")
if match_err then
return nil, match_err
elseif not matches then
return nil, "could not parse openssl time string: " .. (tostring(time_str) or "")
end

local month = months[matches["month"]]
if not month then
return nil, "could not parse month in openssl time string: " .. (tostring(time_str) or "")
end

local year = tonumber(matches["year"])
local day = tonumber(matches["day"])
local hour = tonumber(matches["hour"])
local minute = tonumber(matches["minute"])
local second = tonumber(matches["second"])

-- Convert the parsed time into a unix epoch timestamp. Since the unix
-- timestamp should always be returned according to UTC, we can't use Lua's
-- "os.time", since it returns values based on local time
-- (http://lua-users.org/lists/lua-l/2012-04/msg00557.html), and workarounds
-- seem tricky (http://lua-users.org/lists/lua-l/2012-04/msg00588.html).
--
-- So instead, manually calculate the days since UTC epoch and output based
-- on this math. The algorithm behind this is based on
-- http://howardhinnant.github.io/date_algorithms.html#civil_from_days
if month <= 2 then
year = year - 1
month = month + 9
else
month = month - 3
end
local era = floor(year / 400)
local yoe = year - era * 400
local doy = floor((153 * month + 2) / 5) + day - 1
local doe = (yoe * 365) + floor(yoe / 4) - floor(yoe / 100) + doy
local days = era * 146097 + doe - 719468

return (days * 86400) + (hour * 3600) + (minute * 60) + second
end
185 changes: 185 additions & 0 deletions spec/expiry_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
local cjson = require "cjson.safe"
local file = require "pl.file"
local http = require "resty.http"
local server = require "spec.support.server"
local shell_blocking = require "shell-games"

describe("expiry", function()
before_each(server.stop)
after_each(server.stop)

it("stores the expiry date on issuance", function()
server.start()

local httpc = http.new()
local _, connect_err = httpc:connect("127.0.0.1", 9443)
assert.equal(nil, connect_err)

local _, ssl_err = httpc:ssl_handshake(nil, server.ngrok_hostname, true)
assert.equal(nil, ssl_err)

local res, request_err = httpc:request({ path = "/foo" })
assert.equal(nil, request_err)
assert.equal(200, res.status)

local body, body_err = res:read_body()
assert.equal(nil, body_err)
assert.equal("foo", body)

local error_log = server.nginx_error_log_tail:read()
assert.matches("issuing new certificate for " .. server.ngrok_hostname, error_log, nil, true)
assert.Not.matches("auto-ssl: checking certificate renewals for " .. server.ngrok_hostname, error_log, nil, true)
assert.Not.matches("failed to get the expiry date", error_log, nil, true)

local cert_path = server.current_test_dir .. "/auto-ssl/storage/file/" .. ngx.escape_uri(server.ngrok_hostname .. ":latest")
local content = assert(file.read(cert_path))
assert.string(content)
local data = assert(cjson.decode(content))
assert.number(data["expiry"])
assert(data["expiry"] > 0, data["expiry"] .. " is not greater than 0")
end)

it("fills in missing expiry dates in storage from certificate expiration on renewal", function()
server.start({
auto_ssl_pre_new = [[
options["renew_check_interval"] = 1
]],
})

local httpc = http.new()
local _, connect_err = httpc:connect("127.0.0.1", 9443)
assert.equal(nil, connect_err)

local _, ssl_err = httpc:ssl_handshake(nil, server.ngrok_hostname, true)
assert.equal(nil, ssl_err)

local res, request_err = httpc:request({ path = "/foo" })
assert.equal(nil, request_err)
assert.equal(200, res.status)

local body, body_err = res:read_body()
assert.equal(nil, body_err)
assert.equal("foo", body)

local error_log = server.nginx_error_log_tail:read()
assert.matches("issuing new certificate for", error_log, nil, true)

local cert_path = server.current_test_dir .. "/auto-ssl/storage/file/" .. ngx.escape_uri(server.ngrok_hostname .. ":latest")
local content = assert(file.read(cert_path))
assert.string(content)
local data = assert(cjson.decode(content))
local original_expiry = data["expiry"]
assert.number(data["expiry"])

-- Unset the expiration time.
data["expiry"] = nil
assert.Nil(data["expiry"])

assert(file.write(cert_path, assert(cjson.encode(data))))

-- Wait for scheduled renewals to happen.
ngx.sleep(3)

error_log = server.nginx_error_log_tail:read()
assert.matches("auto-ssl: checking certificate renewals for " .. server.ngrok_hostname, error_log, nil, true)
assert.matches("auto-ssl: setting expiration date of " .. server.ngrok_hostname, error_log, nil, true)
assert.matches("auto-ssl: expiry date is more than 30 days out, skipping renewal: " .. server.ngrok_hostname, error_log, nil, true)

content = assert(file.read(cert_path))
assert.string(content)
data = assert(cjson.decode(content))
assert.number(data["expiry"])
assert.equal(original_expiry, data["expiry"])

error_log = server.read_error_log()
assert.Not.matches("[warn]", error_log, nil, true)
assert.Not.matches("[error]", error_log, nil, true)
assert.Not.matches("[alert]", error_log, nil, true)
assert.Not.matches("[emerg]", error_log, nil, true)
end)

it("removes cert if expiration has expired and renewal fails", function()
server.start({
auto_ssl_pre_new = [[
options["renew_check_interval"] = 1
]],
})

local httpc = http.new()
local _, connect_err = httpc:connect("127.0.0.1", 9443)
assert.equal(nil, connect_err)

local _, ssl_err = httpc:ssl_handshake(nil, server.ngrok_hostname, true)
assert.equal(nil, ssl_err)

local res, request_err = httpc:request({ path = "/foo" })
assert.equal(nil, request_err)
assert.equal(200, res.status)

local body, body_err = res:read_body()
assert.equal(nil, body_err)
assert.equal("foo", body)

local error_log = server.nginx_error_log_tail:read()
assert.matches("issuing new certificate for", error_log, nil, true)

local cert_path = server.current_test_dir .. "/auto-ssl/storage/file/" .. ngx.escape_uri(server.ngrok_hostname .. ":latest")
local content = assert(file.read(cert_path))
assert.string(content)
local data = assert(cjson.decode(content))
assert.number(data["expiry"])

-- Set the expiration time to some time in the past.
data["expiry"] = 1000

assert(file.write(cert_path, assert(cjson.encode(data))))

-- Wait for scheduled renewals to happen.
ngx.sleep(3)

error_log = server.nginx_error_log_tail:read()
assert.matches("auto-ssl: checking certificate renewals for " .. server.ngrok_hostname, error_log, nil, true)
assert.matches("Skipping renew!", error_log, nil, true)

-- Since this cert renewal is still valid, it should still remain despite
-- being marked as expired.
content = assert(file.read(cert_path))
assert.string(content)
data = assert(cjson.decode(content))
assert.number(data["expiry"])

-- Copy the cert to an unresolvable domain to verify that failed renewals
-- will be removed.
local unresolvable_cert_path = server.current_test_dir .. "/auto-ssl/storage/file/" .. ngx.escape_uri("unresolvable-sdjfklsdjf.example:latest")
local _, cp_err = shell_blocking.capture_combined({ "cp", "-p", cert_path, unresolvable_cert_path })
assert.equal(nil, cp_err)

-- Wait for scheduled renewals to happen.
ngx.sleep(5)

error_log = server.nginx_error_log_tail:read()
assert.matches("auto-ssl: checking certificate renewals for " .. server.ngrok_hostname, error_log, nil, true)
assert.matches("Skipping renew!", error_log, nil, true)
assert.matches("auto-ssl: checking certificate renewals for unresolvable-sdjfklsdjf.example", error_log, nil, true)
assert.matches("Ignoring because renew was forced!", error_log, nil, true)
assert.matches("Name does not end in a public suffix", error_log, nil, true)
assert.matches("auto-ssl: issuing renewal certificate failed: dehydrated failure", error_log, nil, true)
assert.matches("auto-ssl: existing certificate is expired, deleting: unresolvable-sdjfklsdjf.example", error_log, nil, true)

-- Verify that the valid cert still remains (despite being marked as
-- expired).
content = assert(file.read(cert_path))
assert.string(content)
data = assert(cjson.decode(content))
assert.number(data["expiry"])

-- Verify that the failed renewal gets deleted.
local file_content, file_err = file.read(unresolvable_cert_path)
assert.equal(nil, file_content)
assert.matches("No such file or directory", file_err, nil, true)

error_log = server.read_error_log()
assert.Not.matches("[alert]", error_log, nil, true)
assert.Not.matches("[emerg]", error_log, nil, true)
end)
end)
Loading