I have a Play Framework app running on Heroku, using Heroku's SSL endpoint.
I would like to make all pages available via SSL
only.
What's the best way to that?
So far, my best solution is to use onRouteRequest
in my GlobalSettings
and route non-SSL
requests to a special redirect handler:
override def onRouteRequest(request: RequestHeader): Option[Handler] = {
if (Play.isProd && !request.headers.get("x-forwarded-proto").getOrElse("").contains("https")) {
Some(controllers.Secure.redirect)
} else {
super.onRouteRequest(request)
}
}
and
package controllers
import play.api.mvc._
object Secure extends Controller {
def redirect = Action { implicit request =>
MovedPermanently("https://" + request.host + request.uri)
}
}
Is there a way to do this entirely from within GlobalSettings
? Or something even better?
Here is another way using filter. This also uses strict transport security to make sure that future requests go to https.
object HTTPSRedirectFilter extends Filter with Logging {
def apply(nextFilter: (RequestHeader) => Future[SimpleResult])(requestHeader: RequestHeader): Future[SimpleResult] = {
//play uses lower case headers.
requestHeader.headers.get("x-forwarded-proto") match {
case Some(header) => {
if ("https" == header) {
nextFilter(requestHeader).map { result =>
result.withHeaders(("Strict-Transport-Security", "max-age=31536000"))
}
} else {
Future.successful(Results.Redirect("https://" + requestHeader.host + requestHeader.uri, 301))
}
}
case None => nextFilter(requestHeader)
}
}
}
We have done that much like you but with a play filter that generates a MovedPermanently instead of a controller method.
I don't think there is a better way with heroku, or at least we couldn't find any feature to disable unencrypted HTTP.
Here is the solution for the Java version of Play Framework.
Add the following to the Global.java file:
@Override
public Handler onRouteRequest(RequestHeader request) {
String[] x = request.headers().get("X-Forwarded-Proto");
if (Play.isProd() && (x == null || x.length == 0 || x[0] == null || !x[0].contains("https")))
return controllers.Default.redirect("https://" + request.host() + request.uri());
return super.onRouteRequest(request);
}
In Play 2.5.x Java (I think should work in play 2.4.x as well but using Promise.F
instead of CompletionStage
), I added a filter:
public class TLSFilter extends Filter {
@Inject
public TLSFilter(Materializer mat) {
super(mat);
}
@Override
public CompletionStage<Result> apply(Function<Http.RequestHeader, CompletionStage<Result>> next, Http.RequestHeader rh) {
if (Play.current().isProd()) {
String[] httpsHeader = rh.headers().getOrDefault(Http.HeaderNames.X_FORWARDED_PROTO, new String[]{"http"});
if (Strings.isNullOrEmpty(httpsHeader[0]) || httpsHeader[0].equalsIgnoreCase("http")) {
return CompletableFuture.completedFuture(Results.movedPermanently("https://".concat(rh.host().concat(rh.uri()))));
}
}
return next.apply(rh).toCompletableFuture();
}
}
And then add it to the list of filters to use it:
public class AppFilters extends DefaultHttpFilters {
@Inject
public AppFilters(TLSFilter tlsFilter, GzipFilter gzipFilter) {
super(tlsFilter, gzipFilter);
}
}
And then to use your filters add the following inside application.conf
:
play.http.filters = "filters.AppFilters"
And please note, if you have a request handler enabled (look for play.http.requestHandler
inside the application.conf
file), filters will not work, I suggest to handle requests using filters and remove your current requestHandler.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With