4

I have written a Vertx-web handler in Koltin that redirects any request I receive that is HTTP to HTTPS, and I'm using context.request().isSSL to determine if the request is not SSL, and this worked fine until I put my code behind a load balancer. If the load balancer talks to my Vertx-web server on HTTPS then it thinks all user requests are HTTPS even if they are not. And if I change the load balancer to talk to Vertx-web on HTTP then every request is redirected endlessly even if already the user is using HTTPS.

Then I also see another problem, that the redirect using context.request().absoluteURI() goes to the private address instead of the publically available address that the user is actually talking to.

Is there a handler in Vertx-web that I'm missing that does this, or some idiomatic way to solve this? Should I just do this from JavaScript since it sees the real user address instead of trying a server-side redirect?

I'm coding in Kotlin, so any examples for that language are great!

Note: this question is intentionally written and answered by the author (Self-Answered Questions), so that solutions for interesting problems are shared in SO.

Community
  • 1
  • 1
Jayson Minard
  • 84,842
  • 38
  • 184
  • 227

1 Answers1

3

First, it is best if your proxy or load balancer can do this check and redirect for you since it has knowledge of the public URL and is a simpler process at that first contact with the user. But, you can also do it server-side with a little more complexity.

The flag you are checking, context.request().isSSL is only valid for the incoming connection to Vertx-web and does not consider the end-user's connect to your proxy or load balancer. You need to use the X-Forwarded-Proto header (and sometimes X-Forwarded-Scheme) and check the actual protocol of the user. And only if that header is not present you can use context.request().isSSL

You also need to externalize your own URL to be able to redirect on the server side to something that the browser can use to find you, your public URL.

First, there is a Kotlin function in this Stack Overflow answer for RoutingContext.externalizeUrl(), you will need it here:
I have a Vertx request and I need to calculate an externally visible (public) URL

Then knowing your public URL you can use the following handler which has default values for the intended public HTTPS port (default 443 will vanish from URL), which form of redirect (i.e. 302), and on any exceptions if the route should be failed or continued:

fun Route.redirectToHttpsHandler(publicHttpsPort: Int = 443, redirectCode: Int = 302, failOnUrlBuilding: Boolean = true) {
    handler { context ->
        val proto = context.request().getHeader("X-Forwarded-Proto")
                ?: context.request().getHeader("X-Forwarded-Scheme")
        if (proto == "https") {
            context.next()
        } else if (proto.isNullOrBlank() && context.request().isSSL) {
            context.next()
        } else {
            try {
                val myPublicUri = URI(context.externalizeUrl())
                val myHttpsPublicUri = URI("https", 
                        myPublicUri.userInfo, 
                        myPublicUri.host, 
                        publicHttpsPort,
                        myPublicUri.rawPath, 
                        myPublicUri.rawQuery, 
                        myPublicUri.rawFragment)
                context.response().putHeader("location", myHttpsPublicUri.toString()).setStatusCode(redirectCode).end()
            } catch (ex: Throwable) {
                if (failOnUrlBuilding) context.fail(ex)
                else context.next()
            }
        }
    }
}

A simpler version might be to just trust the context.externalizeUrl class and see if it has the correct protocol and port and redirect if not:

fun Route.simplifiedRedirectToHttpsHandler(publicHttpsPort: Int = 443, redirectCode: Int = 302, failOnUrlBuilding: Boolean = true) {
    handler { context ->
        try {
            val myPublicUri = URI(context.externalizeUrl())
            if (myPublicUri.scheme == "http") {
                val myHttpsPublicUri = URI("https",
                        myPublicUri.userInfo,
                        myPublicUri.host,
                        publicHttpsPort,
                        myPublicUri.rawPath,
                        myPublicUri.rawQuery,
                        myPublicUri.rawFragment)
                context.response().putHeader("location", myHttpsPublicUri.toString()).setStatusCode(redirectCode).end()
            }
            else {
                context.next()
            }
        } catch (ex: Throwable) {
            if (failOnUrlBuilding) context.fail(ex)
            else context.next()
        }
    }
}
Community
  • 1
  • 1
Jayson Minard
  • 84,842
  • 38
  • 184
  • 227
  • Newbie question here, how will I register (or use) simplifiedRedirectorToHttpsHandler? For instance, my typical set up is: `private fun createRouter() = Router.router(vertx).apply { route().handler(BodyHandler.create()) ...` *Updated*: Never mind, I got it :) `Router.router(vertx).apply { route().redirectToHttpsHandler() route().handler(BodyHandler.create())` – Usman Saleem Oct 06 '17 at 01:57