Reputation: 147
I'm developing a Clojure web API using Ring and Compojure. The API needs to be able to accept HTTP and HTTPS requests based on the specified route.
For example:
Consider the following app-routes:
(defroutes app-routes
(POST "/route-one" {request :request} (processRequet request))
(POST "/route-two" {request :request} (processRequet request)))
I want route-one to accept only HTTP requests and route-two to accept only HTTPS requests.
Is this possible?
I tried running jetty with the following settings:
(jetty/run-jetty #'app {:join? false :ssl? true :ssl-port 8443 :keystore "./resources/keystore.jks" :key-password "12345678"})
That enabled the API to accept HTTPS requests, but it didn't block HTTP requests to the same routes.
I also tried disabling the HTTP protocol with no luck:
(jetty/run-jetty #'app {:port 5000 :join? false :ssl? true :ssl-port 8443 :keystore "./resources/keystore.jks" :key-password "12345678" :http? false})
From what I read online, the standard procedure for Ring HTTPS requests is using nginx as reverse proxy to manage all HTTPS requests.
But I haven't found any implementation online.
Any ideas?
Upvotes: 3
Views: 604
Reputation: 11832
One simple way to achieve this is to check the scheme of the incoming request inside your route, for example:
(defroutes app-routes
(POST "/route-one" request
(if (= (:scheme req) :http)
(process-requet request)
{:status 403 :body "https not supported"}))
(POST "/route-two" request
(if (= (:scheme req) :https)
(process-requet request)
{:status 403 :body "http not supported"})))
You can of course extract this scheme check into a separate function or macro, so even if you have many routes then this could be a viable option without too extra noise in your code.
If you have many routes, and you don't want to add an extra call or check to each, then another way to achieve this is to add some middleware to your application. Middleware can check the :scheme
included in the request and reject requests that don't conform. For example:
(defn wrap-https-only [handler]
(fn [req]
(if (= (:scheme req) :https)
(handler req)
{:status 403 :body "http not supported"})))
(defn wrap-http-only [handler]
(fn [req]
(if (= (:scheme req) :http)
(handler req)
{:status 403 :body "https not supported"})))
The tricky part is that you want to selectively apply this middleware to some routes and not others, and Compojure does not offer a simple way to do this. If you have some common path in all your http routes, and all your https routes (to group them) then you can use a pattern like this:
(def http-routes
(-> (routes (POST "/http-only/route-one" request
(process-requet request)))
wrap-http-only))
(def https-routes
(-> (routes (POST "/http-only/route-two" request
(process-requet request)))
wrap-https-only))
(defroutes app-routes
(ANY "/http-only/*" [] http-routes)
(ANY "/https-only/*" [] https-routes))
You can see here that the middleware is applied to each subset of routes, and so it will only be executed if the first part of the path matches. Now you can add as many routes as you like, and the middleware will take care of the scheme check for all of them. Note, this approach requires that you can identify the subsets of routes in some way in the initial 'app-routes' routes, in this case I've identified them by the path.
One thing to note when checking the scheme: be warned that this wont work if you decide to offload SSL to a load balancer. In that case, your application will always receive HTTP (not HTTPS) requests. You can usually achieve the same by simply checking the X-Forwarded-Proto
header instead of the :scheme
value.
Upvotes: 3