Permalink
Browse files

Add ACME challenge passthrough route for Let's Encrypt support.

  • Loading branch information...
1 parent e98b099 commit 2845459a187dac525e6b32b994c8bae8764d87c2 @timmc committed Jan 7, 2017
@@ -0,0 +1,28 @@
+(ns org.timmc.pellucida.res.acme
+ "Pass through ACME requests to filesystem. (ACME: Automated
+Certificate Management Environment)"
+ (:require
+ [compojure.core :refer [defroutes GET]]
+ [org.timmc.pellucida.settings :as cnf]))
+
+(defn is-token-shaped?
+ "Does this string look like an ACME http-01 token? In particular,
+does it *not* look like a path traversal attack?"
+ [^String s]
+ (boolean (re-matches #"[a-zA-Z0-9\-_=]+" s)))
+
+(defn token-response
+ "Answer with the contents of the ACME challenge file."
+ [magic-token]
+ ;; The ACME http-01 token value is used as the filename as well
+ ;; as the value of a field within the JSON file.
+ (when (is-token-shaped? magic-token)
+ (let [resp-path (str (:acme-challenge-dir @cnf/config) "/" magic-token)]
+ {:status 200
+ :headers {"Content-Type" "text/plain"} ;; Spec: This or none
+ :body (java.io.File. resp-path)})))
+
+(defroutes acme-routes
+ (GET ["/.well-known/acme-challenge/:magic-token"] [magic-token :as r]
+ (when (:acme-challenge-dir @cnf/config)
+ (token-response magic-token))))
@@ -12,7 +12,8 @@
(tags :refer [tags-routes])
(proxy-images :refer [proxy-image-routes])
(legacy-v1 :refer [legacy-v1-routes])
- (stats :refer [stats-routes]))
+ (stats :refer [stats-routes])
+ (acme :refer [acme-routes]))
[ring.adapter.jetty :as jetty]))
(def reloadable-src-dirs
@@ -41,7 +42,9 @@
#'single-routes
#'tags-routes
#'legacy-v1-routes
- #'stats-routes)
+ #'stats-routes
+ #'acme-routes
+ )
(def app "Server entrance point."
(-> (handler/site all-routes)
@@ -37,7 +37,13 @@ To use the filesystem proxy, use /proxy-image/."
:btc-donate-addr
{:doc "Bitcoin donation address"
- :validate string?}})
+ :validate string?}
+
+ :acme-challenge-dir
+ {:doc "ACME challenge directory for automated cert management. Absolute path, since contents are assumed trusted."
+ :validate #(and (string? %)
+ (.startsWith % "/"))}
+ })
(def ^:internal known-keys
(set/union (set (keys keys-required)) (set (keys keys-optional))))
@@ -0,0 +1,32 @@
+(ns org.timmc.pellucida.res.acme-test
+ (:use clojure.test)
+ (:require [org.timmc.pellucida.res.acme :as a]
+ [org.timmc.pellucida.settings :as cnf]))
+
+(deftest is-token-shaped?
+ (testing "Example from spec"
+ (is (= (a/is-token-shaped? "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA")
+ true)))
+ (testing "Base-64, URL variant"
+ (is (= (a/is-token-shaped? "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_=")
+ true)))
+ (testing "Empty"
+ (is (= (a/is-token-shaped? "") false)))
+ (testing "Anchored regex"
+ (is (= (a/is-token-shaped? "...lqiegqweigwejlie...") false)))
+ (testing "No slashes or dots or other tom-foolery"
+ (is (= (a/is-token-shaped? "magic-token") true))
+ (is (= (a/is-token-shaped? "magic.token") false))
+ (is (= (a/is-token-shaped? "magic/token") false))
+ (is (= (a/is-token-shaped? "magic\u0000token") false))))
+
+(deftest token-response
+ (testing "Checks token sanity itself"
+ (is (nil? (a/token-response "bogus/value"))))
+ (testing "Happy path"
+ (with-redefs [cnf/config (atom {:acme-challenge-dir "/base"})]
+ (let [resp (a/token-response "not-bogus")]
+ (is (= (:status resp) 200))
+ (is (= (:headers resp) {"Content-Type" "text/plain"}))
+ (is (= (.getPath ^java.io.File (:body resp))
+ "/base/not-bogus"))))))

0 comments on commit 2845459

Please sign in to comment.