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

script.script.scala Maven / Gradle / Ivy

package otoroshi.script

import akka.Done
import akka.actor.Cancellable
import akka.http.scaladsl.model.Uri
import akka.http.scaladsl.util.FastFuture
import akka.stream.Materializer
import akka.stream.scaladsl.{Sink, Source}
import akka.util.ByteString
import com.google.common.hash.Hashing
import io.github.classgraph.ClassgraphUtils
import otoroshi.auth.AuthModule
import otoroshi.env.Env
import otoroshi.events._
import otoroshi.gateway.GwError
import otoroshi.models._
import otoroshi.next.extensions.AdminExtension
import otoroshi.next.plugins.api._
import otoroshi.security.{IdGenerator, OtoroshiClaim}
import otoroshi.storage.{BasicStore, RedisLike, RedisLikeStore}
import otoroshi.utils.cache.types.UnboundedTrieMap
import otoroshi.utils.config.ConfigUtils
import otoroshi.utils.syntax.implicits._
import otoroshi.utils.{SchedulerHelper, TypedMap}
import play.api.Logger
import play.api.libs.json._
import play.api.libs.ws.{DefaultWSCookie, WSCookie}
import play.api.mvc._

import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.security.cert.X509Certificate
import java.util.concurrent.Executors
import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference}
import javax.script._
import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future, Promise}
import scala.util.{Failure, Success, Try}

sealed trait PluginType {
  def name: String
}

object PluginType {
  object AppType             extends PluginType {
    def name: String = "app"
  }
  object TransformerType     extends PluginType {
    def name: String = "transformer"
  }
  object AccessValidatorType extends PluginType {
    def name: String = "validator"
  }
  object PreRoutingType      extends PluginType {
    def name: String = "preroute"
  }
  object RequestSinkType     extends PluginType {
    def name: String = "sink"
  }
  object EventListenerType   extends PluginType {
    def name: String = "listener"
  }
  object JobType             extends PluginType {
    def name: String = "job"
  }
  object DataExporterType    extends PluginType {
    def name: String = "exporter"
  }
  object TunnelHandlerType   extends PluginType {
    def name: String = "tunnel-handler"
  }
  object RequestHandlerType  extends PluginType {
    def name: String = "request-handler"
  }
  object CompositeType       extends PluginType {
    def name: String = "composite"
  }
}

trait StartableAndStoppable {
  val funit: Future[Unit]                                         = FastFuture.successful(())
  def startWithPluginId(pluginId: String, env: Env): Future[Unit] = start(env)
  def start(env: Env): Future[Unit]                               = FastFuture.successful(())
  def stop(env: Env): Future[Unit]                                = FastFuture.successful(())
}

trait NamedPlugin { self =>
  def deprecated: Boolean               = false
  def core: Boolean                     = false
  def pluginType: PluginType
  def internalName: String              = self.getClass.getName
  def name: String                      = self.getClass.getName
  def description: Option[String]       = None
  def documentation: Option[String]     = None
  def defaultConfig: Option[JsObject]   = None
  def configRoot: Option[String]        =
    defaultConfig match {
      case None                                   => None
      case Some(config) if config.value.size > 1  => None
      case Some(config) if config.value.isEmpty   => None
      case Some(config) if config.value.size == 1 => config.value.headOption.map(_._1)
    }
  def configSchema: Option[JsObject]    =
    defaultConfig.flatMap(c => configRoot.map(r => (c \ r).asOpt[JsObject].getOrElse(Json.obj()))) match {
      case None         => None
      case Some(config) => {
        def genSchema(jsobj: JsObject, prefix: String): JsObject = {
          jsobj.value.toSeq
            .map {
              case (key, JsString(_))              =>
                Json.obj(prefix + key -> Json.obj("type" -> "string", "props" -> Json.obj("label" -> (prefix + key))))
              case (key, JsNumber(_))              =>
                Json.obj(prefix + key -> Json.obj("type" -> "number", "props" -> Json.obj("label" -> (prefix + key))))
              case (key, JsBoolean(_))             =>
                Json.obj(prefix + key -> Json.obj("type" -> "bool", "props" -> Json.obj("label" -> (prefix + key))))
              case (key, JsArray(values))          => {
                if (values.isEmpty) {
                  Json.obj(prefix + key -> Json.obj("type" -> "array", "props" -> Json.obj("label" -> (prefix + key))))
                } else {
                  values.head match {
                    case JsNumber(_) =>
                      Json.obj(
                        prefix + key -> Json.obj(
                          "type"  -> "array",
                          "props" -> Json.obj("label" -> (prefix + key), "inputType" -> "number")
                        )
                      )
                    case _           =>
                      Json.obj(
                        prefix + key -> Json.obj("type" -> "array", "props" -> Json.obj("label" -> (prefix + key)))
                      )
                  }
                }
              }
              case ("mtlsConfig", a @ JsObject(_)) => genSchema(a, prefix + "mtlsConfig.")
              case ("mtls", a @ JsObject(_))       => genSchema(a, prefix + "mtls.")
              case ("filter", a @ JsObject(_))     => genSchema(a, prefix + "filter.")
              case ("not", a @ JsObject(_))        => genSchema(a, prefix + "not.")
              case (key, JsObject(_))              =>
                Json.obj(prefix + key -> Json.obj("type" -> "object", "props" -> Json.obj("label" -> (prefix + key))))
              case (key, JsNull)                   => Json.obj()
            }
            .foldLeft(Json.obj())(_ ++ _)
        }
        Some(genSchema(config, ""))
      }
    }
  def configFlow: Seq[String]           =
    defaultConfig.flatMap(c => configRoot.map(r => (c \ r).asOpt[JsObject].getOrElse(Json.obj()))) match {
      case None         => Seq.empty
      case Some(config) => {
        def genFlow(jsobj: JsObject, prefix: String): Seq[String] = {
          jsobj.value.toSeq.flatMap {
            case ("mtlsConfig", a @ JsObject(_)) => genFlow(a, prefix + "mtlsConfig.")
            case ("mtls", a @ JsObject(_))       => genFlow(a, prefix + "mtls.")
            case ("filter", a @ JsObject(_))     => genFlow(a, prefix + "filter.")
            case ("not", a @ JsObject(_))        => genFlow(a, prefix + "not.")
            case (key, value)                    => Seq(prefix + key)
          }
        }
        genFlow(config, "")
      }
    }
  def visibility: NgPluginVisibility    = NgPluginVisibility.NgUserLand
  def categories: Seq[NgPluginCategory] = Seq(NgPluginCategory.Custom)
  def steps: Seq[NgStep]                = Seq(
    if (this.isInstanceOf[RequestSink]) NgStep.Sink.some else None,
    if (this.isInstanceOf[PreRouting]) NgStep.PreRoute.some else None,
    if (this.isInstanceOf[AccessValidator]) NgStep.ValidateAccess.some else None,
    if (this.isInstanceOf[RequestTransformer]) NgStep.TransformRequest.some else None,
    if (this.isInstanceOf[RequestTransformer]) NgStep.TransformResponse.some else None
  ).collect { case Some(step) =>
    step
  }
  def jsonDescription(): JsObject       =
    Try {
      Json.obj(
        "name"          -> name,
        "description"   -> description.map(JsString.apply).getOrElse(JsNull).as[JsValue],
        "defaultConfig" -> defaultConfig.getOrElse(JsNull).as[JsValue],
        "configRoot"    -> configRoot.map(JsString.apply).getOrElse(JsNull).as[JsValue],
        "configSchema"  -> configSchema.getOrElse(JsNull).as[JsValue],
        "configFlow"    -> JsArray(configFlow.map(JsString.apply))
      )
    } match {
      case Failure(ex) => Json.obj()
      case Success(s)  => s
    }
}

case class HttpRequest(
    url: String,
    method: String,
    headers: Map[String, String],
    cookies: Seq[WSCookie] = Seq.empty[WSCookie],
    version: String,
    clientCertificateChain: Option[Seq[X509Certificate]],
    target: Option[Target],
    claims: OtoroshiClaim,
    body: () => Source[ByteString, _]
) {
  lazy val contentType: Option[String] = headers.get("Content-Type").orElse(headers.get("content-type"))
  lazy val host: String                = headers.get("Host").orElse(headers.get("host")).getOrElse("")
  lazy val uri: Uri                    = Uri(url)
  lazy val scheme: String              = uri.scheme
  lazy val authority: Uri.Authority    = uri.authority
  lazy val fragment: Option[String]    = uri.fragment
  lazy val path: String                = uri.path.toString()
  lazy val queryString: Option[String] = uri.rawQueryString
  lazy val relativeUri: String         = uri.toRelative.toString()
  def json: JsValue                    =
    Json.obj(
      "url"     -> url,
      "method"  -> method,
      "headers" -> headers,
      "version" -> version,
      "cookies" -> JsArray(
        cookies.map(c =>
          Json.obj(
            "name"     -> c.name,
            "value"    -> c.value,
            "domain"   -> c.domain.map(JsString.apply).getOrElse(JsNull).as[JsValue],
            "path"     -> c.path.map(JsString.apply).getOrElse(JsNull).as[JsValue],
            "maxAge"   -> c.maxAge.map(v => JsNumber(BigDecimal(v))).getOrElse(JsNull).as[JsValue],
            "secure"   -> c.secure,
            "httpOnly" -> c.httpOnly
          )
        )
      )
    )
}

case class HttpResponse(
    status: Int,
    headers: Map[String, String],
    cookies: Seq[WSCookie] = Seq.empty[WSCookie],
    body: () => Source[ByteString, _]
) {
  def json: JsValue =
    Json.obj(
      "status"  -> status,
      "headers" -> headers,
      "cookies" -> JsArray(
        cookies.map(c =>
          Json.obj(
            "name"     -> c.name,
            "value"    -> c.value,
            "domain"   -> c.domain.map(JsString.apply).getOrElse(JsNull).as[JsValue],
            "path"     -> c.path.map(JsString.apply).getOrElse(JsNull).as[JsValue],
            "maxAge"   -> c.maxAge.map(v => JsNumber(BigDecimal(v))).getOrElse(JsNull).as[JsValue],
            "secure"   -> c.secure,
            "httpOnly" -> c.httpOnly
          )
        )
      )
    )
}

trait ContextWithConfig {
  def index: Int
  def config: JsValue
  def globalConfig: JsValue
  def configExists(name: String): Boolean         =
    configForOpt(name).isDefined
  def configFor(name: String): JsValue            =
    configForOpt(name).getOrElse(Json.obj())
  def configForOpt(name: String): Option[JsValue] =
    (config \ name).asOpt[JsValue].orElse((globalConfig \ name).asOpt[JsValue])
  private def conf[A](prefix: String = "config-"): Option[JsValue] = {
    config match {
      case json: JsArray  => Option(json.value(index)).orElse((config \ s"$prefix$index").asOpt[JsValue])
      case json: JsObject => (json \ s"$prefix$index").asOpt[JsValue]
      case _              => None
    }
  }
  private def confAt[A](key: String, prefix: String = "config-")(implicit fjs: Reads[A]): Option[A] = {
    val conf = config match {
      case json: JsArray  => Option(json.value(index)).getOrElse((config \ s"$prefix$index").as[JsValue])
      case json: JsObject => (json \ s"$prefix$index").as[JsValue]
      case _              => Json.obj()
    }
    (conf \ key).asOpt[A]
  }
}

sealed trait TransformerContext extends ContextWithConfig {
  def index: Int
  def snowflake: String
  def descriptor: ServiceDescriptor
  def apikey: Option[ApiKey]
  def user: Option[PrivateAppsUser]
  def request: RequestHeader
  def config: JsValue
  def attrs: TypedMap
  def globalConfig: JsValue
  private def conf[A](prefix: String = "config-"): Option[JsValue] = {
    config match {
      case json: JsArray  => Option(json.value(index)).orElse((config \ s"$prefix$index").asOpt[JsValue])
      case json: JsObject => (json \ s"$prefix$index").asOpt[JsValue]
      case _              => None
    }
  }
  private def confAt[A](key: String, prefix: String = "config-")(implicit fjs: Reads[A]): Option[A] = {
    val conf = config match {
      case json: JsArray  => Option(json.value(index)).getOrElse((config \ s"$prefix$index").as[JsValue])
      case json: JsObject => (json \ s"$prefix$index").as[JsValue]
      case _              => Json.obj()
    }
    (conf \ key).asOpt[A]
  }
}

case class BeforeRequestContext(
    index: Int,
    snowflake: String,
    descriptor: ServiceDescriptor,
    request: RequestHeader,
    config: JsValue,
    attrs: TypedMap,
    globalConfig: JsValue = Json.obj()
) extends ContextWithConfig {}

case class AfterRequestContext(
    index: Int,
    snowflake: String,
    descriptor: ServiceDescriptor,
    request: RequestHeader,
    config: JsValue,
    attrs: TypedMap,
    globalConfig: JsValue = Json.obj()
) extends ContextWithConfig {}

case class TransformerRequestContext(
    rawRequest: HttpRequest,
    otoroshiRequest: HttpRequest,
    index: Int,
    snowflake: String,
    descriptor: ServiceDescriptor,
    apikey: Option[ApiKey],
    user: Option[PrivateAppsUser],
    request: RequestHeader,
    config: JsValue,
    attrs: TypedMap,
    globalConfig: JsValue = Json.obj()
) extends TransformerContext {}

case class TransformerResponseContext(
    rawResponse: HttpResponse,
    otoroshiResponse: HttpResponse,
    index: Int,
    snowflake: String,
    descriptor: ServiceDescriptor,
    apikey: Option[ApiKey],
    user: Option[PrivateAppsUser],
    request: RequestHeader,
    config: JsValue,
    attrs: TypedMap,
    globalConfig: JsValue = Json.obj()
) extends TransformerContext {}

case class TransformerRequestBodyContext(
    rawRequest: HttpRequest,
    otoroshiRequest: HttpRequest,
    body: Source[ByteString, Any],
    index: Int,
    snowflake: String,
    descriptor: ServiceDescriptor,
    apikey: Option[ApiKey],
    user: Option[PrivateAppsUser],
    request: RequestHeader,
    config: JsValue,
    attrs: TypedMap,
    globalConfig: JsValue = Json.obj()
) extends TransformerContext {}

case class TransformerResponseBodyContext(
    rawResponse: HttpResponse,
    otoroshiResponse: HttpResponse,
    body: Source[ByteString, Any],
    index: Int,
    snowflake: String,
    descriptor: ServiceDescriptor,
    apikey: Option[ApiKey],
    user: Option[PrivateAppsUser],
    request: RequestHeader,
    config: JsValue,
    attrs: TypedMap,
    globalConfig: JsValue = Json.obj()
) extends TransformerContext {}

case class TransformerErrorContext(
    index: Int,
    snowflake: String,
    message: String,
    otoroshiResult: Result,
    otoroshiResponse: HttpResponse,
    request: RequestHeader,
    maybeCauseId: Option[String],
    callAttempts: Int,
    descriptor: ServiceDescriptor,
    apikey: Option[ApiKey],
    user: Option[PrivateAppsUser],
    config: JsValue,
    globalConfig: JsValue = Json.obj(),
    attrs: TypedMap
) extends TransformerContext {}

trait RequestTransformer extends StartableAndStoppable with NamedPlugin with InternalEventListener {

  def pluginType: PluginType = PluginType.TransformerType

  def beforeRequest(
      context: BeforeRequestContext
  )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Unit] = {
    FastFuture.successful(())
  }

  def afterRequest(
      context: AfterRequestContext
  )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Unit] = {
    FastFuture.successful(())
  }

  def transformErrorWithCtx(
      context: TransformerErrorContext
  )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Result] = {
    FastFuture.successful(context.otoroshiResult)
  }

  def transformRequestWithCtx(
      context: TransformerRequestContext
  )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Either[Result, HttpRequest]] = {
    transformRequest(
      context.snowflake,
      context.rawRequest,
      context.otoroshiRequest,
      context.descriptor,
      context.apikey,
      context.user
    )(env, ec, mat)
  }

  def transformResponseWithCtx(
      context: TransformerResponseContext
  )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Either[Result, HttpResponse]] = {
    transformResponse(
      context.snowflake,
      context.rawResponse,
      context.otoroshiResponse,
      context.descriptor,
      context.apikey,
      context.user
    )(env, ec, mat)
  }

  def transformRequestBodyWithCtx(
      context: TransformerRequestBodyContext
  )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Source[ByteString, _] = {
    transformRequestBody(
      context.snowflake,
      context.otoroshiRequest.body.apply(),
      context.rawRequest,
      context.otoroshiRequest,
      context.descriptor,
      context.apikey,
      context.user
    )(env, ec, mat)
  }

  def transformResponseBodyWithCtx(
      context: TransformerResponseBodyContext
  )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Source[ByteString, _] = {
    transformResponseBody(
      context.snowflake,
      context.otoroshiResponse.body.apply(),
      context.rawResponse,
      context.otoroshiResponse,
      context.descriptor,
      context.apikey,
      context.user
    )(env, ec, mat)
  }

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

  def transformRequestSync(
      snowflake: String,
      rawRequest: HttpRequest,
      otoroshiRequest: HttpRequest,
      desc: ServiceDescriptor,
      apiKey: Option[ApiKey] = None,
      user: Option[PrivateAppsUser] = None
  )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Either[Result, HttpRequest] = {
    Right(otoroshiRequest)
  }

  def transformRequest(
      snowflake: String,
      rawRequest: HttpRequest,
      otoroshiRequest: HttpRequest,
      desc: ServiceDescriptor,
      apiKey: Option[ApiKey] = None,
      user: Option[PrivateAppsUser] = None
  )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Either[Result, HttpRequest]] = {
    FastFuture.successful(
      transformRequestSync(snowflake, rawRequest, otoroshiRequest, desc, apiKey, user)(env, ec, mat)
    )
  }

  def transformResponseSync(
      snowflake: String,
      rawResponse: HttpResponse,
      otoroshiResponse: HttpResponse,
      desc: ServiceDescriptor,
      apiKey: Option[ApiKey] = None,
      user: Option[PrivateAppsUser] = None
  )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Either[Result, HttpResponse] = {
    Right(otoroshiResponse)
  }

  def transformResponse(
      snowflake: String,
      rawResponse: HttpResponse,
      otoroshiResponse: HttpResponse,
      desc: ServiceDescriptor,
      apiKey: Option[ApiKey] = None,
      user: Option[PrivateAppsUser] = None
  )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Either[Result, HttpResponse]] = {
    FastFuture.successful(
      transformResponseSync(snowflake, rawResponse, otoroshiResponse, desc, apiKey, user)(env, ec, mat)
    )
  }

  def transformRequestBody(
      snowflake: String,
      body: Source[ByteString, _],
      rawRequest: HttpRequest,
      otoroshiRequest: HttpRequest,
      desc: ServiceDescriptor,
      apiKey: Option[ApiKey] = None,
      user: Option[PrivateAppsUser] = None
  )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Source[ByteString, _] = {
    body
  }

  def transformResponseBody(
      snowflake: String,
      body: Source[ByteString, _],
      rawResponse: HttpResponse,
      otoroshiResponse: HttpResponse,
      desc: ServiceDescriptor,
      apiKey: Option[ApiKey] = None,
      user: Option[PrivateAppsUser] = None
  )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Source[ByteString, _] = {
    body
  }
}

object DefaultRequestTransformer extends RequestTransformer {
  override def visibility: NgPluginVisibility    = NgPluginVisibility.NgInternal
  override def categories: Seq[NgPluginCategory] = Seq.empty
  override def steps: Seq[NgStep]                = Seq.empty
}

object CompilingRequestTransformer extends RequestTransformer {

  override def visibility: NgPluginVisibility    = NgPluginVisibility.NgInternal
  override def categories: Seq[NgPluginCategory] = Seq.empty
  override def steps: Seq[NgStep]                = Seq.empty

  override def transformRequestWithCtx(
      ctx: TransformerRequestContext
  )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Either[Result, HttpRequest]] = {
    val accept = ctx.rawRequest.headers.get("Accept").getOrElse("text/html").split(",").toSeq.map(_.trim)
    ctx.attrs.put(otoroshi.plugins.Keys.GwErrorKey -> GwError("not ready yet, plugin is loading ..."))
    if (accept.contains("text/html")) { // in a browser
      Left(
        Results
          .ServiceUnavailable("

not ready yet, plugin is loading ...

") .as("text/html") .withHeaders( env.Headers.OtoroshiGatewayError -> "true", env.Headers.OtoroshiErrorMsg -> "not ready yet, plugin is loading ...", env.Headers.OtoroshiStateResp -> ctx.request.headers.get(env.Headers.OtoroshiState).getOrElse("--") ) ).future } else { Left( Results .ServiceUnavailable(Json.obj("error" -> "not ready yet, plugin is loading ...")) .withHeaders( env.Headers.OtoroshiGatewayError -> "true", env.Headers.OtoroshiErrorMsg -> "not ready yet, plugin is loading ...", env.Headers.OtoroshiStateResp -> ctx.request.headers.get(env.Headers.OtoroshiState).getOrElse("--") ) ).future } } } trait NanoApp extends RequestTransformer { override def pluginType: PluginType = PluginType.AppType private val awaitingRequests = new UnboundedTrieMap[String, Promise[Source[ByteString, _]]]() override def beforeRequest( ctx: BeforeRequestContext )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Unit] = { awaitingRequests.putIfAbsent(ctx.snowflake, Promise[Source[ByteString, _]]) funit } override def afterRequest( ctx: AfterRequestContext )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Unit] = { awaitingRequests.remove(ctx.snowflake) funit } override def transformRequestWithCtx( ctx: TransformerRequestContext )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Either[Result, HttpRequest]] = { awaitingRequests.get(ctx.snowflake).map { promise => val consumed = new AtomicBoolean(false) val bodySource: Source[ByteString, _] = Source .future(promise.future) .flatMapConcat(s => s) .alsoTo(Sink.onComplete { case _ => consumed.set(true) }) route(ctx.rawRequest, bodySource).map { r => if (!consumed.get()) bodySource.runWith(Sink.ignore) Left(r) } } getOrElse { FastFuture.successful( Left(Results.InternalServerError(Json.obj("error" -> s"no body promise found for ${ctx.snowflake}"))) ) } } override def transformRequestBodyWithCtx( ctx: TransformerRequestBodyContext )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Source[ByteString, _] = { awaitingRequests.get(ctx.snowflake).map(_.trySuccess(ctx.body)) ctx.body } def route( request: HttpRequest, body: Source[ByteString, _] )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Result] = { FastFuture.successful(routeSync(request, body)) } def routeSync( request: HttpRequest, body: Source[ByteString, _] )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Result = { Results.Ok(Json.obj("message" -> "Hello World!")) } } class ScriptCompiler(env: Env) { private val logger = Logger("otoroshi-script-compiler") private val scriptExec = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(2)) def compile(script: String): Future[Either[JsValue, AnyRef]] = { val start = System.currentTimeMillis() Future .apply { try { val engineManager = new ScriptEngineManager(env.environment.classLoader) val scriptEngine = engineManager.getEngineByName("scala") val engine = scriptEngine.asInstanceOf[ScriptEngine with Invocable] if (scriptEngine == null) { // dev mode Left( Json.obj( "line" -> 0, "column" -> 0, "file" -> "", "rawMessage" -> "", "message" -> "You are in dev mode, Scala script engine does not work inside sbt :(" ) ) } else { val ctx = new SimpleScriptContext val res = engine.eval(script, ctx) // .asInstanceOf[RequestTransformer] ctx.getErrorWriter.flush() ctx.getWriter.flush() Right(res) } } catch { case ex: ScriptException => val message = ex.getMessage.replace("in " + ex.getFileName, "") Left( Json.obj( "line" -> ex.getLineNumber, "column" -> ex.getColumnNumber, "file" -> ex.getFileName, "rawMessage" -> ex.getMessage, "message" -> message ) ) case ex: Throwable => logger.error(s"Compilation error", ex) Left( Json.obj( "line" -> 0, "column" -> 0, "file" -> "none", "rawMessage" -> ex.getMessage, "message" -> ex.getMessage ) ) } }(scriptExec) .andThen { case _ => if (logger.isDebugEnabled) logger.debug(s"Compilation process took ${(System.currentTimeMillis() - start).millis}") }(scriptExec) } } case class ScriptsState(compiling: Boolean, initialized: Boolean) { def json: JsValue = Json.obj( "compiling" -> compiling, "initialized" -> initialized ) } class ScriptManager(env: Env) { private implicit val ec = env.otoroshiExecutionContext private implicit val _env = env private val cpScriptExec = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(2)) private val logger = Logger("otoroshi-script-manager") private val updateRef = new AtomicReference[Cancellable]() private val firstScan = new AtomicBoolean(false) private val compiling = new UnboundedTrieMap[String, Unit]() private val cache = new UnboundedTrieMap[String, (String, PluginType, Any)]() private val cpCache = new UnboundedTrieMap[String, (PluginType, Any)]() private val cpTryCache = new UnboundedTrieMap[String, Unit]() private val listeningCpScripts = new AtomicReference[Seq[InternalEventListener]](Seq.empty) private val _firstPluginsSearchDone = new AtomicBoolean(false) private val _firstCompilationDone = new AtomicBoolean(false) def firstPluginsSearchDone(): Boolean = _firstPluginsSearchDone.get() def firstCompilationDone(): Boolean = _firstCompilationDone.get() val starting = System.currentTimeMillis() lazy val ( transformersNames, validatorsNames, preRouteNames, reqSinkNames, reqHandlerNames, listenerNames, jobNames, exporterNames, ngNames, adminExtensionNames, authModuleNames, tunnelHandlerNames ) = Try { import io.github.classgraph.ClassInfo import collection.JavaConverters._ val start = System.currentTimeMillis() val scanResult = env.openApiSchema.scanResult // val scanResult: ScanResult = new ClassGraph().addClassLoader(env.environment.classLoader).enableAllInfo.blacklistPackages( // "java.*", // "javax.*", // "aix.*", // "akka.*", // "cats.*", // "ch.qos.logback.*", // "com.auth0.*", // "com.blueconic.*", // "com.carrotsearch.*", // "com.codahale.*", // "com.cronutils.*", // "com.datastax.*", // "com.esri.*", // "com.fasterxml.jackson.*", // "com.github.benmanes.*", // "com.github.blemale.*", // "com.github.luben.*", // "com.google.*", // "com.jayway.*", // "com.jcabi.*", // "com.kenai.*", // "com.maxmind.*", // "com.nimbusds.*", // "com.risksense.*", // "com.scurrilous.*", // "com.sksamuel.*", // "com.squareup.*", // "com.sun.*", // "com.thoughtworks.*", // "com.twitter.*", // "com.typesafe.*", // "com.upokecenter.*", // "com.yubico.*", // "common.message.*", // "diffson.*", // "edu.umd.*", // "github.gphat.censorinus.*", // "google.*", // "groovy.*", // "groovyjarjarantlr.*", // "groovyjarjarasm.*", // "groovyjarjarcommonscli/*", // "groovyjarjarpicocli.*", // "groverconfig8491016507689653801.*", // "io.airlift.*", // "io.estatico.*", // "io.github.*", // "io.gsonfire.*", // "io.jsonwebtoken.*", // "io.kubernetes.*", // "io.lettuce.*", // "io.netty.*", // "io.prometheus.*", // "io.sundr.*", // "io.swagger.*", // "javax.*", // "jni.*", // "jnr.*", // "kafka.*", // "kaleidoscope.*", // "linux.*", // "macrocompat.*", // "mozilla.*", // "net.i2p.*", // "net.jcip.*", // "net.jpountz.*", // "net.minidev.*", // "net.objecthunter.*", // "nonapi.io.github.*", // "okhttp3.*", // "okio.*", // "org.apache.*", // "org.aspectj.*", // "org.bouncycastle.*", // "org.checkerframework.*", // "org.codehaus.*", // "org.eclipse.*", // "org.iq80.*", // "org.javatuples.*", // "org.joda.*", // "org.jose4j.*", // "org.json.*", // "org.mindrot.*", // "org.mortbay.*", // "org.objectweb.*", // "org.reactivestreams.*", // "org.shredzone.*", // "org.slf4j.*", // "org.xbill.*", // "org.xerial.*", // "org.yaml.*", // "play.*", // "public.*", // "reactor.*", // "redis.*", // "scala.*", // "templates.*", // "win.*", // ).scan if (logger.isDebugEnabled) logger.debug(s"classpath scanning in ${System.currentTimeMillis() - start} ms.") try { def predicate(c: ClassInfo): Boolean = { c.isInterface || ( c.getName == "otoroshi.script.DefaultRequestTransformer$" || c.getName == "otoroshi.script.CompilingRequestTransformer$" || c.getName == "otoroshi.script.CompilingValidator$" || c.getName == "otoroshi.script.CompilingPreRouting$" || c.getName == "otoroshi.script.CompilingRequestSink$" || c.getName == "otoroshi.script.CompilingOtoroshiEventListener$" || c.getName == "otoroshi.script.DefaultValidator$" || c.getName == "otoroshi.script.DefaultPreRouting$" || c.getName == "otoroshi.script.DefaultRequestSink$" || c.getName == "otoroshi.script.FailingPreRoute" || c.getName == "otoroshi.script.FailingPreRoute$" || c.getName == "otoroshi.script.DefaultOtoroshiEventListener$" || c.getName == "otoroshi.script.DefaultJob$" || c.getName == "otoroshi.script.CompilingJob$" || c.getName == "otoroshi.script.NanoApp" || c.getName == "otoroshi.script.NanoApp$" ) } val requestTransformers: Seq[String] = (scanResult.getSubclasses(classOf[RequestTransformer].getName).asScala ++ scanResult.getClassesImplementing(classOf[RequestTransformer].getName).asScala) .filterNot(predicate) .map(_.getName) val validators: Seq[String] = (scanResult.getSubclasses(classOf[AccessValidator].getName).asScala ++ scanResult.getClassesImplementing(classOf[AccessValidator].getName).asScala) .filterNot(predicate) .map(_.getName) val preRoutes: Seq[String] = (scanResult.getSubclasses(classOf[PreRouting].getName).asScala ++ scanResult.getClassesImplementing(classOf[PreRouting].getName).asScala).filterNot(predicate).map(_.getName) val reqSinks: Seq[String] = (scanResult.getSubclasses(classOf[RequestSink].getName).asScala ++ scanResult.getClassesImplementing(classOf[RequestSink].getName).asScala).filterNot(predicate).map(_.getName) val reqHandlers: Seq[String] = (scanResult.getSubclasses(classOf[RequestHandler].getName).asScala ++ scanResult.getClassesImplementing(classOf[RequestHandler].getName).asScala) .filterNot(predicate) .map(_.getName) val tunnelHandlers: Seq[String] = (scanResult.getSubclasses(classOf[NgTunnelHandler].getName).asScala ++ scanResult.getClassesImplementing(classOf[NgTunnelHandler].getName).asScala) .filterNot(predicate) .map(_.getName) val listenerNames: Seq[String] = (scanResult.getSubclasses(classOf[OtoroshiEventListener].getName).asScala ++ scanResult.getClassesImplementing(classOf[OtoroshiEventListener].getName).asScala) .filterNot(predicate) .map(_.getName) val jobNames: Seq[String] = (scanResult.getSubclasses(classOf[Job].getName).asScala ++ scanResult.getClassesImplementing(classOf[Job].getName).asScala) .filterNot(predicate) .map(_.getName) val customExporters: Seq[String] = (scanResult.getSubclasses(classOf[CustomDataExporter].getName).asScala ++ scanResult.getClassesImplementing(classOf[CustomDataExporter].getName).asScala) .filterNot(predicate) .map(_.getName) val ngPlugins: Seq[String] = (scanResult.getSubclasses(classOf[NgPlugin].getName).asScala ++ scanResult.getClassesImplementing(classOf[NgPlugin].getName).asScala) .filterNot(predicate) .map(_.getName) ++ (scanResult.getSubclasses(classOf[NgNamedPlugin].getName).asScala ++ scanResult.getClassesImplementing(classOf[NgNamedPlugin].getName).asScala) .filterNot(predicate) .map(_.getName) val adminExts: Seq[String] = (scanResult.getSubclasses(classOf[AdminExtension].getName).asScala ++ scanResult.getClassesImplementing(classOf[AdminExtension].getName).asScala) .filterNot(predicate) .map(_.getName) val authModuleConfigs: Seq[String] = (scanResult.getSubclasses(classOf[AuthModule].getName).asScala ++ scanResult.getClassesImplementing(classOf[AuthModule].getName).asScala) .filterNot(predicate) .map(_.getName) ( requestTransformers, validators, preRoutes, reqSinks, reqHandlers, listenerNames, jobNames, customExporters, ngPlugins, adminExts, authModuleConfigs, tunnelHandlers ) } catch { case e: Throwable => e.printStackTrace() ( Seq.empty[String], Seq.empty[String], Seq.empty[String], Seq.empty[String], Seq.empty[String], Seq.empty[String], Seq.empty[String], Seq.empty[String], Seq.empty[String], Seq.empty[String], Seq.empty[String], Seq.empty[String] ) } finally { if (scanResult != null) ClassgraphUtils.clear(scanResult) } } getOrElse ( Seq.empty[String], Seq.empty[String], Seq.empty[String], Seq.empty[String], Seq.empty[String], Seq.empty[String], Seq.empty[String], Seq.empty[String], Seq.empty[String], Seq.empty[String], Seq.empty[String], Seq.empty[String] ) lazy val authModules = authModuleNames.flatMap(ref => { Try(env.environment.classLoader.loadClass(ref)) .map(clazz => clazz.getDeclaredConstructor().newInstance()) match { case Success(tr) => tr.asInstanceOf[AuthModule].authConfig.some case Failure(_) => None } }) private val allPlugins = Seq( transformersNames, validatorsNames, preRouteNames, reqSinkNames, reqHandlerNames, listenerNames, jobNames, exporterNames, tunnelHandlerNames, ngNames ).flatten.distinct.sortWith((s1, s2) => s1.compareTo(s2) < 0) private val blackListedPlugins = allPlugins.filterNot(v => env.blacklistedPlugins.contains(v)) private val printPlugins = env.configuration.getOptionalWithFileSupport[Boolean]("otoroshi.plugins.print").getOrElse(false) logger.info(s"Found ${allPlugins.size} plugins in classpath (${System.currentTimeMillis() - starting} ms)") if (printPlugins) logger.info("\n\n" + allPlugins.map(s => s" - $s").mkString("\n") + "\n") if (printPlugins && blackListedPlugins.nonEmpty) logger.info("Blacklisted plugins") if (printPlugins && blackListedPlugins.nonEmpty) logger.info("\n\n" + blackListedPlugins.map(s => s" - $s").mkString("\n") + "\n") def start(): ScriptManager = { if (env.scriptingEnabled) { // valid updateRef.set( env.otoroshiScheduler.scheduleAtFixedRate(1.second, 10.second)( SchedulerHelper.runnable(updateScriptCache(firstScan.compareAndSet(false, true))) )( env.otoroshiExecutionContext ) ) } env.otoroshiScheduler.scheduleOnce(1.second)(initClasspathModules())(env.otoroshiExecutionContext) this } def stop(): Unit = { cache.foreach(s => Try { s._2._3.asInstanceOf[StartableAndStoppable].stop(env) s._2._3.asInstanceOf[InternalEventListener].stopEvent(env) } ) cpCache.foreach(s => Try { s._2._2.asInstanceOf[StartableAndStoppable].stop(env) s._2._2.asInstanceOf[InternalEventListener].stopEvent(env) } ) Option(updateRef.get()).foreach(_.cancel()) } def state(): Future[ScriptsState] = { env.datastores.scriptDataStore.findAll().map { scripts => val allCompiled = !scripts.forall(s => cache.contains(s.id)) val initial = if (scripts.isEmpty) true else _firstCompilationDone.get() ScriptsState(compiling.nonEmpty, initial) } } private def initClasspathModules(): Future[Unit] = { env.metrics.withTimerAsync("otoroshi.core.plugins.classpath-scanning-starting") { Future { logger.info("Finding and starting plugins ...") val start = System.currentTimeMillis() val plugins = (transformersNames ++ validatorsNames ++ preRouteNames) .map(c => env.scriptManager.getAnyScript[NamedPlugin](s"cp:$c")) .collect { case Right(plugin) => plugin } listeningCpScripts.set(plugins.collect { case listener: InternalEventListener if listener.listening => listener }) _firstPluginsSearchDone.compareAndSet(false, true) logger.info(s"Finding and starting plugins done in ${System.currentTimeMillis() - start} ms.") () }(cpScriptExec) } } private def compileAndUpdate(script: Script, oldScript: Option[InternalEventListener]): Future[Unit] = { compiling.putIfAbsent(script.id, ()) match { case Some(_) => FastFuture.successful(()) // do nothing as something is compiling case None => { if (logger.isDebugEnabled) logger.debug(s"Updating script ${script.name}") env.scriptCompiler.compile(script.code).map { case Left(err) => logger.error(s"Script ${script.name} with id ${script.id} does not compile: ${err}") compiling.remove(script.id) () case Right(trans) => { Try { oldScript.foreach { i => i.asInstanceOf[StartableAndStoppable].stop(env) i.stopEvent(env) } } Try { trans.asInstanceOf[StartableAndStoppable].startWithPluginId(script.id, env) trans.asInstanceOf[InternalEventListener].startEvent(script.id, env) } cache.put(script.id, (script.hash, script.`type`, trans)) compiling.remove(script.id) () } } } } } private def compileAndUpdateIfNeeded(script: Script): Future[Unit] = { (cache.get(script.id), compiling.get(script.id)) match { case (None, None) => compileAndUpdate(script, None) case (None, Some(_)) => FastFuture.successful(()) // do nothing as something is compiling case (Some(_), Some(_)) => FastFuture.successful(()) // do nothing as something is compiling case (Some(cs), None) if cs._1 != script.hash => compileAndUpdate(script, Some(cs._3.asInstanceOf[InternalEventListener])) case (Some(_), None) => FastFuture.successful(()) // do nothing as script has not changed from cache } } private def updateScriptCache(first: Boolean = false): Future[Unit] = { env.metrics.withTimerAsync("otoroshi.core.plugins.update-scripts") { if (logger.isDebugEnabled) logger.debug(s"updateScriptCache") if (first) logger.info("Compiling and starting scripts ...") val start = System.currentTimeMillis() env.datastores.scriptDataStore .findAll() .flatMap { scripts => val all: Future[Seq[Unit]] = Future.sequence(scripts.map(compileAndUpdateIfNeeded)) val ids = scripts.map(_.id) cache.keySet.filterNot(id => ids.contains(id)).foreach(id => cache.remove(id)) all.map(_ => ()) } .andThen { case _ if first => _firstCompilationDone.compareAndSet(false, true) logger.info(s"Compiling and starting scripts done in ${System.currentTimeMillis() - start} ms.") } } } def getScript(ref: String)(implicit ec: ExecutionContext): RequestTransformer = { getAnyScript[RequestTransformer](ref) match { case Left("compiling") => CompilingRequestTransformer case Left(_) => DefaultRequestTransformer case Right(any) => any.asInstanceOf[RequestTransformer] } } def getAnyScript[A](ref: String)(implicit ec: ExecutionContext): Either[String, A] = { if (env.blacklistedPlugins.contains(ref)) { Left(s"blacklisted plugin '${ref}'") } else { ref match { case r if r.startsWith("cp:") => cpTryCache.synchronized { if (!cpTryCache.contains(ref)) { Try(env.environment.classLoader.loadClass(r.replace("cp:", ""))) // .asSubclass(classOf[A])) .map(clazz => clazz.newInstance()) match { case Success(tr) => cpTryCache.put(ref, ()) val typ = tr.asInstanceOf[NamedPlugin].pluginType cpCache.put(ref, (typ, tr)) Try { tr.asInstanceOf[StartableAndStoppable].startWithPluginId(r, env) tr.asInstanceOf[InternalEventListener].startEvent(r, env) } case Failure(e) => e.printStackTrace() logger.error(s"Classpath script `$ref` does not exists ...") } } cpCache.get(ref).flatMap(a => Option(a._2)) match { case Some(script) => Right(script.asInstanceOf[A]) case None => Left("not-in-cache") } } case r if env.scriptingEnabled => { env.datastores.scriptDataStore.findById(ref).map { case Some(script) => compileAndUpdateIfNeeded(script) case None => logger.error(s"Script with id `$ref` does not exists ...") // do nothing as the script does not exists } cache.get(ref).flatMap(a => Option(Right(a._3.asInstanceOf[A]))).getOrElse { if (compiling.contains(ref)) { Left("compiling") } else { Left("not-in-cache") } } } case _ => Left("scripting-not-enabled") } } } def preCompileScript(script: Script)(implicit ec: ExecutionContext): Unit = { compileAndUpdateIfNeeded(script) } def removeScript(id: String): Unit = { cache.remove(id) compiling.remove(id) } def dispatchEvent(evt: OtoroshiEvent)(implicit ec: ExecutionContext): Unit = { if (env.useEventStreamForScriptEvents) { env.metrics.withTimer("otoroshi.core.proxy.event-dispatch") { env.analyticsActorSystem.eventStream.publish(evt) } } else { Future { env.metrics.withTimer("otoroshi.core.proxy.event-dispatch") { val pluginListeners = listeningCpScripts.get() if (pluginListeners.nonEmpty) { pluginListeners.foreach(l => l.onEvent(evt)(env)) } val scriptListeners = cache.values.map(_._3).collect { case listener: InternalEventListener if listener.listening => listener } if (scriptListeners.nonEmpty) { scriptListeners.foreach(l => l.onEvent(evt)(env)) } } evt }(ec) } } } object Implicits { implicit class ServiceDescriptorWithTransformer(val desc: ServiceDescriptor) extends AnyVal { def beforeRequest( ctx: BeforeRequestContext )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Done] = { env.metrics.withTimerAsync("otoroshi.core.proxy.before-request") { val plugs = desc.plugins.requestTransformers(ctx.request) val gScripts = env.datastores.globalConfigDataStore.latestSafe .filter(_.scripts.enabled) .map(_.scripts) .getOrElse(GlobalScripts(transformersConfig = Json.obj())) val refs = (plugs ++ gScripts.transformersRefs ++ desc.transformerRefs).distinct if (refs.nonEmpty) { Source(refs.toList.zipWithIndex).runForeach { case (ref, index) => env.scriptManager .getScript(ref) .beforeRequest( ctx.copy( index = index, globalConfig = ConfigUtils.mergeOpt( gScripts.transformersConfig, env.datastores.globalConfigDataStore.latestSafe.map(_.plugins.config) ), config = ConfigUtils.merge(ctx.config, desc.plugins.config) ) )(env, ec, mat) } } else { FastFuture.successful(Done) } } } def afterRequest( ctx: AfterRequestContext )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Done] = { env.metrics.withTimerAsync("otoroshi.core.proxy.after-request") { val plugs = desc.plugins.requestTransformers(ctx.request) val gScripts = env.datastores.globalConfigDataStore.latestSafe .filter(_.scripts.enabled) .map(_.scripts) .getOrElse(GlobalScripts(transformersConfig = Json.obj())) val refs = (plugs ++ gScripts.transformersRefs ++ desc.transformerRefs).distinct if (refs.nonEmpty) { Source(refs.toList.zipWithIndex).runForeach { case (ref, index) => env.scriptManager .getScript(ref) .afterRequest( ctx.copy( index = index, globalConfig = ConfigUtils.mergeOpt( gScripts.transformersConfig, env.datastores.globalConfigDataStore.latestSafe.map(_.plugins.config) ), config = ConfigUtils.merge(ctx.config, desc.plugins.config) ) )(env, ec, mat) } } else { FastFuture.successful(Done) } } } def transformRequest( context: TransformerRequestContext )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Either[Result, HttpRequest]] = env.metrics.withTimerAsync("otoroshi.core.proxy.transform-request") { val plugs = desc.plugins.requestTransformers(context.request) val gScripts = env.datastores.globalConfigDataStore.latestSafe .filter(_.scripts.enabled) .map(_.scripts) .getOrElse(GlobalScripts(transformersConfig = Json.obj())) val refs = (plugs ++ gScripts.transformersRefs ++ desc.transformerRefs).distinct if (refs.nonEmpty) { val either: Either[Result, HttpRequest] = Right(context.otoroshiRequest) Source(refs.toList.zipWithIndex).runFoldAsync(either) { case (Left(badResult), (_, _)) => FastFuture.successful(Left(badResult)) case (Right(lastHttpRequest), (ref, index)) => env.scriptManager .getScript(ref) .transformRequestWithCtx( context.copy( otoroshiRequest = lastHttpRequest, index = index, globalConfig = ConfigUtils.mergeOpt( gScripts.transformersConfig, env.datastores.globalConfigDataStore.latestSafe.map(_.plugins.config) ), config = ConfigUtils.merge(context.config, desc.plugins.config) ) )(env, ec, mat) } } else { FastFuture.successful(Right(context.otoroshiRequest)) } } def transformResponse( context: TransformerResponseContext )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Either[Result, HttpResponse]] = env.metrics.withTimerAsync("otoroshi.core.proxy.transform-response") { val plugs = desc.plugins.requestTransformers(context.request) val gScripts = env.datastores.globalConfigDataStore.latestSafe .filter(_.scripts.enabled) .map(_.scripts) .getOrElse(GlobalScripts(transformersConfig = Json.obj())) val refs = (plugs ++ gScripts.transformersRefs ++ desc.transformerRefs).distinct if (refs.nonEmpty) { val either: Either[Result, HttpResponse] = Right(context.otoroshiResponse) Source(refs.toList.zipWithIndex).runFoldAsync(either) { case (Left(badResult), _) => FastFuture.successful(Left(badResult)) case (Right(lastHttpResponse), (ref, index)) => env.scriptManager .getScript(ref) .transformResponseWithCtx( context.copy( otoroshiResponse = lastHttpResponse, index = index, globalConfig = ConfigUtils.mergeOpt( gScripts.transformersConfig, env.datastores.globalConfigDataStore.latestSafe.map(_.plugins.config) ), config = ConfigUtils.merge(context.config, desc.plugins.config) ) )(env, ec, mat) } } else { FastFuture.successful(Right(context.otoroshiResponse)) } } def transformError( context: TransformerErrorContext )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Result] = env.metrics.withTimerAsync("otoroshi.core.proxy.transform-error") { val plugs = desc.plugins.requestTransformers(context.request) val gScripts = env.datastores.globalConfigDataStore.latestSafe .filter(_.scripts.enabled) .map(_.scripts) .getOrElse(GlobalScripts(transformersConfig = Json.obj())) val refs = (plugs ++ gScripts.transformersRefs ++ desc.transformerRefs).distinct if (refs.nonEmpty) { val result: Result = context.otoroshiResult Source(refs.toList.zipWithIndex).runFoldAsync(result) { case (lastResult, (ref, index)) => env.scriptManager .getScript(ref) .transformErrorWithCtx( context.copy( otoroshiResult = lastResult, otoroshiResponse = HttpResponse( lastResult.header.status, lastResult.header.headers, lastResult.newCookies.map(c => DefaultWSCookie( name = c.name, value = c.value, domain = c.domain, path = Option(c.path), maxAge = c.maxAge.map(_.toLong), secure = c.secure, httpOnly = c.httpOnly ) ), () => context.otoroshiResult.body.dataStream ), index = index, globalConfig = ConfigUtils.mergeOpt( gScripts.transformersConfig, env.datastores.globalConfigDataStore.latestSafe.map(_.plugins.config) ), config = ConfigUtils.merge(context.config, desc.plugins.config) ) )(env, ec, mat) } } else { FastFuture.successful(context.otoroshiResult) } } def transformRequestBody( context: TransformerRequestBodyContext )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Source[ByteString, Any] = env.metrics.withTimer("otoroshi.core.proxy.transform-request-body") { val plugs = desc.plugins.requestTransformers(context.request) val gScripts = env.datastores.globalConfigDataStore.latestSafe .filter(_.scripts.enabled) .map(_.scripts) .getOrElse(GlobalScripts(transformersConfig = Json.obj())) val refs = (plugs ++ gScripts.transformersRefs ++ desc.transformerRefs).distinct if (refs.nonEmpty) { Source.futureSource(Source(refs.toList.zipWithIndex).runFold(context.body) { case (body, (ref, index)) => env.scriptManager .getScript(ref) .transformRequestBodyWithCtx( context.copy( body = body, index = index, globalConfig = ConfigUtils.mergeOpt( gScripts.transformersConfig, env.datastores.globalConfigDataStore.latestSafe.map(_.plugins.config) ), config = ConfigUtils.merge(context.config, desc.plugins.config) ) )(env, ec, mat) }) } else { context.body } } def transformResponseBody( context: TransformerResponseBodyContext )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Source[ByteString, Any] = env.metrics.withTimer("otoroshi.core.proxy.transform-response-body") { val plugs = desc.plugins.requestTransformers(context.request) val gScripts = env.datastores.globalConfigDataStore.latestSafe .filter(_.scripts.enabled) .map(_.scripts) .getOrElse(GlobalScripts(transformersConfig = Json.obj())) val refs = (plugs ++ gScripts.transformersRefs ++ desc.transformerRefs).distinct if (refs.nonEmpty) { Source.futureSource(Source(refs.toList.zipWithIndex).runFold(context.body) { case (body, (ref, index)) => env.scriptManager .getScript(ref) .transformResponseBodyWithCtx( context.copy( body = body, index = index, globalConfig = ConfigUtils.mergeOpt( gScripts.transformersConfig, env.datastores.globalConfigDataStore.latestSafe.map(_.plugins.config) ), config = ConfigUtils.merge(context.config, desc.plugins.config) ) )(env, ec, mat) }) } else { context.body } } } } case class Script( id: String, name: String, desc: String, code: String, `type`: PluginType, tags: Seq[String] = Seq.empty, metadata: Map[String, String] = Map.empty, location: otoroshi.models.EntityLocation = otoroshi.models.EntityLocation() ) extends otoroshi.models.EntityLocationSupport { def save()(implicit ec: ExecutionContext, env: Env) = env.datastores.scriptDataStore.set(this) def delete()(implicit ec: ExecutionContext, env: Env) = env.datastores.scriptDataStore.delete(this) def exists()(implicit ec: ExecutionContext, env: Env) = env.datastores.scriptDataStore.exists(this) def toJson = Script.toJson(this) def hash: String = Hashing.sha256().hashString(code, StandardCharsets.UTF_8).toString def json: JsValue = toJson def internalId: String = id def theDescription: String = desc def theMetadata: Map[String, String] = metadata def theName: String = name def theTags: Seq[String] = tags } object Script { lazy val logger = Logger("otoroshi-script") val digest: MessageDigest = MessageDigest.getInstance("SHA-256") val _fmt: Format[Script] = new Format[Script] { override def writes(apk: Script): JsValue = apk.location.jsonWithKey ++ Json.obj( "id" -> apk.id, "name" -> apk.name, "desc" -> apk.desc, "code" -> apk.code, "type" -> apk.`type`.name, "metadata" -> apk.metadata, "tags" -> JsArray(apk.tags.map(JsString.apply)) ) override def reads(json: JsValue): JsResult[Script] = Try { val scriptType = (json \ "type").asOpt[String].getOrElse("transformer") match { case "app" => PluginType.AppType case "transformer" => PluginType.TransformerType case "validator" => PluginType.AccessValidatorType case "preroute" => PluginType.PreRoutingType case "sink" => PluginType.RequestSinkType case "job" => PluginType.JobType case "exporter" => PluginType.DataExporterType case "request-handler" => PluginType.RequestHandlerType case "composite" => PluginType.CompositeType case _ => PluginType.TransformerType } Script( location = otoroshi.models.EntityLocation.readFromKey(json), id = (json \ "id").as[String], name = (json \ "name").as[String], desc = (json \ "desc").as[String], code = (json \ "code").as[String], metadata = (json \ "metadata").asOpt[Map[String, String]].getOrElse(Map.empty), tags = (json \ "tags").asOpt[Seq[String]].getOrElse(Seq.empty[String]), `type` = scriptType ) } map { case sd => JsSuccess(sd) } recover { case t => logger.error("Error while reading Script", t) JsError(t.getMessage) } get } def toJson(value: Script): JsValue = _fmt.writes(value) def fromJsons(value: JsValue): Script = try { _fmt.reads(value).get } catch { case e: Throwable => { logger.error(s"Try to deserialize ${Json.prettyPrint(value)}") throw e } } def fromJsonSafe(value: JsValue): Either[Seq[(JsPath, Seq[JsonValidationError])], Script] = _fmt.reads(value).asEither } trait ScriptDataStore extends BasicStore[Script] { def template(env: Env): Script = { val defaultScript = Script( id = IdGenerator.namedId("script", env), name = "New request transformer", desc = "New request transformer", code = """import akka.stream.Materializer |import otoroshi.env.Env |import otoroshi.models.{ApiKey, PrivateAppsUser, ServiceDescriptor} |import otoroshi.script._ |import play.api.Logger |import play.api.mvc.{Result, Results} |import scala.util._ |import scala.concurrent.{ExecutionContext, Future} |import otoroshi.utils.syntax.implicits._ | |/** | * Your own request transformer | */ |class MyTransformer extends RequestTransformer { | | val logger = Logger("my-transformer") | | override def transformRequestWithCtx( | ctx: TransformerRequestContext | )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Either[Result, HttpRequest]] = { | logger.info(s"Request incoming with id: ${ctx.snowflake}") | // Here add a new header to the request between otoroshi and the target | Right(ctx.otoroshiRequest.copy( | headers = ctx.otoroshiRequest.headers + ("Hello" -> "World") | )).future | } |} | |// don't forget to return an instance of the transformer to make it work |new MyTransformer() """.stripMargin, `type` = PluginType.TransformerType, metadata = Map.empty ) env.datastores.globalConfigDataStore .latest()(env.otoroshiExecutionContext, env) .templates .script .map { template => Script._fmt.reads(defaultScript.json.asObject.deepMerge(template)).get } .getOrElse { defaultScript } } } class KvScriptDataStore(redisCli: RedisLike, _env: Env) extends ScriptDataStore with RedisLikeStore[Script] { override def fmt: Format[Script] = Script._fmt override def redisLike(implicit env: Env): RedisLike = redisCli override def key(id: String): String = s"${_env.storageRoot}:scripts:$id" override def extractId(value: Script): String = value.id }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy