All Downloads are FREE. Search and download functionalities are using the official Maven repository.

no.kodeworks.kvarg.actor.HttpService.scala Maven / Gradle / Ivy

There is a newer version: 0.7
Show newest version
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