acme-http01-webroot.lua 2.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. -- ACME http-01 domain validation plugin for Haproxy 1.6+
  2. -- copyright (C) 2015 Jan Broer
  3. --
  4. -- usage:
  5. --
  6. -- 1) copy acme-webroot.lua in your haproxy config dir
  7. --
  8. -- 2) Invoke the plugin by adding in the 'global' section of haproxy.cfg:
  9. --
  10. -- lua-load /etc/haproxy/acme-webroot.lua
  11. --
  12. -- 3) insert these two lines in every http frontend that is
  13. -- serving domains for which you want to create certificates:
  14. --
  15. -- acl url_acme_http01 path_beg /.well-known/acme-challenge/
  16. -- http-request use-service lua.acme-http01 if METH_GET url_acme_http01
  17. --
  18. -- 4) reload haproxy
  19. --
  20. -- 5) create a certificate:
  21. --
  22. -- ./letsencrypt-auto certonly --text --webroot --webroot-path /var/tmp -d blah.example.com --renew-by-default --agree-tos --email my@email.com
  23. --
  24. --
  25. -- Configuration begin
  26. --
  27. -- Path passed to letsencrypt via the '--webroot-path' parameter must match this
  28. WEBROOT = "/var/tmp"
  29. --
  30. -- Configuration end
  31. --
  32. VERSION = "0.1.0"
  33. core.Info("[acme] http-01 plugin v." .. VERSION .. " loaded");
  34. --
  35. -- ACME http-01 validation endpoint
  36. --
  37. core.register_service("acme-http01", "http", function(applet)
  38. local response = ""
  39. local reqPath = applet.sf:path()
  40. local src = applet.sf:src()
  41. local token = reqPath:match( ".+/(.*)$" )
  42. if token then
  43. token = sanitizeToken(token)
  44. end
  45. if (token == nil or token == '') then
  46. response = "bad request\n"
  47. applet:set_status(400)
  48. core.Warning("[acme] malformed request (client-ip: " .. tostring(src) .. ")")
  49. else
  50. auth = getKeyAuth(token)
  51. if (auth:len() >= 1) then
  52. response = auth .. "\n"
  53. applet:set_status(200)
  54. core.Info("[acme] served http-01 token: " .. token .. " (client-ip: " .. tostring(src) .. ")")
  55. else
  56. response = "resource not found\n"
  57. applet:set_status(404)
  58. core.Warning("[acme] http-01 token not found: " .. token .. " (client-ip: " .. tostring(src) .. ")")
  59. end
  60. end
  61. applet:add_header("Server", "haproxy/acme-http01-authenticator")
  62. applet:add_header("Content-Length", string.len(response))
  63. applet:add_header("Content-Type", "text/plain")
  64. applet:start_response()
  65. applet:send(response)
  66. end)
  67. --
  68. -- strip chars that are not in the URL-safe Base64 alphabet
  69. -- see https://github.com/letsencrypt/acme-spec/blob/master/draft-barnes-acme.md
  70. --
  71. function sanitizeToken(token)
  72. _strip="[^%a%d%+%-%_=]"
  73. token = token:gsub(_strip,'')
  74. return token
  75. end
  76. --
  77. -- get key auth from token file
  78. --
  79. function getKeyAuth(token)
  80. local keyAuth = ""
  81. local f = io.open(WEBROOT .. "/.well-known/acme-challenge/" .. token, "rb")
  82. if f ~= nil then
  83. keyAuth = f:read("*all")
  84. f:close()
  85. end
  86. return keyAuth
  87. end