Skip to content
This repository was archived by the owner on Jun 18, 2024. It is now read-only.

Commit 5a74005

Browse files
authored
Merge pull request #7 from delormejonathan/fix-acmev2-getaspost-issue
Compliance with ACMEv2 POST-as-GET API update
2 parents 55c050e + 0613cbd commit 5a74005

File tree

1 file changed

+100
-23
lines changed

1 file changed

+100
-23
lines changed

acme.lua

100644100755
Lines changed: 100 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,12 @@ function ACME.create(conf)
7373
local resp
7474
local err
7575

76-
for _, d in ipairs({"/directory", "/dir", "/"}) do
77-
resp, err = http.get{url=config.ca.proxy_uri .. d}
78-
79-
if resp and resp.status_code == 200 and resp.headers["content-type"]
80-
and resp.headers["content-type"]:match("application/json") then
81-
self.resources = json.decode(resp.content)
82-
self.nonce = resp.headers["replay-nonce"]
83-
break
84-
end
76+
resp, err = self:get{url=config.ca.proxy_uri .. "/directory"}
77+
78+
if resp and resp.status_code == 200 and resp.headers["content-type"]
79+
and resp.headers["content-type"]:match("application/json") then
80+
self.resources = json.decode(resp.content)
81+
self.nonce = resp.headers["replay-nonce"]
8582
end
8683

8784
if not self.resources then
@@ -103,14 +100,31 @@ end
103100

104101
--- Adapt resource URLs when going through HAProxy
105102
function ACME.proxy_url(self, url)
106-
107103
if url:sub(1, #self.conf.ca.uri) == self.conf.ca.uri then
108104
return string.format("%s%s", self.conf.ca.proxy_uri, url:sub(#self.conf.ca.uri))
109105
else
110106
return url
111107
end
112108
end
113109

110+
--- ACME wrapper for http.get()
111+
--
112+
-- @param resource ACME resource type
113+
-- @param url Valid HTTP url (mandatory)G
114+
--
115+
-- @return Response object or tuple (nil, msg) on errors
116+
function ACME.get(self, t)
117+
local resp, err = http.get{url=self:proxy_url(t.url)}
118+
119+
if (not t.retry or t.retry < 5) and (not resp or (resp and resp.status_code == 503)) then
120+
t.retry = not t.retry and 1 or t.retry + 1
121+
122+
return self:get(t)
123+
end
124+
125+
return resp, err
126+
end
127+
114128
--- ACME wrapper for http.post()
115129
--
116130
-- @param resource ACME resource type
@@ -136,6 +150,7 @@ function ACME.post(self, t)
136150

137151
local resp, err = http.post{url=self:proxy_url(t.url), data=jws,
138152
headers=t.headers, timeout=t.timeout}
153+
139154
if resp and resp.headers then
140155
self.nonce = resp.headers["replay-nonce"]
141156

@@ -151,12 +166,74 @@ function ACME.post(self, t)
151166
return nil, err
152167
end
153168

154-
resp, err = http.post{url=self:proxy_url(t.url), data=jws,
169+
resp, err = self:post{url=self:proxy_url(t.url), data=jws,
155170
headers=t.headers}
156171
end
157172
end
158173
end
159174

175+
if (not t.retry or t.retry < 5) and (not resp or (resp and resp.status_code == 503)) then
176+
t.retry = not t.retry and 1 or t.retry + 1
177+
178+
return self:post(t)
179+
end
180+
181+
return resp, err
182+
end
183+
184+
--- ACME wrapper for POST-as-GET
185+
--
186+
-- @param resource ACME resource type
187+
-- @param url Valid HTTP url (mandatory)G
188+
-- @param headers Lua table with request headers
189+
-- @param data Request content
190+
--
191+
-- @return Response object or tuple (nil, msg) on errors
192+
function ACME.postAsGet(self, t)
193+
local jws, err = self:jws{url=t.url, payload=nil}
194+
195+
if not jws then
196+
return nil, err
197+
end
198+
199+
if not t.headers then
200+
t.headers = {
201+
["content-type"] = "application/jose+json"
202+
}
203+
elseif not t.headers["content-type"] then
204+
t.headers["content-type"] = "application/jose+json"
205+
end
206+
207+
local resp, err = http.post{url=self:proxy_url(t.url), data=jws,
208+
headers=t.headers, timeout=t.timeout}
209+
210+
if resp and resp.headers then
211+
self.nonce = resp.headers["replay-nonce"]
212+
213+
if resp.status_code == 400 then
214+
local info = resp:json()
215+
216+
if info and info.type == "urn:ietf:params:acme:error:badNonce" then
217+
218+
-- We need to retry once more with new nonce (hence new jws)
219+
jws, err = self:jws{resource=t.resource, url=t.url,
220+
payload=""}
221+
if not jws then
222+
return nil, err
223+
end
224+
225+
resp, err = self:post{url=self:proxy_url(t.url), data=jws,
226+
headers=t.headers}
227+
end
228+
end
229+
end
230+
231+
if (not t.retry or t.retry < 5) and (not resp or (resp and resp.status_code == 503)) then
232+
t.retry = not t.retry and 1 or t.retry + 1
233+
234+
return self:postAsGet(t)
235+
end
236+
160237
return resp, err
161238
end
162239

@@ -171,14 +248,13 @@ function ACME.refresh_nonce(self)
171248
self.nonce = nil
172249
if nonce then return nonce end
173250

251+
local resp, e = http.head{url=self:proxy_url(self.resources["newNonce"])}
174252

175-
local r, e = http.head{url=self:proxy_url(self.resources["newNonce"])}
176-
177-
if r and r.headers then
253+
if resp and resp.headers then
178254
-- TODO: Expect status code 204
179255
-- TODO: Expect Cache-Control: no-store
180256
-- TODO: Expect content size 0
181-
return r.headers["replay-nonce"]
257+
return resp.headers["replay-nonce"]
182258
else
183259
return nil, e
184260
end
@@ -194,9 +270,9 @@ function ACME.jws(self, t)
194270
return nil, "ACME.jws: Account key does not exist."
195271
end
196272

197-
if not t or not t.resource or not t.url or not t.payload then
273+
if not t or not t.url then
198274
return nil,
199-
"ACME.jws: Missing one or more parameters (resource, url, payload)"
275+
"ACME.jws: Missing one or more parameters (url)"
200276
end
201277

202278
-- if key:type() == rsaEncryption
@@ -233,7 +309,7 @@ function ACME.jws(self, t)
233309
end
234310

235311
jws.protected = http.base64.encode(json.encode(jws.protected), base64enc)
236-
jws.payload = http.base64.encode(json.encode(t.payload), base64enc)
312+
jws.payload = t.payload and http.base64.encode(json.encode(t.payload), base64enc) or ""
237313
local digest = openssl.digest.new("SHA256")
238314
digest:update(jws.protected .. "." .. jws.payload)
239315
jws.signature = http.base64.encode(self.account.key:sign(digest), base64enc)
@@ -341,7 +417,7 @@ local function new_order(applet)
341417
}
342418

343419
-- Get auth token
344-
local auth, err = http.get{url=acme:proxy_url(auth)}
420+
local auth, err = acme:postAsGet{url=auth}
345421

346422
if auth then
347423
local auth_json = auth:json()
@@ -351,7 +427,7 @@ local function new_order(applet)
351427
http_challenges[ch.token] = string.format("%s.%s",
352428
ch.token, acme.account.thumbprint)
353429
resp, err = acme:post{url=ch.url, data=ch,
354-
resource="challengeDone", timeout=1}
430+
resource="challengeDone"}
355431
challenge_token = ch.token
356432
break
357433
end
@@ -363,7 +439,8 @@ local function new_order(applet)
363439
local order_status
364440
for _, t in pairs({1, 1, 2, 3, 5, 8, 13}) do
365441
core.sleep(t)
366-
local resp, err = http.get{url=acme:proxy_url(order.headers["location"])}
442+
local resp, err = acme:postAsGet{url=order.headers["location"]}
443+
367444
if resp then
368445
order_status = resp:json()
369446
if order_status.status == "ready" then
@@ -424,7 +501,7 @@ local function new_order(applet)
424501
return http.response.create{status_code=500, content="No cert"}:send(applet)
425502
end
426503

427-
local resp, err = http.get{url=acme:proxy_url(resp_json.certificate)}
504+
local resp, err = acme:postAsGet{url=resp_json.certificate}
428505
local bundle = string.format("%s%s", resp.content, key:toPEM("private"))
429506
return http.response.create{status_code=200, content=bundle}:send(applet)
430507
else
@@ -476,4 +553,4 @@ local function main(applet)
476553
end
477554
end
478555

479-
core.register_service("acme", "http", main)
556+
core.register_service("acme", "http", main)

0 commit comments

Comments
 (0)