5

I've just tried add this wrapper (-> routes (wrap-ssl-redirect)) for auto redirecting http to https, but when I deploy to heroku, the https:// doesn't get green in my browser, and the website doesn't load.

Isn't the default heroku port 443, which should also be the default from wrap-ssl-redirectfunction?

What is wrong?

Thank you!

EDIT:

My code:

(defn prod-app [routes]
  (-> routes
      (wrap-keyword-params)
      (wrap-params)
      (wrap-ssl-redirect)))

(defn -main []
  (let [port (Integer/parseInt (get (System/getenv) "PORT" "5000"))]
    (jetty/run-jetty (prod-app domain-specific-routes)
                     {:port port :join? false})))

EDIT 2:

I just found this thread which could solve my problem: Clojure / Noir: Force HTTPS, redirect if the request was http:// to https://

Came up with this require-https handler fn:

(defn https-url [request-url]
  (str "https://" 
       (:server-name request-url) 
       ":" 
       (:server-port request-url) 
       (:uri request-url)))

(defn require-https
  [handler]
  (fn [request]
    (if (and (= (:scheme request) :http) 
             (= (get-in request [:headers "host"]) "secure.mydomain.com"))
      (ring.util.response/redirect (https-url request))
      (handler request))))

But when I try to connect to http://secure.mydomain.com, I am seing a port in the browser address bar https://secure.mydomain.com:80/ and got this message ERR_SSL_PROTOCOL_ERROR.

leontalbot
  • 2,513
  • 1
  • 23
  • 32
  • You need to bind to a port environment variable normally on Heroku. Do your logs show any messages? – Daniel Compton Jun 07 '15 at 04:32
  • I haven't seen any problem in my logs. Now if I try `(-> routes (wrap-ssl-redirect {:ssl-port (Integer/parseInt (get (System/getenv) "PORT" "5000"))}`, I get yellow flags in my logs with `status=301` and the browser displays a 5 digits port next to my domain name. in my heroku config, I don't have a `PORT` var. – leontalbot Jun 07 '15 at 10:56
  • Can you update your question with the logs in question, and the exact reproducing steps you're following, along with what has worked in the past for you on Heroku? – Daniel Compton Jun 07 '15 at 19:12

2 Answers2

4

On Heroku, you need to bind to the port they give you through an environment variable. Heroku also sit load balancers in front of your app. They will terminate the SSL connection and communicate with your app over HTTP, so the scheme that your app will see will always be HTTP. The standard wrap-ssl-redirect won't work well with this scenario, I don't think it will ever successfully redirect (it would think the user is connecting over HTTP even if they did get redirected to HTTPS).

However Heroku adds x-forwarded-proto to the headers of every request to work around this, so you can see which protocol your client is using. In the same ring.middleware.ssl namespace is a wrap-forwarded-scheme middleware.

"Middleware that changes the :scheme of the request map to the value present in a request header. This is useful if your application sits behind a reverse proxy or load balancer that handles the SSL transport. The header defaults to x-forwarded-proto."

If you wrap your routes with this first, then with wrap-ssl-redirect, that it should redirect to HTTPS correctly. You can also add ring.middleware.ssl/wrap-hsts to enforce HSTS as well:

(wrap-forwarded-scheme (wrap-ssl-redirect (wrap-hsts app)))
Daniel Compton
  • 13,878
  • 4
  • 40
  • 60
  • 1
    hmm.. adding `wrap-forwarded-scheme` makes chrome say : `ERR_TOO_MANY_REDIRECTS`. Heroku logs looks like this: ` at=info method=GET path="/favicon.ico" host=www.mydomain.com request_id=0e85ea4c-044c-4bf4-a16b-fe729aabad3b fwd="77.74.193.277" dyno=web.1 connect=0ms service=72ms status=301 bytes=170` – leontalbot Jun 08 '15 at 01:56
  • 1
    Perhaps considering opening an issue on the GitHub project asking for documentation showing how to use it with Heroku, they're probably the best people to ask. – Daniel Compton Jun 08 '15 at 02:06
  • Didn't work. I opened a ticket but sadly Heroku could find the answer. It happens... – leontalbot Jun 09 '15 at 03:30
  • I think getting the order if the wrapping right is important. When the request is passed through wrap-forwarded-scheme first it works fine. – Vinai Aug 06 '17 at 08:06
0

Long live lib-noir and its wrap-force-ssl fn.

I just needed to change it a bit to fit my needs (I have a SSL cert. for one subdomain even if my clojure app handles more then one domain)

Here's my fn:

(defn wrap-force-ssl
  "Almost like in lib-noir. 
   If the request's scheme is not https [and is for 'secure.'], redirect with https.
   Also checks the X-Forwarded-Proto header."
  [app]
  (fn [req]
    (let [headers (:headers req)
          host    (headers "host")]
      (if (or (= :https (:scheme req))
              (= "https" (headers "x-forwarded-proto"))
              (not= "secure.mydomain.com" host)) ;you might not need this!
        (app req)
        (noir.response/redirect (str "https://" host (:uri req)) :permanent)))))

Thanks to Chris Granger and friends!

leontalbot
  • 2,513
  • 1
  • 23
  • 32
  • 1
    Thanks! That's really useful. One thought, might want to consider checking for `:query-string` in the request and adding that to the redirect location too. – adamneilson Aug 05 '16 at 16:46
  • @adamneilson Can you provide an example or suggest an edit? – leontalbot Dec 24 '16 at 11:32
  • 1
    I was thinking something along the lines of: `(noir.response/redirect (if (nil? (:query-string req)) (format "https://%s%s" host (:uri req)) (format "https://%s%s?%s" host (:uri req) (:query-string req))) :permanent)` – adamneilson Jan 06 '17 at 13:32