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

env.Env.scala Maven / Gradle / Ivy

The newest version!
package otoroshi.env

import akka.actor.{ActorSystem, Cancellable, PoisonPill, Scheduler}
import akka.http.scaladsl.util.FastFuture._
import akka.stream.Materializer
import ch.qos.logback.classic.{Level, LoggerContext}
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import com.typesafe.config.{ConfigFactory, ConfigRenderOptions}
import io.netty.util.internal.PlatformDependent
import io.otoroshi.wasm4s.scaladsl.WasmIntegration
import otoroshi.metrics.{HasMetrics, Metrics}
import org.joda.time.DateTime
import org.mindrot.jbcrypt.BCrypt
import org.slf4j.LoggerFactory
import otoroshi.auth.{AuthModuleConfig, SessionCookieValues}
import otoroshi.cluster._
import otoroshi.events._
import otoroshi.gateway.{AnalyticsQueue, CircuitBreakersHolder}
import otoroshi.health.HealthCheckerActor
import otoroshi.jobs.updates.Version
import otoroshi.models._
import otoroshi.next.extensions.{AdminExtensionConfig, AdminExtensionId, AdminExtensions}
import otoroshi.next.models.NgRoute
import otoroshi.next.proxy.NgProxyState
import otoroshi.next.tunnel.{TunnelAgent, TunnelManager}
import otoroshi.next.utils.Vaults
import otoroshi.openapi.ClassGraphScanner
import otoroshi.script.plugins.Plugins
import otoroshi.script.{AccessValidatorRef, JobManager, ScriptCompiler, ScriptManager}
import otoroshi.security.{ClaimCrypto, IdGenerator}
import otoroshi.ssl.pki.BouncyCastlePki
import otoroshi.ssl.{Cert, DynamicSSLEngineProvider, OcspResponder}
import otoroshi.storage.{DataStores, DataStoresBuilder}
import otoroshi.storage.drivers.cassandra._
import otoroshi.storage.drivers.inmemory._
import otoroshi.storage.drivers.lettuce._
import otoroshi.storage.drivers.reactivepg.ReactivePgDataStores
import otoroshi.storage.drivers.rediscala._
import otoroshi.tcp.TcpService
import otoroshi.utils.{JsonPathValidator, JsonValidator}
import otoroshi.utils.http.{AkkWsClient, WsClientChooser}
import otoroshi.utils.syntax.implicits._
import otoroshi.wasm.OtoroshiWasmIntegrationContext
import play.api._
import play.api.http.{HttpConfiguration, HttpRequestHandler}
import play.api.inject.ApplicationLifecycle
import play.api.libs.json.{JsObject, JsSuccess, JsValue, Json}
import play.api.libs.ws._
import play.api.libs.ws.ahc._
import play.shaded.ahc.org.asynchttpclient.DefaultAsyncHttpClient
import play.twirl.api.Html

import java.io.File
import java.lang.management.ManagementFactory
import java.nio.file.Files
import java.rmi.registry.LocateRegistry
import java.util.concurrent.{Executors, TimeUnit}
import java.util.concurrent.atomic.AtomicReference
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
import javax.management.remote.{JMXConnectorServerFactory, JMXServiceURL}
import scala.concurrent.duration._
import scala.concurrent.{Await, ExecutionContext, Future, Promise}
import scala.io.Source
import scala.util.{Failure, Success}

case class RoutingInfo(id: String, name: String)

object JavaVersion                                         {
  val default                                       = JavaVersion(PlatformDependent.javaVersion().toString, "--")
  def fromString(value: String): JavaVersion        = fromJson(Json.parse(value).asOpt[JsValue])
  def fromJson(value: Option[JsValue]): JavaVersion = value match {
    case None       => JavaVersion.default
    case Some(json) =>
      (for {
        version <- json.select("version").asOpt[String]
        vendor  <- json.select("vendor").asOpt[String]
      } yield JavaVersion(version, vendor)).getOrElse(JavaVersion.default)
  }
}
case class JavaVersion(version: String, vendor: String)    {
  def str: String     = s"${version} $vendor"
  def jsonStr: String = json.stringify
  def json: JsValue   = Json.obj(
    "version" -> version,
    "vendor"  -> vendor
  )
}
object OS                                                  {
  val default                              = OS("undefined", "undefined", "undefined")
  def fromString(value: String): OS        = fromJson(Json.parse(value).asOpt[JsValue])
  def fromJson(value: Option[JsValue]): OS = value match {
    case None       => OS.default
    case Some(json) =>
      (for {
        name    <- json.select("name").asOpt[String]
        version <- json.select("version").asOpt[String]
        arch    <- json.select("arch").asOpt[String]
      } yield OS(name, version, arch)).getOrElse(OS.default)
  }
}
case class OS(name: String, version: String, arch: String) {
  def str: String     = s"${name} ${version} ($arch)"
  def jsonStr: String = json.stringify
  def json: JsValue   = Json.obj(
    "name"    -> name,
    "version" -> version,
    "arch"    -> arch
  )
}

case class SidecarConfig(
    serviceId: String,
    target: Target,
    from: String = "127.0.0.1",
    apiKeyClientId: Option[String] = None,
    strict: Boolean = true
)

case class ElSettings(allowEnvAccess: Boolean, allowConfigAccess: Boolean)

class Env(
    val _configuration: Configuration,
    val environment: Environment,
    val lifecycle: ApplicationLifecycle,
    val httpConfiguration: HttpConfiguration,
    wsClient: WSClient,
    val circuitBeakersHolder: CircuitBreakersHolder,
    getHttpPort: => Option[Int],
    getHttpsPort: => Option[Int],
    testing: Boolean
) extends HasMetrics {

  val logger = Logger("otoroshi-env")

  val handlerRef = new AtomicReference[HttpRequestHandler]()

  val otoroshiActorSystem: ActorSystem           = ActorSystem(
    "otoroshi-actor-system",
    _configuration
      .getOptional[Configuration]("app.actorsystems.otoroshi")
      .orElse(_configuration.getOptional[Configuration]("otoroshi.analytics.actorsystem"))
      .map(_.underlying)
      .getOrElse(ConfigFactory.empty)
  )
  val otoroshiExecutionContext: ExecutionContext = otoroshiActorSystem.dispatcher
  val otoroshiScheduler: Scheduler               = otoroshiActorSystem.scheduler
  val otoroshiMaterializer: Materializer         = Materializer(otoroshiActorSystem)
  val vaults                                     = new Vaults(this)

  private val (merged_configuration: Configuration, merged_configuration_json: JsObject) = (for {
    appConfig <- _configuration.getOptional[Configuration]("app")
    otoConfig <- _configuration.getOptional[Configuration]("otoroshi")
  } yield {
    val wholeConfigJson: JsObject     =
      Json
        .parse(_configuration.underlying.root().render(ConfigRenderOptions.concise()))
        .as[JsObject]
        .-("app")
        .-("otoroshi")
    val appConfigJson: JsObject       =
      Json.parse(appConfig.underlying.root().render(ConfigRenderOptions.concise())).as[JsObject]
    val otoConfigJson: JsObject       =
      Json.parse(otoConfig.underlying.root().render(ConfigRenderOptions.concise())).as[JsObject]
    // val appKeys = appConfigJson.value.keySet
    // val otoKeys = otoConfigJson.value.keySet
    // appKeys.filter(key => otoKeys.contains(key)).debugPrintln
    val mergeConfig: JsObject         = appConfigJson.deepMerge(otoConfigJson)
    val _finalConfigJson1: JsObject   =
      wholeConfigJson.deepMerge(Json.obj("otoroshi" -> mergeConfig, "app" -> mergeConfig))
    val _finalConfigJson1Str          = _finalConfigJson1.stringify
    val _finalConfigJson1StrWithVault = Await.result(
      vaults.fillSecretsAsync("otoroshi-config", _finalConfigJson1Str)(
        ExecutionContext.fromExecutor(Executors.newFixedThreadPool(3))
      ),
      30.seconds
    )
    val finalConfigJson1: JsObject    = Json.parse(_finalConfigJson1StrWithVault).asObject
    (Configuration(ConfigFactory.parseString(Json.stringify(finalConfigJson1))), finalConfigJson1)
  }) getOrElse (_configuration, Json
    .parse(_configuration.underlying.root().render(ConfigRenderOptions.concise()))
    .asObject)

  val configuration     = merged_configuration // _configuration
  val configurationJson = merged_configuration_json

  private lazy val xmasStart =
    DateTime.now().withMonthOfYear(12).withDayOfMonth(20).withMillisOfDay(0)
  private lazy val xmasStop  =
    DateTime.now().withMonthOfYear(12).dayOfMonth().withMaximumValue().plusDays(1).withMillisOfDay(1)

  private lazy val halloweenStart =
    DateTime.now().withMonthOfYear(10).withDayOfMonth(31).withMillisOfDay(0)
  private lazy val halloweenStop  =
    DateTime.now().withMonthOfYear(10).withDayOfMonth(31).plusDays(1).withMillisOfDay(1)

  lazy val maxHeaderSizeToBackend   =
    configuration.getOptionalWithFileSupport[Long]("otoroshi.options.maxHeaderSizeToBackend")
  lazy val maxHeaderSizeToClient    =
    configuration.getOptionalWithFileSupport[Long]("otoroshi.options.maxHeaderSizeToClient")
  lazy val limitHeaderSizeToBackend =
    configuration.getOptionalWithFileSupport[Long]("otoroshi.options.limitHeaderSizeToBackend")
  lazy val limitHeaderSizeToClient  =
    configuration.getOptionalWithFileSupport[Long]("otoroshi.options.limitHeaderSizeToClient")

  lazy val jsonPathNullReadIsJsNull =
    configuration.getOptionalWithFileSupport[Boolean]("otoroshi.options.jsonPathNullReadIsJsNull").getOrElse(false)

  lazy val dynamicBodySizeCompute =
    configuration.getOptionalWithFileSupport[Boolean]("otoroshi.options.dynamicBodySizeCompute").getOrElse(true)

  private lazy val disableFunnyLogos: Boolean =
    configuration.getOptionalWithFileSupport[Boolean]("otoroshi.options.disableFunnyLogos").getOrElse(false)

  lazy val allowRedirectQueryParamOnLogin: Boolean =
    configuration
      .getOptionalWithFileSupport[Boolean]("otoroshi.options.allowRedirectQueryParamOnLogin")
      .getOrElse(false)

  lazy val customLogo: Option[String] = configuration.getOptionalWithFileSupport[String]("app.instance.logo")

  lazy val elSettings: ElSettings = ElSettings(
    allowEnvAccess =
      configuration.getOptionalWithFileSupport[Boolean]("otoroshi.elSettings.allowEnvAccess").getOrElse(true),
    allowConfigAccess =
      configuration.getOptionalWithFileSupport[Boolean]("otoroshi.elSettings.allowConfigAccess").getOrElse(true)
  )

  lazy val devMimetypes: Map[String, String] = configuration
    .betterGetOptional[String]("play.http.fileMimeTypes")
    .map { types =>
      types
        .split("\\n")
        .toSeq
        .map(_.trim)
        .filter(_.nonEmpty)
        .map(_.split("=").toSeq)
        .filter(_.size == 2)
        .map(v => (v.head, v.tail.head))
        .toMap
    }
    .getOrElse(Map.empty[String, String])

  def otoroshiLogo: String = {
    val now = DateTime.now()
    customLogo match {
      case Some(logo) => logo
      case None       => {
        if (disableFunnyLogos) {
          "/__otoroshi_assets/images/otoroshi-logo-color.png"
        } else if (now.isAfter(xmasStart) && now.isBefore(xmasStop)) {
          "/__otoroshi_assets/images/otoroshi-logo-xmas.png"
        } else if (now.isAfter(halloweenStart) && now.isBefore(halloweenStop)) {
          "/__otoroshi_assets/images/otoroshi-logo-halloween3.png"
        } else {
          "/__otoroshi_assets/images/otoroshi-logo-color.png"
        }
      }
    }
  }

  val analyticsPressureEnabled: Boolean =
    configuration.getOptionalWithFileSupport[Boolean]("otoroshi.analytics.pressure.enabled").getOrElse(false)

  val analyticsActorSystem: ActorSystem           =
    if (analyticsPressureEnabled)
      ActorSystem(
        "otoroshi-analytics-actor-system",
        configuration
          .getOptionalWithFileSupport[Configuration]("otoroshi.analytics.actorsystem")
          .map(_.underlying)
          .getOrElse(ConfigFactory.empty)
      )
    else otoroshiActorSystem
  val analyticsExecutionContext: ExecutionContext =
    if (analyticsPressureEnabled) analyticsActorSystem.dispatcher else otoroshiExecutionContext
  val analyticsScheduler: Scheduler               =
    if (analyticsPressureEnabled) analyticsActorSystem.scheduler else otoroshiScheduler
  val analyticsMaterializer: Materializer         =
    if (analyticsPressureEnabled) Materializer(analyticsActorSystem) else otoroshiMaterializer

  def timeout(duration: FiniteDuration): Future[Unit] = {
    val promise = Promise[Unit]
    otoroshiActorSystem.scheduler.scheduleOnce(duration) {
      promise.trySuccess(())
    }(otoroshiExecutionContext)
    promise.future
  }

  // val healthCheckerActor  = otoroshiActorSystem.actorOf(HealthCheckerActor.props(this))
  val otoroshiEventsActor = otoroshiActorSystem.actorOf(OtoroshiEventsActorSupervizer.props(this))
  val analyticsQueue      = otoroshiActorSystem.actorOf(AnalyticsQueue.props(this))

  lazy val sidecarConfig: Option[SidecarConfig] = (
    configuration.getOptionalWithFileSupport[String]("app.sidecar.serviceId"),
    configuration.getOptionalWithFileSupport[String]("app.sidecar.target"),
    configuration.getOptionalWithFileSupport[String]("app.sidecar.from"),
    configuration.getOptionalWithFileSupport[String]("app.sidecar.apikey.clientId"),
    configuration.getOptionalWithFileSupport[Boolean]("app.sidecar.strict")
  ) match {
    case (Some(serviceId), Some(target), from, clientId, strict) =>
      val conf = SidecarConfig(
        serviceId = serviceId,
        target = Target(target.split("://")(1), target.split("://")(0)),
        from = from.getOrElse("127.0.0.1"),
        apiKeyClientId = clientId,
        strict = strict.getOrElse(true)
      )
      Some(conf)
    case a                                                       => None
  }

  lazy val healtCheckWorkers: Int        =
    configuration.getOptionalWithFileSupport[Int]("otoroshi.healthcheck.workers").getOrElse(4)
  lazy val healtCheckBlockOnRed: Boolean =
    configuration.getOptionalWithFileSupport[Boolean]("otoroshi.healthcheck.block-on-red").getOrElse(false)
  lazy val healtCheckTTL: Long           =
    configuration.getOptionalWithFileSupport[Long]("otoroshi.healthcheck.ttl").getOrElse(60 * 1000)
  lazy val healtCheckTTLOnly: Boolean    =
    configuration.getOptionalWithFileSupport[Boolean]("otoroshi.healthcheck.ttl-only").getOrElse(true)

  lazy val maxWebhookSize: Int = configuration.getOptionalWithFileSupport[Int]("app.webhooks.size").getOrElse(100)

  lazy val clusterConfig: ClusterConfig           = ClusterConfig.fromRoot(configuration, this)
  lazy val clusterAgent: ClusterAgent             = ClusterAgent(clusterConfig, this)
  lazy val clusterLeaderAgent: ClusterLeaderAgent = ClusterLeaderAgent(clusterConfig, this)

  lazy val routeBaseDomain =
    configuration.getOptionalWithFileSupport[String]("otoroshi.routeBaseDomain").getOrElse("newroute.oto.tools")

  lazy val defaultPrettyAdminApi: Boolean =
    configuration.getOptionalWithFileSupport[Boolean]("otoroshi.options.defaultPrettyAdminApi").getOrElse(true)

  lazy val bypassUserRightsCheck: Boolean =
    configuration.getOptionalWithFileSupport[Boolean]("otoroshi.bypassUserRightsCheck").getOrElse(false)

  lazy val globalMaintenanceMode: Boolean =
    configuration.getOptionalWithFileSupport[Boolean]("otoroshi.maintenanceMode").getOrElse(false)

  lazy val metricsEnabled: Boolean =
    configuration.getOptionalWithFileSupport[Boolean]("otoroshi.metrics.enabled").getOrElse(true)

  lazy val staticExposedDomain: Option[String] =
    configuration.getOptionalWithFileSupport[String]("otoroshi.options.staticExposedDomain")

  lazy val staticExposedDomainEnabled: Boolean = staticExposedDomain.isDefined

  lazy val providerDashboardUrl: Option[String] =
    configuration.getOptionalWithFileSupport[String]("otoroshi.provider.dashboardUrl")

  lazy val providerJsUrl: Option[String] =
    configuration.getOptionalWithFileSupport[String]("otoroshi.provider.jsUrl")

  lazy val providerCssUrl: Option[String] =
    configuration.getOptionalWithFileSupport[String]("otoroshi.provider.cssUrl")

  lazy val providerJsUrlHtml: Html =
    providerJsUrl.map(url => Html(s"""""")).getOrElse(Html(""))

  lazy val providerCssUrlHtml: Html =
    providerCssUrl.map(url => Html(s"""""")).getOrElse(Html(""))

  lazy val otoroshiSecret: String = configuration.getOptionalWithFileSupport[String]("otoroshi.secret").get

  lazy val providerDashboardSecret: String =
    configuration.getOptionalWithFileSupport[String]("otoroshi.provider.secret").getOrElse("secret")

  lazy val providerDashboardTitle: String =
    configuration.getOptionalWithFileSupport[String]("otoroshi.provider.title").getOrElse("Provider's dashboard")

  lazy val useEventStreamForScriptEvents: Boolean =
    configuration.getOptionalWithFileSupport[Boolean]("otoroshi.options.useEventStreamForScriptEvents").getOrElse(true)

  lazy val emptyContentLengthIsChunked: Boolean =
    configuration.getOptionalWithFileSupport[Boolean]("otoroshi.options.emptyContentLengthIsChunked").getOrElse(false)

  lazy val detectApiKeySooner: Boolean =
    configuration.getOptionalWithFileSupport[Boolean]("otoroshi.options.detectApiKeySooner").getOrElse(false)

  lazy val metricsAccessKey: Option[String] =
    configuration.getOptionalWithFileSupport[String]("otoroshi.metrics.accessKey").orElse(healthAccessKey)

  lazy val metricsEvery: FiniteDuration =
    configuration
      .getOptionalWithFileSupport[Long]("otoroshi.metrics.every")
      .map(v => FiniteDuration(v, TimeUnit.MILLISECONDS))
      .getOrElse(FiniteDuration(30, TimeUnit.SECONDS))

  lazy val staticGlobalScripts: GlobalScripts = {
    GlobalScripts(
      enabled = configuration.getOptionalWithFileSupport[Boolean]("otoroshi.scripts.static.enabled").getOrElse(false),
      transformersRefs = configuration
        .getOptionalWithFileSupport[Seq[String]]("otoroshi.scripts.static.transformersRefs")
        .orElse(
          configuration
            .getOptionalWithFileSupport[String]("otoroshi.scripts.static.transformersRefsStr")
            .map(_.split(",").map(_.trim).toSeq)
        )
        .getOrElse(Seq.empty[String]),
      transformersConfig = configuration
        .getOptionalWithFileSupport[Configuration]("otoroshi.scripts.static.transformersConfig")
        .map(c => Json.parse(c.underlying.root().render(ConfigRenderOptions.concise())))
        .orElse(
          configuration
            .getOptionalWithFileSupport[String]("otoroshi.scripts.static.transformersConfigStr")
            .map(Json.parse)
        )
        .getOrElse(Json.obj()),
      validatorRefs = configuration
        .getOptionalWithFileSupport[Seq[String]]("otoroshi.scripts.static.validatorRefs")
        .orElse(
          configuration
            .getOptionalWithFileSupport[String]("otoroshi.scripts.static.validatorRefsStr")
            .map(_.split(",").map(_.trim).toSeq)
        )
        .getOrElse(Seq.empty[String]),
      validatorConfig = configuration
        .getOptionalWithFileSupport[Configuration]("otoroshi.scripts.static.validatorConfig")
        .map(c => Json.parse(c.underlying.root().render(ConfigRenderOptions.concise())))
        .orElse(
          configuration.getOptionalWithFileSupport[String]("otoroshi.scripts.static.validatorConfigStr").map(Json.parse)
        )
        .getOrElse(Json.obj()),
      preRouteRefs = configuration
        .getOptionalWithFileSupport[Seq[String]]("otoroshi.scripts.static.preRouteRefs")
        .orElse(
          configuration
            .getOptionalWithFileSupport[String]("otoroshi.scripts.static.preRouteRefsStr")
            .map(_.split(",").map(_.trim).toSeq)
        )
        .getOrElse(Seq.empty[String]),
      preRouteConfig = configuration
        .getOptionalWithFileSupport[Configuration]("otoroshi.scripts.static.preRouteConfig")
        .map(c => Json.parse(c.underlying.root().render(ConfigRenderOptions.concise())))
        .orElse(
          configuration.getOptionalWithFileSupport[String]("otoroshi.scripts.static.preRouteConfigStr").map(Json.parse)
        )
        .getOrElse(Json.obj()),
      sinkRefs = configuration
        .getOptionalWithFileSupport[Seq[String]]("otoroshi.scripts.static.sinkRefs")
        .orElse(
          configuration
            .getOptionalWithFileSupport[String]("otoroshi.scripts.static.sinkRefsStr")
            .map(_.split(",").map(_.trim).toSeq)
        )
        .getOrElse(Seq.empty[String]),
      sinkConfig = configuration
        .getOptionalWithFileSupport[Configuration]("otoroshi.scripts.static.sinkConfig")
        .map(c => Json.parse(c.underlying.root().render(ConfigRenderOptions.concise())))
        .orElse(
          configuration.getOptionalWithFileSupport[String]("otoroshi.scripts.static.sinkConfigStr").map(Json.parse)
        )
        .getOrElse(Json.obj()),
      jobRefs = configuration
        .getOptionalWithFileSupport[Seq[String]]("otoroshi.scripts.static.jobsRefs")
        .orElse(
          configuration
            .getOptionalWithFileSupport[String]("otoroshi.scripts.static.jobsRefsStr")
            .map(_.split(",").map(_.trim).toSeq)
        )
        .getOrElse(Seq.empty[String]),
      jobConfig = configuration
        .getOptionalWithFileSupport[Configuration]("otoroshi.scripts.static.jobsConfig")
        .map(c => Json.parse(c.underlying.root().render(ConfigRenderOptions.concise())))
        .orElse(
          configuration.getOptionalWithFileSupport[String]("otoroshi.scripts.static.jobsConfigStr").map(Json.parse)
        )
        .getOrElse(Json.obj())
    )
  }

  lazy val requestTimeout: FiniteDuration =
    configuration.getOptionalWithFileSupport[Int]("app.proxy.requestTimeout").map(_.millis).getOrElse(1.hour)

  lazy val longRequestTimeout: FiniteDuration =
    configuration.getOptionalWithFileSupport[Int]("app.proxy.longRequestTimeout").map(_.millis).getOrElse(3.hour)

  lazy val initialTrustXForwarded: Boolean =
    configuration.getOptionalWithFileSupport[Boolean]("otoroshi.options.trustXForwarded").getOrElse(true)

  lazy val wasmCacheTtl: Int        =
    configuration.getOptionalWithFileSupport[Int]("otoroshi.wasm.cache.ttl").getOrElse(10000)
  lazy val wasmCacheSize: Int       =
    configuration.getOptionalWithFileSupport[Int]("otoroshi.wasm.cache.size").getOrElse(100)
  lazy val wasmQueueBufferSize: Int =
    configuration.getOptionalWithFileSupport[Int]("otoroshi.wasm.queue.buffer.size").getOrElse(2048)

  lazy val manualDnsResolve: Boolean         =
    configuration.getOptionalWithFileSupport[Boolean]("otoroshi.options.manualDnsResolve").getOrElse(true)
  lazy val useOldHeadersComposition: Boolean =
    configuration.getOptionalWithFileSupport[Boolean]("otoroshi.options.useOldHeadersComposition").getOrElse(false)
  lazy val sendClientChainAsPem: Boolean     =
    configuration.getOptionalWithFileSupport[Boolean]("otoroshi.options.sendClientChainAsPem").getOrElse(false)
  lazy val validateRequests: Boolean         =
    configuration.getOptionalWithFileSupport[Boolean]("otoroshi.requests.validate").getOrElse(true)
  lazy val maxUrlLength: Long                =
    Option(configuration.underlying.getBytes("otoroshi.requests.maxUrlLength")).map(_.toLong).getOrElse(16 * 1024L)
  lazy val maxCookieLength: Long             =
    Option(configuration.underlying.getBytes("otoroshi.requests.maxCookieLength")).map(_.toLong).getOrElse(16 * 1024L)
  lazy val maxHeaderValueLength: Long        = Option(
    configuration.underlying.getBytes("otoroshi.requests.maxHeaderValueLength")
  ).map(_.toLong).getOrElse(16 * 1024L)
  lazy val maxHeaderNameLength: Long         =
    Option(configuration.underlying.getBytes("otoroshi.requests.maxHeaderNameLength")).map(_.toLong).getOrElse(128L)

  lazy val healthAccessKey: Option[String] = configuration.getOptionalWithFileSupport[String]("app.health.accessKey")
  lazy val overheadThreshold: Double       =
    configuration.getOptionalWithFileSupport[Double]("app.overheadThreshold").getOrElse(500.0)
  lazy val healthLimit: Double             = configuration.getOptionalWithFileSupport[Double]("app.health.limit").getOrElse(1000.0)
  lazy val throttlingWindow: Int           = configuration.getOptionalWithFileSupport[Int]("app.throttlingWindow").getOrElse(10)
  lazy val analyticsWindow: Int            = configuration.getOptionalWithFileSupport[Int]("app.analyticsWindow").getOrElse(30)
  lazy val eventsName: String              = configuration.getOptionalWithFileSupport[String]("app.eventsName").getOrElse("otoroshi")
  lazy val storageRoot: String             =
    configuration.getOptionalWithFileSupport[String]("app.storageRoot").getOrElse("otoroshi")
  lazy val useCache: Boolean               =
    configuration.getOptionalWithFileSupport[Boolean]("otoroshi.cache.enabled").getOrElse(false)
  lazy val cacheTtl: Int                   =
    configuration.getOptionalWithFileSupport[Int]("otoroshi.cache.ttl").filter(_ >= 2000).getOrElse(2000)
  lazy val useRedisScan: Boolean           =
    configuration.getOptionalWithFileSupport[Boolean]("app.redis.useScan").getOrElse(false)
  lazy val secret: String                  = configuration.getOptionalWithFileSupport[String]("play.crypto.secret").get
  lazy val secretSession: String           =
    configuration
      .getOptionalWithFileSupport[String]("otoroshi.sessions.secret")
      .map(_.padTo(16, "0").mkString("").take(16))
      .get
  lazy val sharedKey: String               = configuration.getOptionalWithFileSupport[String]("app.claim.sharedKey").get
  lazy val env: String                     = configuration.getOptionalWithFileSupport[String]("app.env").getOrElse("prod")
  lazy val isDev: Boolean                  = env == "dev"
  lazy val isProd: Boolean                 = !isDev
  lazy val number: Int                     = configuration.getOptionalWithFileSupport[Int]("app.instance.number").getOrElse(0)
  lazy val name: String                    = configuration.getOptionalWithFileSupport[String]("app.instance.name").getOrElse("otoroshi")
  lazy val title: String                   = configuration
    .getOptionalWithFileSupport[String]("app.instance.title")
    .map {
      case v if v.startsWith("ReplaceAll(") => v.substring(11, v.length).init
      case v                                => v
    }
    .getOrElse("Otoroshi")
  // lazy val rack: String                    = configuration.getOptionalWithFileSupport[String]("app.instance.rack").getOrElse("local")
  // lazy val infraProvider: String           =
  //   configuration.getOptionalWithFileSupport[String]("app.instance.provider").getOrElse("local")
  // lazy val dataCenter: String              = configuration.getOptionalWithFileSupport[String]("app.instance.dc").getOrElse("local")
  // lazy val zone: String                    = configuration.getOptionalWithFileSupport[String]("app.instance.zone").getOrElse("local")
  // lazy val region: String                  = configuration.getOptionalWithFileSupport[String]("app.instance.region").getOrElse("local")
  lazy val rack: String                    = clusterConfig.relay.location.rack
  lazy val infraProvider: String           = clusterConfig.relay.location.provider
  lazy val dataCenter: String              = clusterConfig.relay.location.datacenter
  lazy val zone: String                    = clusterConfig.relay.location.zone
  lazy val region: String                  = clusterConfig.relay.location.region
  lazy val liveJs: Boolean                 = configuration
    .getOptionalWithFileSupport[String]("app.env")
    .filter(_ == "dev")
    .map(_ => true)
    .orElse(configuration.getOptionalWithFileSupport[Boolean]("app.liveJs"))
    .getOrElse(false)
  lazy val revolver: Boolean               = configuration.getOptionalWithFileSupport[Boolean]("app.revolver").getOrElse(false)

  lazy val exposeAdminApi: Boolean                         =
    if (clusterConfig.mode.isWorker) false
    else configuration.getOptionalWithFileSupport[Boolean]("app.adminapi.exposed").getOrElse(true)
  lazy val exposeAdminDashboard: Boolean                   =
    if (clusterConfig.mode.isWorker) false
    else configuration.getOptionalWithFileSupport[Boolean]("app.backoffice.exposed").getOrElse(true)
  lazy val adminApiProxyHttps: Boolean                     =
    configuration.getOptionalWithFileSupport[Boolean]("app.adminapi.proxy.https").getOrElse(false)
  lazy val adminApiProxyUseLocal: Boolean                  =
    configuration.getOptionalWithFileSupport[Boolean]("app.adminapi.proxy.local").getOrElse(true)
  lazy val domain: String                                  = configuration.getOptionalWithFileSupport[String]("app.domain").getOrElse("oto.tools")
  lazy val adminApiSubDomain: String                       =
    configuration
      .getOptionalWithFileSupport[String]("app.adminapi.targetSubdomain")
      .getOrElse("otoroshi-admin-internal-api")
  lazy val adminApiExposedSubDomain: String                =
    configuration.getOptionalWithFileSupport[String]("app.adminapi.exposedSubdomain").getOrElse("otoroshi-api")
  lazy val adminApiAdditionalExposedDomain: Option[String] =
    configuration.getOptionalWithFileSupport[String]("app.adminapi.additionalExposedDomain")

  // lazy val backofficeUseNewEngine: Boolean =
  //   configuration.getOptionalWithFileSupport[Boolean]("app.backoffice.useNewEngine").getOrElse(false)
  lazy val backofficeUsePlay: Boolean   =
    configuration.getOptionalWithFileSupport[Boolean]("app.backoffice.usePlay").getOrElse(true)
  lazy val backOfficeSubDomain: String  =
    configuration.getOptionalWithFileSupport[String]("app.backoffice.subdomain").getOrElse("otoroshi")
  lazy val privateAppsSubDomain: String =
    configuration.getOptionalWithFileSupport[String]("app.privateapps.subdomain").getOrElse("privateapps")
  lazy val retries: Int                 = configuration.getOptionalWithFileSupport[Int]("app.retries").getOrElse(5)

  lazy val backOfficeServiceId          =
    configuration.getOptionalWithFileSupport[String]("app.adminapi.defaultValues.backOfficeServiceId").get
  lazy val backOfficeGroupId            =
    configuration.getOptionalWithFileSupport[String]("app.adminapi.defaultValues.backOfficeGroupId").get
  lazy val backOfficeApiKeyClientId     =
    configuration.getOptionalWithFileSupport[String]("app.adminapi.defaultValues.backOfficeApiKeyClientId").get
  lazy val backOfficeApiKeyClientSecret =
    configuration.getOptionalWithFileSupport[String]("app.adminapi.defaultValues.backOfficeApiKeyClientSecret").get

  def composeMainUrl(subdomain: String): String = s"$subdomain.$domain"

  lazy val adminApiExposedHost = composeMainUrl(adminApiExposedSubDomain)
  lazy val adminApiHost        = composeMainUrl(adminApiSubDomain)
  lazy val backOfficeHost      = composeMainUrl(backOfficeSubDomain)
  lazy val privateAppsHost     = composeMainUrl(privateAppsSubDomain)

  lazy val adminApiExposedDomains = configuration
    .getOptionalWithFileSupport[Seq[String]]("app.adminapi.exposedDomains")
    .orElse(
      configuration
        .getOptionalWithFileSupport[String]("app.adminapi.exposedDomainsStr")
        .map(ds => ds.split(",").toSeq.map(_.trim))
    )
    .getOrElse(Seq.empty)
  lazy val adminApiDomains        = configuration
    .getOptionalWithFileSupport[Seq[String]]("app.adminapi.domains")
    .orElse(
      configuration
        .getOptionalWithFileSupport[String]("app.adminapi.domainsStr")
        .map(ds => ds.split(",").toSeq.map(_.trim))
    )
    .getOrElse(Seq.empty)
  lazy val privateAppsDomains     = configuration
    .getOptionalWithFileSupport[Seq[String]]("app.privateapps.domains")
    .orElse(
      configuration
        .getOptionalWithFileSupport[String]("app.privateapps.domainsStr")
        .map(ds => ds.split(",").toSeq.map(_.trim))
    )
    .getOrElse(Seq.empty)
  lazy val backofficeDomains      = configuration
    .getOptionalWithFileSupport[Seq[String]]("app.backoffice.domains")
    .orElse(
      configuration
        .getOptionalWithFileSupport[String]("app.backoffice.domainsStr")
        .map(ds => ds.split(",").toSeq.map(_.trim))
    )
    .getOrElse(Seq.empty)

  lazy val procNbr = Runtime.getRuntime.availableProcessors()

  lazy val adminEntityValidators: Map[String, Seq[JsonValidator]] = configurationJson
    .select("otoroshi")
    .select("adminapi")
    .select("entity_validators")
    .asOpt[JsObject]
    .map { obj =>
      obj.value.mapValues { arr =>
        arr.asArray.value
          .map { item =>
            JsonValidator.format.reads(item)
          }
          .collect { case JsSuccess(v, _) =>
            v
          }
      }.toMap
    }
    .getOrElse(Map.empty[String, Seq[JsonValidator]])

  lazy val ahcStats         = new AtomicReference[Cancellable]()
  lazy val internalAhcStats = new AtomicReference[Cancellable]()

  lazy val reactorClientInternal = new otoroshi.netty.NettyHttpClient(this)
  lazy val reactorClientGateway  = new otoroshi.netty.NettyHttpClient(this)
  lazy val http3Client           = new otoroshi.netty.NettyHttp3Client(this)

  lazy val gatewayClient = {
    val parser: WSConfigParser         = new WSConfigParser(configuration.underlying, environment.classLoader)
    val config: AhcWSClientConfig      = new AhcWSClientConfig(wsClientConfig = parser.parse()).copy(
      keepAlive = configuration.getOptionalWithFileSupport[Boolean]("app.proxy.keepAlive").getOrElse(true)
      //setHttpClientCodecMaxChunkSize(1024 * 100)
    )
    val wsClientConfig: WSClientConfig = config.wsClientConfig.copy(
      userAgent = Some("Otoroshi-akka"),
      compressionEnabled =
        configuration.getOptionalWithFileSupport[Boolean]("app.proxy.compressionEnabled").getOrElse(false),
      requestTimeout = configuration
        .getOptionalWithFileSupport[Int]("app.proxy.requestTimeout")
        .map(_.millis)
        .getOrElse((60 * 60 * 1000).millis),
      idleTimeout = configuration
        .getOptionalWithFileSupport[Int]("app.proxy.idleTimeout")
        .map(_.millis)
        .getOrElse((60 * 60 * 1000).millis),
      connectionTimeout = configuration
        .getOptionalWithFileSupport[Int]("app.proxy.connectionTimeout")
        .map(_.millis)
        .getOrElse((2 * 60 * 1000).millis)
    )
    val ahcClient: AhcWSClient         = AhcWSClient(
      config.copy(
        wsClientConfig = wsClientConfig
      )
    )(otoroshiMaterializer)

    import collection.JavaConverters._
    ahcStats.set(otoroshiActorSystem.scheduler.scheduleWithFixedDelay(1.second, 1.second) { () =>
      scala.util.Try {
        val stats = ahcClient.underlying[DefaultAsyncHttpClient].getClientStats
        metrics.histogramUpdate("ahc-total-active-connections", stats.getTotalActiveConnectionCount)
        metrics.histogramUpdate("ahc-total-connections", stats.getTotalConnectionCount)
        metrics.histogramUpdate("ahc-total-idle-connections", stats.getTotalIdleConnectionCount)
        stats.getStatsPerHost.asScala.foreach { case (key, value) =>
          metrics.histogramUpdate(key + "-ahc-total-active-connections", value.getHostActiveConnectionCount)
          metrics.histogramUpdate(key + "-ahc-total-connections", value.getHostConnectionCount)
          metrics.histogramUpdate(key + "-ahc-total-idle-connections", value.getHostIdleConnectionCount)
        }
      } match {
        case Success(_) => ()
        case Failure(e) => logger.error("error while publishing ahc stats", e)
      }
    }(otoroshiExecutionContext))

    WsClientChooser(
      ahcClient,
      new AkkWsClient(wsClientConfig, this)(otoroshiActorSystem, otoroshiMaterializer),
      reactorClientGateway,
      configuration.getOptionalWithFileSupport[Boolean]("app.proxy.useAkkaClient").getOrElse(false),
      this
    )
  }

  lazy val _internalClient = {
    val parser: WSConfigParser         = new WSConfigParser(configuration.underlying, environment.classLoader)
    val config: AhcWSClientConfig      = new AhcWSClientConfig(wsClientConfig = parser.parse()).copy(
      keepAlive = configuration.getOptionalWithFileSupport[Boolean]("app.proxy.keepAlive").getOrElse(true)
      //setHttpClientCodecMaxChunkSize(1024 * 100)
    )
    val wsClientConfig: WSClientConfig = config.wsClientConfig.copy(
      userAgent = Some("Otoroshi-akka"),
      compressionEnabled =
        configuration.getOptionalWithFileSupport[Boolean]("app.proxy.compressionEnabled").getOrElse(false),
      requestTimeout = configuration
        .getOptionalWithFileSupport[Int]("app.proxy.requestTimeout")
        .map(_.millis)
        .getOrElse((60 * 60 * 1000).millis),
      idleTimeout = configuration
        .getOptionalWithFileSupport[Int]("app.proxy.idleTimeout")
        .map(_.millis)
        .getOrElse((60 * 60 * 1000).millis),
      connectionTimeout = configuration
        .getOptionalWithFileSupport[Int]("app.proxy.connectionTimeout")
        .map(_.millis)
        .getOrElse((2 * 60 * 1000).millis)
    )
    import collection.JavaConverters._
    internalAhcStats.set(otoroshiActorSystem.scheduler.scheduleWithFixedDelay(1.second, 1.second) { () =>
      scala.util.Try {
        val stats = wsClient.underlying[DefaultAsyncHttpClient].getClientStats
        metrics.histogramUpdate("ahc-total-active-connections", stats.getTotalActiveConnectionCount)
        metrics.histogramUpdate("ahc-total-connections", stats.getTotalConnectionCount)
        metrics.histogramUpdate("ahc-total-idle-connections", stats.getTotalIdleConnectionCount)
        stats.getStatsPerHost.asScala.foreach { case (key, value) =>
          metrics.histogramUpdate(key + "-ahc-total-active-connections", value.getHostActiveConnectionCount)
          metrics.histogramUpdate(key + "-ahc-total-connections", value.getHostConnectionCount)
          metrics.histogramUpdate(key + "-ahc-total-idle-connections", value.getHostIdleConnectionCount)
        }
      } match {
        case Success(_) => ()
        case Failure(e) => logger.error("error while publishing ahc stats", e)
      }
    }(otoroshiExecutionContext))
    WsClientChooser(
      wsClient,
      new AkkWsClient(wsClientConfig, this)(otoroshiActorSystem, otoroshiMaterializer),
      reactorClientInternal,
      configuration.getOptionalWithFileSupport[Boolean]("app.proxy.useAkkaClient").getOrElse(false),
      this
    )
  }

  // lazy val geoloc = new GeoLite2GeolocationHelper(this)
  // lazy val ua = new UserAgentHelper(this)

  lazy val statsd  = new StatsdWrapper(otoroshiActorSystem, this)
  lazy val metrics = new Metrics(this, lifecycle)
  lazy val pki     = new BouncyCastlePki(snowflakeGenerator, this)

  lazy val tunnelManager = new TunnelManager(this)
  lazy val tunnelAgent   = new TunnelAgent(this)

  lazy val hash = s"${System.currentTimeMillis()}"

  lazy val backOfficeSessionExp = configuration.getOptionalWithFileSupport[Long]("app.backoffice.session.exp").get

  lazy val exposedRootScheme = configuration.getOptionalWithFileSupport[String]("app.rootScheme").getOrElse("https")

  def rootScheme               = s"${exposedRootScheme}://"
  def exposedRootSchemeIsHttps = exposedRootScheme == "https"

  lazy val Ws     = _internalClient
  lazy val MtlsWs = otoroshi.utils.http.MtlsWs(_internalClient)

  lazy val snowflakeSeed             = configuration.getOptionalWithFileSupport[Long]("app.snowflake.seed").get
  lazy val snowflakeGenerator        = IdGenerator(snowflakeSeed)
  lazy val redirections: Seq[String] =
    configuration.getOptionalWithFileSupport[Seq[String]]("app.redirections").map(_.toSeq).getOrElse(Seq.empty[String])

  lazy val crypto = ClaimCrypto(sharedKey)

  object Headers {
    lazy val OtoroshiVizFromLabel               = configuration.getOptionalWithFileSupport[String]("otoroshi.headers.trace.label").get
    lazy val OtoroshiVizFrom                    = configuration.getOptionalWithFileSupport[String]("otoroshi.headers.trace.from").get
    lazy val OtoroshiGatewayParentRequest       =
      configuration.getOptionalWithFileSupport[String]("otoroshi.headers.trace.parent").get
    lazy val OtoroshiAdminProfile               =
      configuration.getOptionalWithFileSupport[String]("otoroshi.headers.request.adminprofile").get
    lazy val OtoroshiClientId                   =
      configuration.getOptionalWithFileSupport[String]("otoroshi.headers.request.clientid").get
    lazy val OtoroshiSimpleApiKeyClientId       =
      configuration.getOptionalWithFileSupport[String]("otoroshi.headers.request.simpleapiclientid").get
    lazy val OtoroshiClientSecret               =
      configuration.getOptionalWithFileSupport[String]("otoroshi.headers.request.clientsecret").get
    lazy val OtoroshiRequestId                  = configuration.getOptionalWithFileSupport[String]("otoroshi.headers.request.id").get
    lazy val OtoroshiRequestTimestamp           =
      configuration.getOptionalWithFileSupport[String]("otoroshi.headers.request.timestamp").get
    lazy val OtoroshiAuthorization              =
      configuration.getOptionalWithFileSupport[String]("otoroshi.headers.request.authorization").get
    lazy val OtoroshiBearer                     = configuration.getOptionalWithFileSupport[String]("otoroshi.headers.request.bearer").get
    lazy val OtoroshiJWTAuthorization           =
      configuration.getOptionalWithFileSupport[String]("otoroshi.headers.request.jwtAuthorization").get
    lazy val OtoroshiBasicAuthorization         =
      configuration.getOptionalWithFileSupport[String]("otoroshi.headers.request.basicAuthorization").get
    lazy val OtoroshiBearerAuthorization        =
      configuration.getOptionalWithFileSupport[String]("otoroshi.headers.request.bearerAuthorization").get
    lazy val OtoroshiProxiedHost                =
      configuration.getOptionalWithFileSupport[String]("otoroshi.headers.response.proxyhost").get
    lazy val OtoroshiGatewayError               =
      configuration.getOptionalWithFileSupport[String]("otoroshi.headers.response.error").get
    lazy val OtoroshiErrorMsg                   =
      configuration.getOptionalWithFileSupport[String]("otoroshi.headers.response.errormsg").get
    lazy val OtoroshiErrorCause                 =
      configuration.getOptionalWithFileSupport[String]("otoroshi.headers.response.errorcause").get
    lazy val OtoroshiProxyLatency               =
      configuration.getOptionalWithFileSupport[String]("otoroshi.headers.response.proxylatency").get
    lazy val OtoroshiUpstreamLatency            =
      configuration.getOptionalWithFileSupport[String]("otoroshi.headers.response.upstreamlatency").get
    lazy val OtoroshiDailyCallsRemaining        =
      configuration.getOptionalWithFileSupport[String]("otoroshi.headers.response.dailyquota").get
    lazy val OtoroshiMonthlyCallsRemaining      =
      configuration.getOptionalWithFileSupport[String]("otoroshi.headers.response.monthlyquota").get
    lazy val OtoroshiState                      = configuration.getOptionalWithFileSupport[String]("otoroshi.headers.comm.state").get
    lazy val OtoroshiStateResp                  = configuration.getOptionalWithFileSupport[String]("otoroshi.headers.comm.stateresp").get
    lazy val OtoroshiClaim                      = configuration.getOptionalWithFileSupport[String]("otoroshi.headers.comm.claim").get
    lazy val OtoroshiHealthCheckLogicTest       =
      configuration.getOptionalWithFileSupport[String]("otoroshi.headers.healthcheck.test").get
    lazy val OtoroshiHealthCheckLogicTestResult =
      configuration.getOptionalWithFileSupport[String]("otoroshi.headers.healthcheck.testresult").get
    lazy val OtoroshiIssuer                     = configuration.getOptionalWithFileSupport[String]("otoroshi.headers.jwt.issuer").get
    lazy val OtoroshiTrackerId                  = configuration.getOptionalWithFileSupport[String]("otoroshi.headers.canary.tracker").get
    lazy val OtoroshiClientCertChain            =
      configuration.getOptionalWithFileSupport[String]("otoroshi.headers.client.cert.chain").get
  }

  val confPackages: Seq[String] =
    configuration.getOptionalWithFileSupport[Seq[String]]("otoroshi.plugins.packages").getOrElse(Seq.empty) ++
    configuration
      .getOptionalWithFileSupport[String]("otoroshi.plugins.packagesStr")
      .map(v => v.split(",").map(_.trim).toSeq)
      .getOrElse(Seq.empty)

  val blacklistedPlugins: Set[String] =
    (configuration.getOptionalWithFileSupport[Seq[String]]("otoroshi.plugins.blacklisted").getOrElse(Seq.empty) ++
      configuration
        .getOptionalWithFileSupport[String]("otoroshi.plugins.blacklistedStr")
        .map(v => v.split(",").map(_.trim).toSeq)
        .getOrElse(Seq.empty)).toSet

  logger.info(s"Otoroshi version ${otoroshiVersion}")
  // logger.info(s"Scala version ${scala.util.Properties.versionNumberString} / ${scala.tools.nsc.Properties.versionNumberString}")
  if (!testing) {
    logger.info(s"Admin API exposed on http://$adminApiExposedHost:$port")
    logger.info(s"Admin UI  exposed on http://$backOfficeHost:$port")
  }

  def displayDefaultValuesWarning(): Unit = {

    def checkValue(value: String, default: String, path: String, envvar: String, desc: String): Option[String] = {
      if (value == default) {
        Some(s"$path (env. var. $envvar): $desc")
      } else {
        None
      }
    }

    val values: Seq[String] = Seq(
      checkValue(
        otoroshiSecret,
        "verysecretvaluethatyoumustoverwrite",
        "otoroshi.secret",
        "OTOROSHI_SECRET",
        "used to sign various stuff including session cookies"
      ),
      checkValue(
        backOfficeApiKeyClientSecret,
        "admin-api-apikey-secret",
        "otoroshi.admin-api-secret",
        "OTOROSHI_ADMIN_API_SECRET",
        "used to access otoroshi admin api"
      )
    ).collect { case Some(mess) =>
      s" - $mess"
    }

    if (!clusterConfig.mode.isWorker && values.nonEmpty) {
      logger.warn("")
      logger.warn("#########################################")
      logger.warn("")
      logger.warn("DEFAULT VALUES USAGE DETECTED !!!")
      logger.warn("")
      logger.warn("You are using the default values for the following security involved configs:")
      logger.warn("")
      values.foreach(m => logger.warn(m))
      logger.warn("")
      logger.warn("You MUST change those values before deploying to production")
      logger.warn("You can change configuration by passing path values with config file or via runtime flags")
      logger.warn(
        "    https://maif.github.io/otoroshi/manual/install/setup-otoroshi.html#setup-your-configuration-file"
      )
      logger.warn("You can change configuration by passing environment variables")
      logger.warn(
        "    https://maif.github.io/otoroshi/manual/install/setup-otoroshi.html#configuration-with-env-variables"
      )
      logger.warn("")
      logger.warn("#########################################")
      logger.warn("")
    }
  }

  displayDefaultValuesWarning()

  lazy val datastoreKind: String = configuration.getOptionalWithFileSupport[String]("app.storage").getOrElse("lettuce")
  lazy val datastores: DataStores = {
    configuration.getOptionalWithFileSupport[String]("app.storage").getOrElse("lettuce") match {
      case _ if clusterConfig.mode == ClusterMode.Worker                   =>
        new SwappableInMemoryDataStores(configuration, environment, lifecycle, this)
      case v if v.startsWith("cp:")                                        =>
        scriptManager.getAnyScript[DataStoresBuilder](v)(otoroshiExecutionContext) match {
          case Left(err)  => {
            logger.error(s"specified datastore with name '${v}' does not exists or failed to instanciate: ${err}")
            System.exit(-1)
            ???
          }
          case Right(dsb) => dsb.build(configuration, environment, lifecycle, clusterConfig.mode, this)
        }
      case v if v.startsWith("ext:")                                       =>
        val parts = v.split(":").toSeq
        if (parts.size == 2) {
          val name = parts.apply(1)
          adminExtensions.datastore(name) match {
            case None      => {
              logger.error(s"specified datastore with name '${v}' does not exists")
              System.exit(-1)
              ???
            }
            case Some(dsb) => dsb.build(configuration, environment, lifecycle, clusterConfig.mode, this)
          }
        } else {
          val extensionId = parts.apply(1)
          val name        = parts.apply(2)
          adminExtensions.datastoreFrom(AdminExtensionId(extensionId), name) match {
            case None      => {
              logger.error(s"specified datastore with name '${v}' does not exists")
              System.exit(-1)
              ???
            }
            case Some(dsb) => dsb.build(configuration, environment, lifecycle, clusterConfig.mode, this)
          }
        }
      case "redis-pool" if clusterConfig.mode == ClusterMode.Leader        =>
        new RedisCPDataStores(configuration, environment, lifecycle, this)
      case "redis-mpool" if clusterConfig.mode == ClusterMode.Leader       =>
        new RedisMCPDataStores(configuration, environment, lifecycle, this)
      case "redis-cluster" if clusterConfig.mode == ClusterMode.Leader     =>
        new RedisClusterDataStores(configuration, environment, lifecycle, this)
      case "redis-lf" if clusterConfig.mode == ClusterMode.Leader          =>
        new RedisLFDataStores(configuration, environment, lifecycle, this)
      case "redis-sentinel" if clusterConfig.mode == ClusterMode.Leader    =>
        new RedisSentinelDataStores(configuration, environment, lifecycle, this)
      case "redis-sentinel-lf" if clusterConfig.mode == ClusterMode.Leader =>
        new RedisSentinelLFDataStores(configuration, environment, lifecycle, this)
      case "redis" if clusterConfig.mode == ClusterMode.Leader             =>
        new RedisLFDataStores(configuration, environment, lifecycle, this)
      case "inmemory" if clusterConfig.mode == ClusterMode.Leader          =>
        new InMemoryDataStores(configuration, environment, lifecycle, PersistenceKind.NoopPersistenceKind, this)
      case "memory" if clusterConfig.mode == ClusterMode.Leader            =>
        new InMemoryDataStores(configuration, environment, lifecycle, PersistenceKind.NoopPersistenceKind, this)
      case "mem" if clusterConfig.mode == ClusterMode.Leader               =>
        new InMemoryDataStores(configuration, environment, lifecycle, PersistenceKind.NoopPersistenceKind, this)
      case "leveldb" if clusterConfig.mode == ClusterMode.Leader           =>
        logger.error(
          "LevelDB datastore is not supported anymore, supported datastores are listed here: https://maif.github.io/otoroshi/manual/install/setup-otoroshi.html#setup-the-database"
        )
        sys.exit(1)
      case "file" if clusterConfig.mode == ClusterMode.Leader              =>
        new InMemoryDataStores(configuration, environment, lifecycle, PersistenceKind.FilePersistenceKind, this)
      case "http" if clusterConfig.mode == ClusterMode.Leader              =>
        new InMemoryDataStores(configuration, environment, lifecycle, PersistenceKind.HttpPersistenceKind, this)
      case "s3" if clusterConfig.mode == ClusterMode.Leader                =>
        new InMemoryDataStores(configuration, environment, lifecycle, PersistenceKind.S3PersistenceKind, this)
      case "cassandra-naive" if clusterConfig.mode == ClusterMode.Leader   =>
        new CassandraDataStores(true, configuration, environment, lifecycle, this)
      case "cassandra" if clusterConfig.mode == ClusterMode.Leader         =>
        new CassandraDataStores(false, configuration, environment, lifecycle, this)
      case "mongo" if clusterConfig.mode == ClusterMode.Leader             =>
        logger.error(
          "MongoDB datastore is not supported anymore, supported datastores are listed here: https://maif.github.io/otoroshi/manual/install/setup-otoroshi.html#setup-the-database"
        )
        sys.exit(1)
      case "lettuce" if clusterConfig.mode == ClusterMode.Leader           =>
        new LettuceDataStores(configuration, environment, lifecycle, this)
      case "experimental-pg" if clusterConfig.mode == ClusterMode.Leader   =>
        new ReactivePgDataStores(configuration, environment, lifecycle, this)
      case "pg" if clusterConfig.mode == ClusterMode.Leader                =>
        new ReactivePgDataStores(configuration, environment, lifecycle, this)
      case "postgresql" if clusterConfig.mode == ClusterMode.Leader        =>
        new ReactivePgDataStores(configuration, environment, lifecycle, this)
      case "redis"                                                         => new RedisLFDataStores(configuration, environment, lifecycle, this)
      case "inmemory"                                                      =>
        new InMemoryDataStores(configuration, environment, lifecycle, PersistenceKind.NoopPersistenceKind, this)
      case "memory"                                                        =>
        new InMemoryDataStores(configuration, environment, lifecycle, PersistenceKind.NoopPersistenceKind, this)
      case "mem"                                                           =>
        new InMemoryDataStores(configuration, environment, lifecycle, PersistenceKind.NoopPersistenceKind, this)
      case "leveldb"                                                       =>
        logger.error(
          "LevelDB datastore is not supported anymore, supported datastores are listed here: https://maif.github.io/otoroshi/manual/install/setup-otoroshi.html#setup-the-database"
        )
        sys.exit(1)
      case "file"                                                          =>
        new InMemoryDataStores(configuration, environment, lifecycle, PersistenceKind.FilePersistenceKind, this)
      case "http"                                                          =>
        new InMemoryDataStores(configuration, environment, lifecycle, PersistenceKind.HttpPersistenceKind, this)
      case "s3"                                                            =>
        new InMemoryDataStores(configuration, environment, lifecycle, PersistenceKind.S3PersistenceKind, this)
      case "cassandra-naive"                                               => new CassandraDataStores(true, configuration, environment, lifecycle, this)
      case "cassandra"                                                     => new CassandraDataStores(false, configuration, environment, lifecycle, this)
      case "mongo"                                                         =>
        logger.error(
          "MongoDB datastore is not supported anymore, supported datastores are listed here: https://maif.github.io/otoroshi/manual/install/setup-otoroshi.html#setup-the-database"
        )
        sys.exit(1)
      case "redis-pool"                                                    => new RedisCPDataStores(configuration, environment, lifecycle, this)
      case "redis-mpool"                                                   => new RedisMCPDataStores(configuration, environment, lifecycle, this)
      case "redis-cluster"                                                 => new RedisClusterDataStores(configuration, environment, lifecycle, this)
      case "redis-lf"                                                      => new RedisLFDataStores(configuration, environment, lifecycle, this)
      case "redis-sentinel"                                                => new RedisSentinelDataStores(configuration, environment, lifecycle, this)
      case "redis-sentinel-lf"                                             => new RedisSentinelLFDataStores(configuration, environment, lifecycle, this)
      case "lettuce"                                                       => new LettuceDataStores(configuration, environment, lifecycle, this)
      case "experimental-pg"                                               => new ReactivePgDataStores(configuration, environment, lifecycle, this)
      case "pg"                                                            => new ReactivePgDataStores(configuration, environment, lifecycle, this)
      case "postgresql"                                                    => new ReactivePgDataStores(configuration, environment, lifecycle, this)
      case e                                                               => throw new RuntimeException(s"Bad storage value from conf: $e")
    }
  }

  val openApiSchema = new ClassGraphScanner().run(confPackages, this)

  val scriptingEnabled = configuration.getOptionalWithFileSupport[Boolean]("otoroshi.scripts.enabled").getOrElse(false)
  val scriptCompiler   = new ScriptCompiler(this)
  val scriptManager    = new ScriptManager(this).start()

  if (scriptingEnabled) logger.warn("Scripting is enabled on this Otoroshi instance !")

  if (useCache) logger.warn(s"Datastores will use cache to speed up operations")

  val jobManager = new JobManager(this)

  val servers = TcpService.runServers(this)

  lazy val allResources = new otoroshi.api.OtoroshiResources(this)

  lazy val adminExtensionsConfig = AdminExtensionConfig(
    enabled = configuration.getOptionalWithFileSupport[Boolean]("otoroshi.admin-extensions.enabled").getOrElse(true)
  )

  lazy val adminExtensions = AdminExtensions.current(this, adminExtensionsConfig)

  lazy val wasmIntegration = WasmIntegration(new OtoroshiWasmIntegrationContext(this))

  datastores.before(configuration, environment, lifecycle)
  // geoloc.start()
  // ua.start()
  adminExtensions.start()
  lifecycle.addStopHook(() => {
    implicit val ec = otoroshiExecutionContext
    // geoloc.stop()
    // ua.stop()
    // healthCheckerActor ! PoisonPill
    adminExtensions.stop()
    otoroshiEventsActor ! StopExporters
    otoroshiEventsActor ! PoisonPill
    Option(ahcStats.get()).foreach(_.cancel())
    Option(internalAhcStats.get()).foreach(_.cancel())
    jobManager.stop()
    scriptManager.stop()
    clusterAgent.stop()
    clusterLeaderAgent.stop()
    otoroshiActorSystem.terminate()
    datastores.after(configuration, environment, lifecycle)
    servers.stop()
    // FastFuture.successful(())
  })

  lazy val port = getHttpPort.getOrElse(
    configuration
      .getOptionalWithFileSupport[Int]("play.server.http.port")
      .orElse(configuration.getOptionalWithFileSupport[Int]("http.port"))
      .getOrElse(9999)
  )

  lazy val httpPort = port

  lazy val httpsPort = getHttpsPort.getOrElse(
    configuration
      .getOptionalWithFileSupport[Int]("play.server.https.port")
      .orElse(configuration.getOptionalWithFileSupport[Int]("https.port"))
      .getOrElse(9998)
  )

  lazy val privateAppsPort: String = {
    if (exposedRootSchemeIsHttps) {
      exposedHttpsPort
    } else {
      exposedHttpPort
    }
  }
  // lazy val privateAppsPort: Option[Int]                    =
  //   configuration.getOptionalWithFileSupport[Int]("app.privateapps.port")

  lazy val exposedHttpPort: String = configuration
    .getOptionalWithFileSupport[Int]("app.exposed-ports.http")
    .orElse(port.some)
    .map {
      case 80 => ""
      case v  => s":$v"
    }
    .getOrElse("")

  lazy val exposedHttpPortInt: Int = configuration
    .getOptionalWithFileSupport[Int]("app.exposed-ports.http")
    .getOrElse(port)

  lazy val exposedHttpsPort: String = configuration
    .getOptionalWithFileSupport[Int]("app.exposed-ports.https")
    .orElse(httpsPort.some)
    .map {
      case 443 => ""
      case v   => s":$v"
    }
    .getOrElse("")

  lazy val exposedHttpsPortInt: Int = configuration
    .getOptionalWithFileSupport[Int]("app.exposed-ports.https")
    .getOrElse(httpsPort)

  lazy val bestExposedPort: String = if (exposedRootSchemeIsHttps) {
    exposedHttpsPort
  } else {
    exposedHttpPort
  }

  lazy val proxyState = new NgProxyState(this)

  lazy val http2ClientProxyEnabled = configuration
    .getOptionalWithFileSupport[Boolean]("otoroshi.next.experimental.http2-client-proxy.enabled")
    .getOrElse(false)
  lazy val http2ClientProxyPort    =
    configuration.getOptionalWithFileSupport[Int]("otoroshi.next.experimental.http2-client-proxy.port").getOrElse(8555)

  lazy val defaultConfig = GlobalConfig(
    initWithNewEngine = true,
    trustXForwarded = initialTrustXForwarded,
    perIpThrottlingQuota = 500,
    throttlingQuota = 100000,
    maxLogsSize = configuration.getOptionalWithFileSupport[Int]("app.events.maxSize").getOrElse(100),
    otoroshiId =
      configuration.getOptionalWithFileSupport[String]("otoroshi.instance.instanceId").getOrElse(IdGenerator.uuid),
    plugins = Plugins(
      enabled = true,
      refs = Seq(
        "cp:otoroshi.next.proxy.ProxyEngine",
        "cp:otoroshi.plugins.apikeys.ClientCredentialService"
      ),
      config = Json.obj(
        "NextGenProxyEngine"      -> Json.obj(
          "enabled"          -> true,
          "debug"            -> false,
          "debug_headers"    -> false,
          "domains"          -> Seq("*"),
          "routing_strategy" -> "tree"
        ),
        "ClientCredentialService" -> Json.obj(
          "domain"         -> "*",
          "expiration"     -> 1.hour.toMillis,
          "defaultKeyPair" -> Cert.OtoroshiJwtSigning,
          "secure"         -> true
        )
      )
    )
  )

  lazy val backOfficeGroup = ServiceGroup(
    id = backOfficeGroupId,
    name = "Otoroshi Admin Api group",
    metadata = Map.empty
  )

  lazy val backOfficeApiKey = ApiKey(
    backOfficeApiKeyClientId,
    backOfficeApiKeyClientSecret,
    "Otoroshi Backoffice ApiKey",
    "The apikey use by the Otoroshi UI",
    Seq(ServiceGroupIdentifier(backOfficeGroupId)),
    validUntil = None,
    throttlingQuota = 10000
  )

  private lazy val backOfficeDescriptorHostHeader: String = s"$adminApiSubDomain.$domain"

  lazy val adminHosts: Seq[String] =
    adminApiExposedDomains ++ adminApiAdditionalExposedDomain :+ s"${adminApiExposedSubDomain}.${domain}"

  lazy val backOfficeServiceDescriptor = ServiceDescriptor(
    id = backOfficeServiceId,
    groups = Seq(backOfficeGroupId),
    name = "otoroshi-admin-api",
    env = "prod",
    subdomain = adminApiExposedSubDomain,
    hosts = adminHosts,
    domain = domain,
    targets = Seq(
      Target(
        host = if (adminApiProxyUseLocal) s"127.0.0.1:$port" else s"$adminApiHost:$exposedHttpPort",
        scheme = if (adminApiProxyHttps) "https" else "http"
      )
    ),
    detectApiKeySooner = false,
    redirectToLocal = false,
    localHost = s"127.0.0.1:$port",
    forceHttps = false,
    additionalHeaders = Map(
      "Host" -> backOfficeDescriptorHostHeader
    ),
    publicPatterns = Seq("/health", "/metrics"),
    allowHttp10 = true,
    letsEncrypt = false,
    removeHeadersIn = Seq.empty,
    removeHeadersOut = Seq.empty,
    accessValidator = AccessValidatorRef(),
    missingOnlyHeadersIn = Map.empty,
    missingOnlyHeadersOut = Map.empty,
    stripPath = true,
    useAkkaHttpClient = false
  )

  lazy val backofficeRoute =
    NgRoute.fromServiceDescriptor(backOfficeServiceDescriptor, false)(otoroshiExecutionContext, this)

  lazy val backOfficeDescriptor = RoutingInfo(
    id = backofficeRoute.id,
    name = backofficeRoute.name
  )

  lazy val otoroshiVersion    = "16.22.0"
  lazy val otoroshiVersionSem = Version(otoroshiVersion)
  lazy val checkForUpdates    = configuration.getOptionalWithFileSupport[Boolean]("app.checkForUpdates").getOrElse(true)

  lazy val jmxEnabled = configuration.getOptionalWithFileSupport[Boolean]("otoroshi.jmx.enabled").getOrElse(false)
  lazy val jmxPort    = configuration.getOptionalWithFileSupport[Int]("otoroshi.jmx.port").getOrElse(16000)

  if (jmxEnabled) {
    LocateRegistry.createRegistry(jmxPort)
    val mbs = ManagementFactory.getPlatformMBeanServer
    val url = new JMXServiceURL(s"service:jmx:rmi://localhost/jndi/rmi://localhost:$jmxPort/jmxrmi")
    val svr = JMXConnectorServerFactory.newJMXConnectorServer(url, null, mbs)
    svr.start()
    logger.info(s"Starting JMX remote server at 127.0.0.1:$jmxPort")
  }

  val ocspResponder = OcspResponder(this, otoroshiExecutionContext)

  def beforeListening(): Future[Unit] = {
    ().vfuture
  }

  def afterListening(): Future[Unit] = {
    tunnelManager.start()
    // TODO: remove timeout
    timeout(300.millis).andThen { case _ =>
      tunnelAgent.start()
    }(otoroshiExecutionContext)
    ().vfuture
  }

  private def setupLoggers(): Unit = {
    val loggerContext                          = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext]
    val loggersAndLevel: Seq[(String, String)] = configuration
      .getOptionalWithFileSupport[Configuration]("otoroshi.loggers")
      .map { loggers =>
        loggers.entrySet.map { case (key, value) =>
          (key, value.unwrapped().asInstanceOf[String])
        }.toSeq
      }
      .getOrElse(Seq.empty) ++ {
      sys.env.toSeq
        .filter {
          case (key, _) if key.toLowerCase().startsWith("otoroshi_loggers_") => true
          case _                                                             => false
        }
        .map { case (key, value) =>
          (key.toLowerCase.replace("otoroshi_loggers_", "").replaceAll("_", "-"), value)
        }
    }
    loggersAndLevel.foreach { case (logName, level) =>
      logger.info(s"Setting logger $logName to level $level")
      val _logger = loggerContext.getLogger(logName)
      _logger.setLevel(Level.valueOf(level))
    }
  }

  lazy val javaVersion = PlatformDependent.javaVersion()

  lazy val theJavaVersion = (for {
    version <- Option(System.getProperty("java.version"))
    vendor  <- Option(System.getProperty("java.vendor"))
  } yield JavaVersion(version, vendor)).getOrElse(JavaVersion.default)

  lazy val os = (for {
    name    <- Option(System.getProperty("os.name"))
    arch    <- Option(System.getProperty("os.arch"))
    version <- Option(System.getProperty("os.version"))
  } yield OS(name, version, arch)).getOrElse(OS.default)

  timeout(300.millis).andThen { case _ =>
    implicit val ec = otoroshiExecutionContext // internalActorSystem.dispatcher

    setupLoggers()

    DynamicSSLEngineProvider.setCurrentEnv(this)

    clusterAgent.warnAboutHttpLeaderUrls()
    if (clusterConfig.mode == ClusterMode.Leader) {
      logger.info(s"Running Otoroshi Leader agent !")
      clusterLeaderAgent.start()
    } else if (clusterConfig.mode == ClusterMode.Worker) {
      logger.info(s"Running Otoroshi Worker agent !")
      clusterAgent.startF()
    }

    val modernTlsProtocols: Seq[String] =
      configuration.getOptionalWithFileSupport[Seq[String]]("otoroshi.ssl.modernProtocols").getOrElse(Seq.empty)
    val protocolsJDK11: Seq[String]     =
      configuration.getOptionalWithFileSupport[Seq[String]]("otoroshi.ssl.protocolsJDK11").getOrElse(Seq.empty)
    val protocolsJDK8: Seq[String]      =
      configuration.getOptionalWithFileSupport[Seq[String]]("otoroshi.ssl.protocolsJDK8").getOrElse(Seq.empty)

    val cipherSuitesJDK8: Seq[String]      =
      configuration.getOptionalWithFileSupport[Seq[String]]("otoroshi.ssl.cipherSuitesJDK8").getOrElse(Seq.empty)
    val cipherSuitesJDK11: Seq[String]     =
      configuration.getOptionalWithFileSupport[Seq[String]]("otoroshi.ssl.cipherSuitesJDK11").getOrElse(Seq.empty)
    val cipherSuitesJDK11Plus: Seq[String] =
      configuration.getOptionalWithFileSupport[Seq[String]]("otoroshi.ssl.cipherSuitesJDK11Plus").getOrElse(Seq.empty)

    configuration
      .getOptionalWithFileSupport[Seq[String]]("otoroshi.ssl.cipherSuites")
      .filterNot(_.isEmpty)
      .foreach { s =>
        if (!(s == cipherSuitesJDK8 || s == cipherSuitesJDK11 || s == cipherSuitesJDK11Plus)) {
          DynamicSSLEngineProvider.logger.warn(s"Using custom SSL cipher suites: ${s.mkString(", ")}")
        }
      }

    configuration
      .getOptionalWithFileSupport[Seq[String]]("otoroshi.ssl.protocols")
      .filterNot(_.isEmpty)
      .foreach { p =>
        if (!(p == protocolsJDK11 || p == protocolsJDK8 || p == modernTlsProtocols)) {
          DynamicSSLEngineProvider.logger.warn(s"Using custom SSL protocols: ${p.mkString(", ")}")
        }
      }

    configuration.betterHas("app.importFrom")
    datastores.globalConfigDataStore
      .isOtoroshiEmpty()
      .andThen {
        case Success(true) if clusterConfig.mode == ClusterMode.Worker  => {
          logger.info(s"The main datastore seems to be empty, registering default config.")
          defaultConfig.save()(ec, this)
        }
        case Success(true) if clusterConfig.mode != ClusterMode.Worker  => {
          logger.info(s"The main datastore seems to be empty, registering some basic services")
          val login                          =
            configuration.getOptionalWithFileSupport[String]("app.adminLogin").getOrElse("[email protected]")
          val password                       =
            configuration.getOptionalWithFileSupport[String]("app.adminPassword").getOrElse(IdGenerator.token(32))
          val headers: Seq[(String, String)] = configuration
            .getOptionalWithFileSupport[Seq[String]]("app.importFromHeaders")
            .map(headers => headers.toSeq.map(h => h.split(":")).map(h => (h(0).trim, h(1).trim)))
            .getOrElse(Seq.empty[(String, String)])
          if (configuration.betterHas("app.importFrom")) {
            configuration.getOptionalWithFileSupport[String]("app.importFrom") match {
              case Some(url) if url.startsWith("http://") || url.startsWith("https://") => {
                logger.info(s"Importing from URL: $url")
                _internalClient.url(url).withHttpHeaders(headers: _*).get().fast.map { resp =>
                  val json = resp.json.as[JsObject]
                  datastores.globalConfigDataStore
                    .fullImport(json)(ec, this)
                    .andThen {
                      case Success(_) => logger.info("Successful import !")
                      case Failure(e) => logger.error("Error while importing initial data !", e)
                    }(ec)
                }
              }
              case Some(path)                                                           => {
                logger.info(s"Importing from: $path")
                val source = Source.fromFile(path).getLines().mkString("\n")
                val json   = Json.parse(source).as[JsObject]
                datastores.globalConfigDataStore
                  .fullImport(json)(ec, this)
                  .andThen {
                    case Success(_) => logger.info("Successful import !")
                    case Failure(e) => logger.error("Error while importing initial data !", e)
                  }(ec)
              }
            }
          } else {
            configuration.getOptionalWithFileSupport[play.api.Configuration]("app.initialData") match {
              case Some(obj) => {
                val importJson = Json
                  .parse(
                    obj.underlying
                      .root()
                      .render(ConfigRenderOptions.concise())
                  )
                  .as[JsObject]
                logger.info(s"Importing from config file")
                datastores.globalConfigDataStore
                  .fullImport(importJson)(ec, this)
                  .andThen {
                    case Success(_) => logger.info("Successful import !")
                    case Failure(e) => logger.error("Error while importing initial data !", e)
                  }(ec)
              }
              case _         => {

                val defaultGroup       =
                  ServiceGroup("default", "default-group", "The default service group", Seq.empty, Map.empty)
                val defaultGroupApiKey = ApiKey(
                  IdGenerator.token(16),
                  IdGenerator.token(64),
                  "default-apikey",
                  "the default apikey",
                  Seq(ServiceGroupIdentifier("default")),
                  validUntil = None
                )

                val admin = SimpleOtoroshiAdmin(
                  username = login,
                  password = BCrypt.hashpw(password, BCrypt.gensalt()),
                  label = "Otoroshi Admin",
                  createdAt = DateTime.now(),
                  typ = OtoroshiAdminType.SimpleAdmin,
                  metadata = Map.empty,
                  rights = UserRights.varargs(UserRight(TenantAccess("*"), Seq(TeamAccess("*")))),
                  location = EntityLocation(),
                  adminEntityValidators = Map.empty
                )

                val defaultTenant = Tenant(
                  id = TenantId("default"),
                  name = "Default organization",
                  description = "The default organization",
                  metadata = Map.empty[String, String]
                )

                val defaultTeam = Team(
                  id = TeamId("default"),
                  tenant = TenantId("default"),
                  name = "Default Team",
                  description = "The default Team of the default organization",
                  metadata = Map.empty[String, String]
                )

                val baseExport = OtoroshiExport(
                  config = defaultConfig,
                  descs = if (defaultConfig.initWithNewEngine) Seq.empty else Seq(backOfficeServiceDescriptor),
                  routes = if (defaultConfig.initWithNewEngine) Seq(backofficeRoute) else Seq.empty,
                  apikeys = Seq(backOfficeApiKey, defaultGroupApiKey),
                  groups = Seq(backOfficeGroup, defaultGroup),
                  simpleAdmins = Seq(admin),
                  teams = Seq(defaultTeam),
                  tenants = Seq(defaultTenant),
                  extensions = Map.empty
                )

                val initialCustomization = configuration
                  .getOptionalWithFileSupport[String]("app.initialCustomization")
                  .map(Json.parse)
                  .map(_.asObject)
                  .orElse(
                    configuration
                      .getOptionalWithFileSupport[play.api.Configuration]("app.initialCustomization")
                      .map(v => Json.parse(v.underlying.root().render(ConfigRenderOptions.concise())).asObject)
                  )
                  .getOrElse(Json.obj())

                val finalConfig = baseExport.customizeWith(initialCustomization)(this)

                logger.info(
                  s"You can log into the Otoroshi admin console with the following credentials: $login / $password"
                )

                datastores.globalConfigDataStore.fullImport(finalConfig.json)(ec, this)
              }
            }
          }
        }
        case Success(false) if clusterConfig.mode != ClusterMode.Worker => {
          datastores.serviceDescriptorDataStore.findById(backOfficeServiceId)(ec, this).flatMap {
            case Some(adminService) if !adminApiExposedDomains.forall(d => adminService.hosts.contains(d))    => {
              adminService
                .copy(
                  hosts =
                    (adminService.hosts ++ adminApiAdditionalExposedDomain ++ adminApiExposedDomains :+ s"${adminApiExposedSubDomain}.${domain}").distinct,
                  additionalHeaders = Map("Host" -> backOfficeDescriptorHostHeader)
                )
                .save()(ec, this)
            }
            case Some(adminService) if !adminService.hosts.contains(s"${adminApiExposedSubDomain}.${domain}") => {
              adminService
                .copy(
                  hosts =
                    (adminService.hosts ++ adminApiAdditionalExposedDomain ++ adminApiExposedDomains :+ s"${adminApiExposedSubDomain}.${domain}").distinct,
                  additionalHeaders = Map("Host" -> backOfficeDescriptorHostHeader)
                )
                .save()(ec, this)
            }
            case Some(adminService)
                if !adminService.additionalHeaders
                  .exists(t => t._1 == "Host" && t._2 == backOfficeDescriptorHostHeader) => {
              adminService
                .copy(
                  hosts =
                    (adminService.hosts ++ adminApiAdditionalExposedDomain ++ adminApiExposedDomains :+ s"${adminApiExposedSubDomain}.${domain}").distinct,
                  additionalHeaders = Map("Host" -> backOfficeDescriptorHostHeader)
                )
                .save()(ec, this)
            }
            case Some(adminService)                                                                           => {
              ().future
            }
            case _                                                                                            => ().future
          }
        }
      }
      .map { _ =>
        datastores.serviceDescriptorDataStore.findById(backOfficeServiceId)(ec, this).map {
          case Some(s) if !s.publicPatterns.contains("/health")  =>
            logger.info("Updating BackOffice service to handle health check ...")
            s.copy(publicPatterns = s.publicPatterns :+ "/health").save()(ec, this)
          case Some(s) if !s.publicPatterns.contains("/metrics") =>
            logger.info("Updating BackOffice service to handle metrics ...")
            s.copy(publicPatterns = s.publicPatterns :+ "/metrics").save()(ec, this)
          case _                                                 =>
        }
      }

    {
      datastores.tenantDataStore.findById("default")(ec, this).map {
        case None    =>
          datastores.tenantDataStore.set(
            Tenant(
              id = TenantId("default"),
              name = "Default organization",
              description = "Default organization created for any otoroshi instance",
              metadata = Map.empty
            )
          )(ec, this)
        case Some(_) =>
      }
      datastores.teamDataStore.findById("default")(ec, this).map {
        case None    =>
          datastores.teamDataStore.set(
            Team(
              id = TeamId("default"),
              tenant = TenantId("default"),
              name = "Default team",
              description = "Default team created for any otoroshi instance",
              metadata = Map.empty
            )
          )(ec, this)
        case Some(_) =>
      }
    }
    ()
  }(otoroshiExecutionContext)

  timeout(1000.millis).andThen { case _ =>
    jobManager.start()
    otoroshiEventsActor ! StartExporters
  }(otoroshiExecutionContext)

  timeout(5000.millis).andThen {
    case _ if clusterConfig.mode != ClusterMode.Worker => {
      implicit val ec = otoroshiExecutionContext
      implicit val ev = this
      for {
        _ <- datastores.globalConfigDataStore.migrate()
      } yield ()
    }
  }(otoroshiExecutionContext)

  /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

  lazy val sessionDomain = configuration.getOptionalWithFileSupport[String]("play.http.session.domain").get
  lazy val playSecret    = configuration.getOptionalWithFileSupport[String]("play.http.secret.key").get

  def sign(message: String): String =
    scala.util.Try {
      val mac = javax.crypto.Mac.getInstance("HmacSHA256")
      mac.init(new javax.crypto.spec.SecretKeySpec(playSecret.getBytes("utf-8"), "HmacSHA256"))
      org.apache.commons.codec.binary.Hex.encodeHexString(mac.doFinal(message.getBytes("utf-8")))
    } match {
      case scala.util.Success(s) => s
      case scala.util.Failure(e) => {
        logger.error(s"Error while signing: ${message}", e)
        throw e
      }
    }

  def extractPrivateSessionId(cookie: play.api.mvc.Cookie): Option[String] = {
    cookie.value.split("::").toList match {
      case signature :: value :: Nil if sign(value) == signature => Some(value)
      case _                                                     => None
    }
  }

  def extractPrivateSessionIdFromString(value: String): Option[String] = {
    value.split("::").toList match {
      case signature :: value :: Nil if sign(value) == signature => Some(value)
      case _                                                     => None
    }
  }

  def signPrivateSessionId(id: String): String = {
    val signature = sign(id)
    s"$signature::$id"
  }

  lazy val encryptionKey = new SecretKeySpec(otoroshiSecret.padTo(16, "0").mkString("").take(16).getBytes, "AES")
  lazy val sha256Alg     = Algorithm.HMAC256(otoroshiSecret)
  lazy val sha512Alg     = Algorithm.HMAC512(otoroshiSecret)

  def encryptedJwt(user: PrivateAppsUser): String = {
    val added   = clusterConfig.worker.state.pollEvery.millis.toSeconds.toInt * 3
    val session = aesEncrypt(Json.stringify(user.json))
    JWT
      .create()
      .withIssuer("otoroshi")
      .withIssuedAt(DateTime.now().toDate)
      .withExpiresAt(DateTime.now().plusSeconds(added).toDate)
      .withClaim("sessid", user.randomId)
      .withClaim("sess", session)
      .sign(sha512Alg)
  }

  def aesEncrypt(content: String): String = {
    val cipher: Cipher = Cipher.getInstance("AES")
    cipher.init(Cipher.ENCRYPT_MODE, encryptionKey)
    val bytes          = cipher.doFinal(content.getBytes)
    java.util.Base64.getUrlEncoder.encodeToString(bytes)
  }

  def aesDecrypt(content: String): String = {
    val bytes          = java.util.Base64.getUrlDecoder.decode(content)
    val cipher: Cipher = Cipher.getInstance("AES")
    cipher.init(Cipher.DECRYPT_MODE, encryptionKey)
    new String(cipher.doFinal(bytes))
  }

  def createPrivateSessionCookies(
      host: String,
      id: String,
      desc: ServiceDescriptor,
      authConfig: AuthModuleConfig,
      userOpt: Option[PrivateAppsUser]
  ): Seq[play.api.mvc.Cookie] = {
    createPrivateSessionCookiesWithSuffix(
      host,
      id,
      authConfig.cookieSuffix(desc),
      authConfig.sessionMaxAge,
      authConfig.sessionCookieValues,
      userOpt
    )
  }

  def createPrivateSessionCookiesWithSuffix(
      host: String,
      id: String,
      suffix: String,
      sessionMaxAge: Int,
      sessionCookieValues: SessionCookieValues,
      userOpt: Option[PrivateAppsUser]
  ): Seq[play.api.mvc.Cookie] = {
    val tmpSessionAge = clusterConfig.worker.state.pollEvery.millis.toSeconds.toInt * 3
    if (host.endsWith(sessionDomain)) {
      Seq(
        play.api.mvc.Cookie(
          name = "oto-papps-" + suffix,
          value = signPrivateSessionId(id),
          maxAge = Some(sessionMaxAge),
          path = "/",
          domain = Some(sessionDomain),
          httpOnly = sessionCookieValues.httpOnly,
          secure = sessionCookieValues.secure
        )
      ) ++ userOpt.map { user =>
        play.api.mvc.Cookie(
          name = "oto-papps-tsess-" + suffix,
          value = encryptedJwt(user),
          maxAge = Some(tmpSessionAge),
          path = "/",
          domain = Some(sessionDomain),
          httpOnly = sessionCookieValues.httpOnly,
          secure = sessionCookieValues.secure
        )
      }
    } else {
      Seq(
        play.api.mvc.Cookie(
          name = "oto-papps-" + suffix,
          value = signPrivateSessionId(id),
          maxAge = Some(sessionMaxAge),
          path = "/",
          domain = Some(host),
          httpOnly = sessionCookieValues.httpOnly,
          secure = sessionCookieValues.secure
        ),
        play.api.mvc.Cookie(
          name = "oto-papps-" + suffix,
          value = signPrivateSessionId(id),
          maxAge = Some(sessionMaxAge),
          path = "/",
          domain = Some(sessionDomain),
          httpOnly = sessionCookieValues.httpOnly,
          secure = sessionCookieValues.secure
        )
      ) ++ userOpt.map { user =>
        play.api.mvc.Cookie(
          name = "oto-papps-tsess-" + suffix,
          value = encryptedJwt(user),
          maxAge = Some(tmpSessionAge),
          path = "/",
          domain = Some(host),
          httpOnly = sessionCookieValues.httpOnly,
          secure = sessionCookieValues.secure
        )
      } ++ userOpt.map { user =>
        play.api.mvc.Cookie(
          name = "oto-papps-tsess-" + suffix,
          value = encryptedJwt(user),
          maxAge = Some(tmpSessionAge),
          path = "/",
          domain = Some(sessionDomain),
          httpOnly = sessionCookieValues.httpOnly,
          secure = sessionCookieValues.secure
        )
      }
    }
  }

  def removePrivateSessionCookies(
      host: String,
      desc: ServiceDescriptor,
      authConfig: AuthModuleConfig
  ): Seq[play.api.mvc.DiscardingCookie] = {
    removePrivateSessionCookiesWithSuffix(host, authConfig.cookieSuffix(desc))
  }

  def removePrivateSessionCookiesWithSuffix(host: String, suffix: String): Seq[play.api.mvc.DiscardingCookie] =
    Seq(
      play.api.mvc.DiscardingCookie(
        name = "oto-papps-" + suffix,
        path = "/",
        domain = Some(host)
      ),
      play.api.mvc.DiscardingCookie(
        name = "oto-papps-" + suffix,
        path = "/",
        domain = Some(sessionDomain)
      )
    )
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy