Create plugins

When everything has failed and you absolutely need a feature in Otoroshi to make your use case work, there is a solution. Plugins is the feature in Otoroshi that allow you to code how Otoroshi should behave when receiving, validating and routing an http request. With request plugin, you can change request / response headers and request / response body the way you want, provide your own apikey, etc.

Plugin types

there are many plugin types explained here

Code and signatures

for more information about APIs you can use

Plugin examples

A lot of plugins comes with otoroshi, you can find them on github

Providing a transformer from Java classpath

You can write your own transformer using your favorite IDE. Just create an SBT project with the following dependencies. It can be quite handy to manage the source code like any other piece of code, and it avoid the compilation time for the script at Otoroshi startup.

lazy val root = (project in file(".")).
  settings(
    inThisBuild(List(
      organization := "com.example",
      scalaVersion := "2.12.7",
      version      := "0.1.0-SNAPSHOT"
    )),
    name := "request-transformer-example",
    libraryDependencies += "fr.maif" %% "otoroshi" % "17.x.x"
  )

then, you can write something like

package otoroshi_plugins.mycompany.demo

import akka.stream.Materializer
import otoroshi.env.Env
import otoroshi.next.plugins.api._
import otoroshi.utils.syntax.implicits._
import play.api.libs.json._
import play.api.mvc.Result

import scala.concurrent.{ExecutionContext, Future}
import scala.util._

case class BodyLengthLimiterConfig(maxRequestBodySize: Option[Long] = None, maxResponseBodySize: Option[Long] = None) extends NgPluginConfig {
  def json: JsValue = BodyLengthLimiterConfig.format.writes(this)
}

object BodyLengthLimiterConfig {
  val configFlow: Seq[String]        = Seq("max_request_body_size", "max_response_body_size")
  val configSchema: Option[JsObject] = Some(
    Json.obj(
      "max_response_body_size" -> Json.obj(
        "type"  -> "number",
        "label" -> "Max response length",
        "props" -> Json.obj(
          "label"  -> "Max response Length",
          "suffix" -> "bytes"
        )
      ),
      "max_request_body_size" -> Json.obj(
        "type"  -> "number",
        "label" -> "Max request length",
        "props" -> Json.obj(
          "label"  -> "Max request Length",
          "suffix" -> "bytes"
        )
      )
    )
  )
  val format = new Format[BodyLengthLimiterConfig] {
    override def reads(json: JsValue): JsResult[BodyLengthLimiterConfig] = Try {
      BodyLengthLimiterConfig(
        maxRequestBodySize = json.select("max_request_body_size").asOpt[Long],
        maxResponseBodySize = json.select("max_response_body_size").asOpt[Long],
      )
    } match {
      case Failure(e) => JsError(e.getMessage)
      case Success(e) => JsSuccess(e)
    }
    override def writes(o: BodyLengthLimiterConfig): JsValue = Json.obj(
      "max_request_body_size" -> o.maxRequestBodySize,
      "max_response_body_size" -> o.maxResponseBodySize
    )
  }
}

class BodyLengthLimiter extends NgRequestTransformer {

  override def steps: Seq[NgStep]                = Seq(NgStep.TransformRequest, NgStep.TransformResponse)
  override def categories: Seq[NgPluginCategory] = Seq(NgPluginCategory.Transformations)
  override def visibility: NgPluginVisibility    = NgPluginVisibility.NgUserLand

  override def multiInstance: Boolean                      = true
  override def usesCallbacks: Boolean                      = false
  override def transformsRequest: Boolean                  = true
  override def transformsResponse: Boolean                 = true
  override def name: String                                = "Body length limiter"
  override def description: Option[String]                 = "This plugin will limit request and response body length".some
  override def defaultConfigObject: Option[NgPluginConfig] = Some(BodyLengthLimiterConfig())
  override def noJsForm: Boolean = true
  override def configFlow: Seq[String] = BodyLengthLimiterConfig.configFlow
  override def configSchema: Option[JsObject] = BodyLengthLimiterConfig.configSchema

  override def transformRequest(ctx: NgTransformerRequestContext)(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Either[Result, NgPluginHttpRequest]] = {
    val config = ctx.cachedConfig(internalName)(BodyLengthLimiterConfig.format).getOrElse(BodyLengthLimiterConfig())
    val maxFromConfigFile = env.configuration.getOptional[Long]("my-transformer.maxRequestBodySize")
    val max: Long = config.maxRequestBodySize.orElse(maxFromConfigFile).getOrElse(4 * 1024 * 1024)
    Right(ctx.otoroshiRequest.copy(body = ctx.otoroshiRequest.body.limitWeighted(max)(_.size))).vfuture
  }

  override def transformResponse(ctx: NgTransformerResponseContext)(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Either[Result, NgPluginHttpResponse]] = {
    val config = ctx.cachedConfig(internalName)(BodyLengthLimiterConfig.format).getOrElse(BodyLengthLimiterConfig())
    val maxFromConfigFile = env.configuration.getOptional[Long]("my-transformer.maxResponseBodySize")
    val max: Long = config.maxResponseBodySize.orElse(maxFromConfigFile).getOrElse(4 * 1024 * 1024)
    Right(ctx.otoroshiResponse.copy(body = ctx.otoroshiResponse.body.limitWeighted(max)(_.size))).vfuture
  }
}
Warning

you MUST provide plugins that lies in the otoroshi_plugins package or in a sub-package of otoroshi_plugins. If you do not, your plugin will not be found by otoroshi. for example

package otoroshi_plugins.mycompany.demo

When your code is ready, create a jar file

sbt package

and add the jar file to the Otoroshi classpath

java -cp "/path/to/transformer.jar:$/path/to/otoroshi.jar" play.core.server.ProdServerStart

then, in the route designer, you can chose your transformer in the list. If you want to do it from the API, you have to defined the transformerRef using cp: prefix like

{
  "plugin": "cp:otoroshi_plugins.mycompany.demo.BodyLengthLimiter",
  "enabled": true,
  "debug": false,
  "include": [],
  "exclude": [],
  "bound_listeners": [],
  "config": {
    "max_request_body_size": null,
    "max_response_body_size": null
  }
}

Getting custom configuration from the Otoroshi config. file

Let say you need to provide custom configuration values for a script, then you can customize a configuration file of Otoroshi

include "application.conf"

my-transformer {
  env = "prod"
  maxRequestBodySize = 2048
  maxResponseBodySize = 2048
}

then start Otoroshi like

java -Dconfig.file=/path/to/custom.conf -jar otoroshi.jar

Using a library that is not embedded in Otoroshi

Just use the classpath option when running Otoroshi

java -cp "/path/to/library.jar:/path/to/transformer.jar:$/path/to/otoroshi.jar" play.core.server.ProdServerStart

Be carefull as your library can conflict with other libraries used by Otoroshi and affect its stability

Full examples

you can find some example projects on the Cloud APIM github account: