acme-http01-api.lua 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. -- ACME http-01 domain validation and token provisioning plugin for Haproxy 1.6+
  2. -- copyright (C) 2015 Jan Broer
  3. -- serves http-01 validation resources from a map-store
  4. -- allows provisioning new validation resources via POST request
  5. -- usage:
  6. --
  7. -- challenge endpoint example haproxy.cfg:
  8. --
  9. -- frontend public-http
  10. -- ...
  11. -- acl url_acme_http01 path_beg /.well-known/acme-challenge/
  12. -- http-request use-service lua.acme-http01 if url_acme_http01
  13. --
  14. -- provision endpoint example haproxy.cfg:
  15. --
  16. -- frontend internal-http
  17. -- bind bind *:8081
  18. -- ...
  19. -- acl url_acme_provision path /.well-known/provision-token
  20. -- http-request lua.acme-provision if METH_POST url_acme_provision
  21. --
  22. -- Provision new http01 token/keyauth resource:
  23. --
  24. -- curl -X POST --header 'X-Auth-Secret: VERYSECRET' -d "token=123&authkey=ABC123" \
  25. -- http://haproxy-ip:8081/.well-known/provision-token
  26. --
  27. -- Begin configuration
  28. -- Client must send AUTH_SECRET in the 'X-Auth-Secret' header to provision new token
  29. AUTH_SECRET = "042Fa8_bf0/9fb0"
  30. -- Path to map file that stores key authorizations (dummy file must exist)
  31. AUTH_STORE = "/etc/haproxy/acme-store.map"
  32. -- End configuration
  33. acme = Map.new(AUTH_STORE, Map.str);
  34. -- http-01 token provisioning endpoint
  35. core.register_action("acme-provision", { "tcp-req", "http-req" }, function(txn)
  36. local msg = ""
  37. local response = ""
  38. local src = txn.sf:src()
  39. local contentType = txn.sf:hdr("Content-Type")
  40. if contentType == "application/x-www-form-urlencoded" then
  41. local auth = txn.sf:hdr("X-Auth-Secret")
  42. if auth == AUTH_SECRET then
  43. local token = txn.sf:req_body_param("token")
  44. local authkey = txn.sf:req_body_param("authkey")
  45. if (token ~= nil and token ~= '' and authkey ~= nil and authkey ~= '') then
  46. token = sanitizeToken(token)
  47. core.Info("[acme] http-01 token provisioned: " .. token .. " (client-ip: " .. tostring(src) .. ")")
  48. core.set_map(AUTH_STORE, token, authkey)
  49. msg = "OK"
  50. response = response .. "HTTP/1.0 200 OK\r\n"
  51. else
  52. core.Warning("[acme] failed to provision http-01 token: invalid request (client-ip: " .. tostring(src) .. ")")
  53. msg = "invalid request"
  54. response = response .. "HTTP/1.0 400 Bad Request\r\n"
  55. end
  56. else
  57. core.Warning("[acme] failed to provision http-01 token: invalid authentication (client-ip: " .. tostring(src) .. ")")
  58. msg = "invalid authentication"
  59. response = response .. "HTTP/1.0 401 Unauthorized\r\n"
  60. end
  61. else
  62. core.Warning("[acme] failed to provision http-01 token: invalid content-type: " .. contentType .." (client-ip: " .. tostring(src) .. ")")
  63. msg = "invalid content-type: " .. contentType
  64. response = response .. "HTTP/1.0 400 Bad Request\r\n"
  65. end
  66. response = response .. "Server: haproxy/acme-http01-authenticator\r\n"
  67. response = response .. "Content-Type: text/plain\r\n"
  68. response = response .. "Content-Length: " .. msg:len() .. "\r\n"
  69. response = response .. "Connection: close\r\n"
  70. response = response .. "\r\n"
  71. response = response .. msg
  72. txn.res:send(response)
  73. txn.done(txn)
  74. end)
  75. -- http-01 validation endpoint
  76. core.register_service("acme-http01", "http", function(applet)
  77. local response = ""
  78. local reqPath = applet.sf:path()
  79. local src = applet.sf:src()
  80. local token = reqPath:match( ".+/(.*)$" )
  81. if token then
  82. token = sanitizeToken(token)
  83. end
  84. if (token == nil or token == '') then
  85. response = "bad request\n"
  86. applet:set_status(400)
  87. core.Warning("[acme] malformed request (client-ip: " .. tostring(src) .. ")")
  88. else
  89. auth = getKeyAuth(token)
  90. if (auth:len() >= 1) then
  91. response = auth .. "\n"
  92. applet:set_status(200)
  93. core.Info("[acme] served http-01 token: " .. token .. " (client-ip: " .. tostring(src) .. ")")
  94. core.del_map(AUTH_STORE, token)
  95. else
  96. response = "resource not found\n"
  97. applet:set_status(404)
  98. core.Warning("[acme] http-01 token not found: " .. token .. " (client-ip: " .. tostring(src) .. ")")
  99. end
  100. end
  101. applet:add_header("Server", "haproxy/acme-http01-authenticator")
  102. applet:add_header("Content-Length", string.len(response))
  103. applet:add_header("Content-Type", "text/plain")
  104. applet:start_response()
  105. applet:send(response)
  106. end)
  107. -- strip chars that are not in the URL-safe Base64 alphabet
  108. -- see https://github.com/letsencrypt/acme-spec/blob/master/draft-barnes-acme.md
  109. function sanitizeToken(token)
  110. _strip="[^%a%d%+%-%_=]"
  111. token = token:gsub(_strip,'')
  112. return token
  113. end
  114. -- get key auth from token map store
  115. function getKeyAuth(token)
  116. local keyAuth = ""
  117. local k = acme:lookup(token)
  118. if k ~= nil then
  119. keyAuth = k
  120. end
  121. return keyAuth
  122. end