acme-http01-webroot.lua 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  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. acme = {}
  25. acme.version = "0.1.1"
  26. --
  27. -- Configuration
  28. --
  29. -- When HAProxy is *not* configured with the 'chroot' option you must set an absolute path here and pass
  30. -- that as 'webroot-path' to the letsencrypt client
  31. acme.conf = {
  32. ["non_chroot_webroot"] = ""
  33. }
  34. --
  35. -- Startup
  36. --
  37. acme.startup = function()
  38. core.Info("[acme] http-01 plugin v" .. acme.version);
  39. end
  40. --
  41. -- ACME http-01 validation endpoint
  42. --
  43. acme.http01 = function(applet)
  44. local response = ""
  45. local reqPath = applet.path
  46. local src = applet.sf:src()
  47. local token = reqPath:match( ".+/(.*)$" )
  48. if token then
  49. token = sanitizeToken(token)
  50. end
  51. if (token == nil or token == '') then
  52. response = "bad request\n"
  53. applet:set_status(400)
  54. core.Warning("[acme] malformed request (client-ip: " .. tostring(src) .. ")")
  55. else
  56. auth = getKeyAuth(token)
  57. if (auth:len() >= 1) then
  58. response = auth .. "\n"
  59. applet:set_status(200)
  60. core.Info("[acme] served http-01 token: " .. token .. " (client-ip: " .. tostring(src) .. ")")
  61. else
  62. response = "resource not found\n"
  63. applet:set_status(404)
  64. core.Warning("[acme] http-01 token not found: " .. token .. " (client-ip: " .. tostring(src) .. ")")
  65. end
  66. end
  67. applet:add_header("Server", "haproxy/acme-http01-authenticator")
  68. applet:add_header("Content-Length", string.len(response))
  69. applet:add_header("Content-Type", "text/plain")
  70. applet:start_response()
  71. applet:send(response)
  72. end
  73. --
  74. -- strip chars that are not in the URL-safe Base64 alphabet
  75. -- see https://github.com/letsencrypt/acme-spec/blob/master/draft-barnes-acme.md
  76. --
  77. function sanitizeToken(token)
  78. _strip="[^%a%d%+%-%_=]"
  79. token = token:gsub(_strip,'')
  80. return token
  81. end
  82. --
  83. -- get key auth from token file
  84. --
  85. function getKeyAuth(token)
  86. local keyAuth = ""
  87. local path = acme.conf.non_chroot_webroot .. "/.well-known/acme-challenge/" .. token
  88. local f = io.open(path, "rb")
  89. if f ~= nil then
  90. keyAuth = f:read("*all")
  91. f:close()
  92. end
  93. return keyAuth
  94. end
  95. core.register_init(acme.startup)
  96. core.register_service("acme-http01", "http", acme.http01)