Reputation: 550
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?
Upvotes: 16
Views: 4792
Reputation: 8219
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.
Upvotes: 2
Reputation: 1
I had a similar requirement using Play 2.2.3. I'm running my application behind a load balancer which is doing the SSL termination, and wanted all HTTP requests to be redirected to SSL. In my case, my load balancer (Amazon ELB) adds the X-Forwarded-Proto header, so I keyed off it as follows. In Global.java (or whatever class you've got that extends GlobalSettings), add this method:
@SuppressWarnings("rawtypes")
@Override
public Action onRequest(Request actionRequest, Method actionMethod) {
String forwardedProtocol = actionRequest.getHeader("X-Forwarded-Proto");
if (StringUtils.equalsIgnoreCase(forwardedProtocol, "http")) {
// Redirect to HTTPS
final String secureURL = "https://" + actionRequest.host() + actionRequest.uri();
return new Action.Simple() {
@Override
public Promise<play.mvc.SimpleResult> call(Context ctx) throws Throwable {
return Promise.pure(Results.movedPermanently(secureURL));
}
};
} else {
return super.onRequest(actionRequest, actionMethod);
}
}
This doesn't handle custom ports, so some might implementations might require a little extra coding.
Upvotes: 0
Reputation: 4593
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)
}
}
}
Upvotes: 12
Reputation: 541
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);
}
Upvotes: 2
Reputation: 11479
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.
Upvotes: 4