Skip to content

Instantly share code, notes, and snippets.

@agemooij
Last active April 15, 2020 23:21
Show Gist options
  • Save agemooij/0e947780f73d55bc1baa to your computer and use it in GitHub Desktop.
Save agemooij/0e947780f73d55bc1baa to your computer and use it in GitHub Desktop.
Spray HttpDirectives
package scalapenos.spray.auth
import spray.routing._
import spray.routing.Directives._
import spray.http.HttpHeaders._
import spray.http.StatusCodes._
trait HttpsDirectives {
import HttpsDirectives._
def enforceHttpsIf(yes: => Boolean): Directive0 = {
if (yes) enforceHttps
else pass
}
def enforceHttps: Directive0 = {
respondWithHeader(StrictTransportSecurity) &
extract(isHttpsRequest).flatMap(
if (_) pass
else redirectToHttps
)
}
def redirectToHttps: Directive0 = {
requestUri.flatMap { uri =>
redirect(uri.copy(scheme = "https"), MovedPermanently)
}
}
}
object HttpsDirectives {
/** Hardcoded max-age of one year (31536000 seconds) for now. */
val StrictTransportSecurity = RawHeader("Strict-Transport-Security", "max-age=31536000")
val isHttpsRequest: RequestContext => Boolean = { ctx =>
ctx.request.uri.scheme == "https" || ctx.request.headers.exists(h => h.is("x-forwarded-proto") && h.value == "https")
}
}
package scalapenos.spray.auth
import org.specs2.mutable.Specification
import spray.http._
import spray.http.HttpHeaders._
import spray.http.StatusCodes._
import spray.routing._
import spray.routing.Directives._
import spray.testkit.Specs2RouteTest
import HttpsDirectives._
class HttpsDirectivesSpec extends Specification
with Specs2RouteTest
with HttpsDirectives {
val httpUri = Uri("http://example.com/api/awesome")
val httpsUri = Uri("https://example.com/api/awesome")
"The enforceHttps directive" should {
val route = enforceHttps {
complete(OK)
}
"allow https requests and respond with the HSTS header" in {
Get(httpsUri) ~> route ~> check {
status === OK
header(StrictTransportSecurity.name) must beSome(StrictTransportSecurity)
}
}
"allow terminated https requests containing a 'X-Forwarded-Proto' header and respond with the HSTS header" in {
Get(httpUri) ~> addHeader(RawHeader("X-Forwarded-Proto", "https")) ~> route ~> check {
status === OK
header(StrictTransportSecurity.name) must beSome(StrictTransportSecurity)
}
}
"redirect plain http requests to the matching https URI" in {
Get(httpUri) ~> route ~> check {
status === MovedPermanently
header[Location].map(l => Uri(l.value)) must beSome(httpsUri)
header(StrictTransportSecurity.name) must beSome(StrictTransportSecurity)
}
}
"redirect terminated http requests to the matching https URI" in {
Get(httpUri) ~> addHeader(RawHeader("X-Forwarded-Proto", "http")) ~> route ~> check {
status === MovedPermanently
header[Location].map(l => Uri(l.value)) must beSome(httpsUri)
header(StrictTransportSecurity.name) must beSome(StrictTransportSecurity)
}
}
}
"The enforceHttpsIf directive" should {
"enforce https when the argument resolves to true" in {
val route = enforceHttpsIf(true) {
complete(OK)
}
Get(httpsUri) ~> route ~> check {
status === OK
header(StrictTransportSecurity.name) must beSome(StrictTransportSecurity)
}
Get(httpUri) ~> route ~> check {
status === MovedPermanently
header[Location].map(l => Uri(l.value)) must beSome(httpsUri)
header(StrictTransportSecurity.name) must beSome(StrictTransportSecurity)
}
}
"not enforce https when the argument resolves to false" in {
val route = enforceHttpsIf(false) {
complete(OK)
}
Get(httpsUri) ~> route ~> check {
status === OK
header(StrictTransportSecurity.name) must beNone
}
Get(httpUri) ~> route ~> check {
status === OK
header(StrictTransportSecurity.name) must beNone
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment