no.kodeworks.kvarg.actor.HttpService.scala Maven / Gradle / Ivy
package no.kodeworks.kvarg.actor
import no.kodeworks.kvarg.actor._
import no.kodeworks.kvarg.actor.AuthService._
import no.kodeworks.kvarg.actor.SessionService.ensureSession
import no.kodeworks.kvarg.util.PageDirectives._
import java.io.File
import akka.actor.{Actor, ActorLogging, ActorRef}
import akka.event.Logging
import akka.http.javadsl.server.AuthorizationFailedRejection
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.HttpMethods._
import akka.http.scaladsl.model.Uri.Path
import akka.http.scaladsl.model._
import akka.http.scaladsl.model.headers._
import akka.http.scaladsl.server._
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.RouteResult.{Complete, Rejected}
import ch.megard.akka.http.cors.scaladsl.CorsDirectives._
import ch.megard.akka.http.cors.scaladsl.settings.CorsSettings
import akka.pattern.pipe
import akka.stream.ActorMaterializer
import akka.util.Timeout
import no.kodeworks.kvarg.actor.HttpService._
import no.kodeworks.kvarg.message._
import no.kodeworks.kvarg.model.Page
import com.softwaremill.session.{SessionConfig, SessionManager}
import no.kodeworks.kvarg.util.{DomainsRouter, RichFuture}
import com.typesafe.config.Config
import shapeless.HList
import scala.concurrent.duration._
import scala.util.{Failure, Success}
import scala.language.postfixOps
import collection.immutable
import cats.syntax.option._
class HttpService[Domains <: HList]
(domainsRouter: DomainsRouter[Domains]
, bootService: ActorRef
, sessionService: ActorRef
, cometService: ActorRef
, authService: ActorRef
, sessionConfig: SessionConfig
, sessionManager: SessionManager[String]
, pageManager: SessionManager[String]
, httpInterface: String = "0.0.0.0"
, httpPort: Int = 8080
, webPath: Option[String] = None
, webContent: Option[File] = None
, spnegoConfig: Option[Config] = None
, restTimeout: Timeout = Timeout(2 seconds)
, cometTimeout: Timeout
, innerRoute: Route = reject
)
extends Actor with ActorLogging {
implicit val ac = context.system
implicit val materializer = ActorMaterializer()
implicit val to = restTimeout
implicit val ec = context.dispatcher
override def preStart() {
log.info("born")
context.become(initing)
bind
}
override def postStop() {
log.info("died")
}
def bind() {
Http().bindAndHandle(route, httpInterface, httpPort).mapAll(t => t).pipeTo(self)
}
val initing: Receive = {
case Success(ok) =>
log.info("Bound to {}:{}", httpInterface, httpPort)
bootService ! InitSuccess
context.unbecome
case Failure(no) =>
log.error("Could not bind to {}:{} because of: {}", httpInterface, httpPort, no.getMessage)
bootService ! InitFailure
case x =>
log.error("Initing - unknown message " + x)
bootService ! InitFailure
}
def route() = {
implicit def log0 = log
handleErrors {
logRequest("REQ") {
logResult("RES") {
cors(corsSettings) {
ensureSession(sessionManager, sessionService, ec, ac, to) { session =>
log.info("SESSION: " + session)
purgeSlashes {
webPathDir(webPath) {
path("rest" / "restClient") {
get {
complete("{}")
} ~ (post & pathEndOrSingleSlash) {
ensurePage(session)(pageManager) { page =>
complete(s"""{"id":"$page"}""")
}
}
} ~
pathPrefixTest("rest" | "poll") {
touchRequiredPage(session)(pageManager) { page =>
auth(session, authService, spnegoConfig, to, log) { auth0 =>
//TODO reset xsrf token
pathPrefix("rest") {
domainsRouter.route(this, materializer, to, page.some) ~
path("props") {
import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._
import io.circe.generic.auto._
complete(AuthService.Props(auth0))
} ~
innerRoute
} ~
CometService.route(cometService, domainsRouter.patchEncoders, page, cometTimeout, ec, ac, log)
}
}
}
}
}
}
} ~ webContentRoute(webPath, webContent)
}
}
}
}
override def receive() = {
case x =>
log.error("Unknown " + x)
}
}
object HttpService {
def mapPath(path: Path) = path match {
case p if !p.isEmpty =>
Path {
val path = p.toString
val firstColon = path.indexOf(";")
val firstQmark = path.indexOf("?")
val (pathColon, reqParams) =
if (-1 == firstQmark) (path, "")
else path.splitAt(firstQmark)
val (plainPath, pathParams) =
if (-1 == firstColon) (pathColon, "")
else path.splitAt(firstColon)
val pathSingleSlashes = plainPath
.replaceAll("/+", "/")
val pathNoLastSlash =
if (pathSingleSlashes.last == '/') pathSingleSlashes.substring(0, pathSingleSlashes.size - 1)
else pathSingleSlashes
val x = pathNoLastSlash + pathParams + reqParams
x
}
case p => p
}
val purgeSlashes: Directive0 =
mapRequestContext(_.mapRequest(r =>
r.copy(uri = r.uri.copy(path = mapPath(r.uri.path))))
.mapUnmatchedPath(mapPath _))
def webPathDir(webPath: Option[String]): Directive0 =
webPath.map(pathPrefix(_))
.getOrElse(failWith(new RuntimeException("No web path")))
def webContentRoute(webPath: Option[String], webContent: Option[File]): Route =
webPathDir(webPath) {
webContent.map(wc => getFromDirectory(wc.getAbsolutePath))
.getOrElse(failWith(new RuntimeException("No web content, or unreadable/corrupt part of web content")))
} ~
(path("favicon.ico") & extractUri) { uri =>
webPath.map(wp =>
redirect(uri.copy(path = Uri.Path("/" + wp + uri.path.toString)), StatusCodes.TemporaryRedirect))
.getOrElse(reject)
}
val corsSettings = CorsSettings.defaultSettings.copy(allowedMethods = List(GET, POST, PATCH, PUT, DELETE, OPTIONS))
val authFailHandler = mapRejections { rejections =>
val maybeAuthFails = rejections.filter {
case _: AuthenticationFailedRejection => true
case _ => false
}
if (maybeAuthFails.isEmpty) rejections
else maybeAuthFails
}
val rejectionHandler = corsRejectionHandler withFallback RejectionHandler.default
val exceptionHandler = ExceptionHandler {
case e: NoSuchElementException => complete(StatusCodes.NotFound -> e.getMessage)
}
val handleErrors =
handleRejections(rejectionHandler) &
authFailHandler &
handleExceptions(exceptionHandler)
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy