next.plugins.api.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of otoroshi_2.12 Show documentation
Show all versions of otoroshi_2.12 Show documentation
Lightweight api management on top of a modern http reverse proxy
The newest version!
package otoroshi.next.plugins.api
import akka.Done
import akka.http.scaladsl.model.{ContentType, StatusCodes, Uri}
import akka.stream.Materializer
import akka.stream.scaladsl.{Flow, Sink, Source}
import akka.util.ByteString
import com.github.blemale.scaffeine.{Cache, Scaffeine}
import otoroshi.env.Env
import otoroshi.gateway.Errors
import otoroshi.models.{ApiKey, PrivateAppsUser, Target}
import otoroshi.next.models.{NgMatchedRoute, NgPluginInstance, NgRoute, NgTarget}
import otoroshi.next.plugins.RejectStrategy
import otoroshi.next.proxy.{NgExecutionReport, NgProxyEngineError, NgReportPluginSequence, NgReportPluginSequenceItem}
import otoroshi.next.utils.JsonHelpers
import otoroshi.script.{InternalEventListener, NamedPlugin, PluginType, StartableAndStoppable}
import otoroshi.utils.TypedMap
import otoroshi.utils.http.WSCookieWithSameSite
import otoroshi.utils.syntax.implicits._
import play.api.Logger
import play.api.http.HttpEntity
import play.api.http.websocket.{
CloseMessage,
Message,
PingMessage,
PongMessage,
BinaryMessage => PlayWSBinaryMessage,
TextMessage => PlayWSTextMessage
}
import play.api.libs.json._
import play.api.libs.ws.{DefaultWSCookie, WSCookie, WSResponse}
import play.api.mvc.{Cookie, RequestHeader, Result, Results}
import java.security.cert.X509Certificate
import scala.concurrent.duration.DurationInt
import scala.concurrent.{ExecutionContext, Future}
import scala.reflect.ClassTag
import scala.util.{Either, Failure, Success, Try}
object NgPluginHelper {
def pluginId[A](implicit ct: ClassTag[A]): String = s"cp:${ct.runtimeClass.getName}"
}
object NgPluginHttpRequest {
def fromRequest(req: RequestHeader): NgPluginHttpRequest = {
NgPluginHttpRequest(
url = req.uri,
method = req.method,
headers = req.headers.toSimpleMap,
cookies = Seq.empty,
version = req.version,
clientCertificateChain = () => req.clientCertificateChain,
body = Source.empty,
backend = None
)
}
}
case class NgPluginHttpRequest(
url: String,
method: String,
headers: Map[String, String],
cookies: Seq[WSCookie] = Seq.empty[WSCookie],
version: String,
clientCertificateChain: () => Option[Seq[X509Certificate]],
body: Source[ByteString, _],
backend: Option[NgTarget]
) {
lazy val contentType: Option[String] = header("Content-Type")
lazy val typedContentType: Option[ContentType] = contentType.flatMap(ct => ContentType.parse(ct).toOption)
lazy val contentLengthStr: Option[String] = header("Content-Length")
lazy val transferEncoding: Option[String] = header("Transfer-Encoding")
lazy val host: String = header("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()
lazy val hasBodyWithoutLength: (Boolean, Boolean) =
otoroshi.utils.body.BodyUtils.hasBodyWithoutLengthGen(
method.toUpperCase(),
contentLengthStr,
contentType,
transferEncoding
)
lazy val hasBody: Boolean = hasBodyWithoutLength._1
lazy val queryParams = uri.query().toMap
// val ctype = contentType
// (method.toUpperCase(), header("Content-Length")) match {
// case ("GET", Some(_)) => true
// case ("GET", None) if ctype.isDefined => true
// case ("GET", None) => false
// case ("HEAD", Some(_)) => true
// case ("HEAD", None) if ctype.isDefined => true
// case ("HEAD", None) => false
// case ("PATCH", _) => true
// case ("POST", _) => true
// case ("PUT", _) => true
// case ("QUERY", _) => true
// case ("DELETE", Some(_)) => true
// case ("DELETE", None) if ctype.isDefined => true
// case ("DELETE", None) => false
// case _ => true
// }
// }
def queryParam(name: String): Option[String] = uri.query().get(name).orElse(uri.query().get(name.toLowerCase()))
def header(name: String): Option[String] = headers.get(name).orElse(headers.get(name.toLowerCase()))
def json: JsValue = {
val certs: Option[Seq[X509Certificate]] = clientCertificateChain()
Json.obj(
"url" -> url,
"method" -> method,
"headers" -> headers,
"query" -> uri.query().toMultiMap,
"version" -> version,
"client_cert_chain" -> JsonHelpers.clientCertChainToJson(certs),
"backend" -> backend.map(_.json).getOrElse(JsNull).asValue,
"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
)
)
)
)
}
}
object NgPluginHttpResponse {
def fromResult(result: Result): NgPluginHttpResponse = {
val headers = result.header.headers
.applyOnWithOpt(result.body.contentType) { case (headers, ctype) =>
headers + ("Content-Type" -> ctype)
}
.applyOnWithOpt(result.body.contentLength) { case (headers, clength) =>
headers + ("Content-Length" -> clength.toString)
}
NgPluginHttpResponse(
status = result.header.status,
headers = headers,
cookies = result.newCookies.map(c =>
DefaultWSCookie(
name = c.name,
value = c.value,
maxAge = c.maxAge.map(_.toLong),
path = Option(c.path),
domain = c.domain,
secure = c.secure,
httpOnly = c.httpOnly
)
),
body = result.body.dataStream
)
}
}
case class NgPluginHttpResponse(
status: Int,
headers: Map[String, String],
cookies: Seq[WSCookie] = Seq.empty[WSCookie],
body: Source[ByteString, _]
) {
lazy val statusText: String = StatusCodes.getForKey(status).map(_.reason()).getOrElse("NONE")
lazy val transferEncoding: Option[String] = header("Transfer-Encoding")
lazy val isChunked: Boolean = transferEncoding.exists(h => h.toLowerCase().contains("chunked"))
lazy val contentType: Option[String] = header("Content-Type")
lazy val contentLengthStr: Option[String] = header("Content-Length")
lazy val contentLength: Option[Long] = contentLengthStr.map(_.toLong)
lazy val hasLength: Boolean = contentLengthStr.isDefined
def header(name: String): Option[String] = headers.get(name).orElse(headers.get(name.toLowerCase()))
def asResult: Result = {
val ctype = header("Content-Type")
val clength = header("Content-Length").map(_.toLong)
Results
.Status(status)
.sendEntity(HttpEntity.Streamed(body, clength, ctype))
.withHeaders(headers.toSeq: _*)
.withCookies(cookies.map { c =>
Cookie(
name = c.name,
value = c.value,
maxAge = c.maxAge.map(_.toInt),
path = c.path.getOrElse("/"),
domain = c.domain,
secure = c.secure,
httpOnly = c.httpOnly,
sameSite = c.asInstanceOf[WSCookieWithSameSite].sameSite // this one is risky ;)
)
}: _*)
.applyOnWithOpt(ctype) { case (r, typ) =>
r.as(typ)
}
}
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
)
)
)
)
}
sealed trait NgPluginVisibility {
def name: String
def json: JsValue = name.json
}
object NgPluginVisibility {
case object NgInternal extends NgPluginVisibility { def name: String = "internal" }
case object NgUserLand extends NgPluginVisibility { def name: String = "userland" }
}
sealed trait NgPluginCategory {
def name: String
def json: JsValue = name.json
}
object NgPluginCategory {
case object Custom extends NgPluginCategory { def name: String = "Custom" }
case object Other extends NgPluginCategory { def name: String = "Other" }
case object Security extends NgPluginCategory { def name: String = "Security" }
case object Authentication extends NgPluginCategory { def name: String = "Authentication" }
case object AccessControl extends NgPluginCategory { def name: String = "AccessControl" }
case object Logging extends NgPluginCategory { def name: String = "Logging" }
case object TrafficControl extends NgPluginCategory { def name: String = "TrafficControl" }
case object Monitoring extends NgPluginCategory { def name: String = "Monitoring" }
case object Transformations extends NgPluginCategory { def name: String = "Transformations" }
case object Headers extends NgPluginCategory { def name: String = "Headers" }
case object Experimental extends NgPluginCategory { def name: String = "Experimental" }
case object Integrations extends NgPluginCategory { def name: String = "Integrations" }
case object Tunnel extends NgPluginCategory { def name: String = "Tunnel" }
case object Wasm extends NgPluginCategory { def name: String = "Wasm" }
case object Classic extends NgPluginCategory { def name: String = "Classic" }
case object ServiceDiscovery extends NgPluginCategory { def name: String = "ServiceDiscovery" }
case object Websocket extends NgPluginCategory { def name: String = "Websocket" }
case class Custom(name: String) extends NgPluginCategory
val all = Seq(
Classic,
AccessControl,
Authentication,
Custom,
Experimental,
Headers,
Integrations,
Logging,
Monitoring,
Other,
Security,
ServiceDiscovery,
TrafficControl,
Transformations,
Tunnel,
Wasm,
Websocket
)
}
sealed trait NgStep {
def name: String
def json: JsValue = name.json
}
object NgStep {
case object Router extends NgStep { def name: String = "Router" }
case object Sink extends NgStep { def name: String = "Sink" }
case object PreRoute extends NgStep { def name: String = "PreRoute" }
case object ValidateAccess extends NgStep { def name: String = "ValidateAccess" }
case object TransformRequest extends NgStep { def name: String = "TransformRequest" }
case object TransformResponse extends NgStep { def name: String = "TransformResponse" }
case object MatchRoute extends NgStep { def name: String = "MatchRoute" }
case object HandlesTunnel extends NgStep { def name: String = "HandlesTunnel" }
case object HandlesRequest extends NgStep { def name: String = "HandlesRequest" }
case object CallBackend extends NgStep { def name: String = "CallBackend" }
case object Job extends NgStep { def name: String = "Job" }
val all = Seq(
Router,
Sink,
PreRoute,
ValidateAccess,
TransformRequest,
TransformResponse,
MatchRoute,
HandlesTunnel,
HandlesRequest,
CallBackend
)
def apply(value: String): Option[NgStep] = value match {
case "Router" => Router.some
case "Sink" => Sink.some
case "PreRoute" => PreRoute.some
case "ValidateAccess" => ValidateAccess.some
case "TransformRequest" => TransformRequest.some
case "TransformResponse" => TransformResponse.some
case "MatchRoute" => MatchRoute.some
case "HandlesTunnel" => HandlesTunnel.some
case "HandlesRequest" => HandlesRequest.some
case "CallBackend" => CallBackend.some
case "Job" => Job.some
case _ => None
}
}
trait NgPluginConfig {
def json: JsValue
}
trait NgNamedPlugin extends NamedPlugin { self =>
def visibility: NgPluginVisibility
def categories: Seq[NgPluginCategory]
def tags: Seq[String] = Seq.empty
def steps: Seq[NgStep]
def multiInstance: Boolean = true
def noJsForm: Boolean = false
def defaultConfigObject: Option[NgPluginConfig]
override final def defaultConfig: Option[JsObject] =
defaultConfigObject.map(_.json.asOpt[JsObject].getOrElse(Json.obj()))
override def pluginType: PluginType = PluginType.CompositeType
override def configRoot: Option[String] = None
override def jsonDescription(): JsObject =
Try {
Json.obj(
"name" -> name,
"description" -> description.map(JsString.apply).getOrElse(JsNull).as[JsValue],
"defaultConfig" -> defaultConfig.getOrElse(JsNull).as[JsValue]
// "configSchema" -> configSchema.getOrElse(JsNull).as[JsValue],
// "configFlow" -> JsArray(configFlow.map(JsString.apply))
)
} match {
case Failure(_) => Json.obj()
case Success(s) => s
}
}
object NgCachedConfigContext {
private val cache: Cache[String, Any] = Scaffeine()
.expireAfterWrite(5.seconds)
.maximumSize(1000)
.build()
}
trait NgCachedConfigContext {
def idx: Int
def route: NgRoute
def config: JsValue
def cachedConfig[A](plugin: String)(reads: Reads[A]): Option[A] = Try {
val key = s"${route.cacheableId}::$plugin::$idx"
NgCachedConfigContext.cache.getIfPresent(key) match {
case None =>
reads.reads(config) match {
case JsError(_) => None
case JsSuccess(value, _) =>
NgCachedConfigContext.cache.put(key, value)
Some(value)
}
case Some(v) => Some(v.asInstanceOf[A])
}
}.toOption.flatten
def cachedConfigFn[A](plugin: String)(reads: JsValue => Option[A]): Option[A] = Try {
val key = s"${route.cacheableId}::${plugin}::$idx"
NgCachedConfigContext.cache.getIfPresent(key) match {
case None =>
reads(config) match {
case None => None
case s @ Some(value) =>
NgCachedConfigContext.cache.put(key, value)
s
}
case Some(v) => Some(v.asInstanceOf[A])
}
}.toOption.flatten
def rawConfig[A](reads: Reads[A]): Option[A] = reads.reads(config).asOpt
def rawConfigFn[A](reads: JsValue => Option[A]): Option[A] = reads(config)
}
trait NgPlugin extends StartableAndStoppable with NgNamedPlugin with InternalEventListener
case class NgPreRoutingContext(
snowflake: String,
request: RequestHeader,
route: NgRoute,
config: JsValue,
globalConfig: JsValue,
attrs: TypedMap,
report: NgExecutionReport,
sequence: NgReportPluginSequence,
markPluginItem: Function4[NgReportPluginSequenceItem, NgPreRoutingContext, Boolean, JsValue, Unit],
idx: Int = 0
) extends NgCachedConfigContext {
def wasmJson: JsValue = json.asObject ++ Json.obj("route" -> route.json)
def json: JsValue = Json.obj(
"snowflake" -> snowflake,
// "route" -> route.json,
"request" -> JsonHelpers.requestToJson(request),
"config" -> config,
"global_config" -> globalConfig,
"attrs" -> attrs.json
)
}
sealed trait NgPluginWrapper[A <: NgNamedPlugin] {
def instance: NgPluginInstance
def plugin: A
}
object NgPluginWrapper {
case class NgSimplePluginWrapper[A <: NgNamedPlugin](instance: NgPluginInstance, plugin: A) extends NgPluginWrapper[A]
case class NgMergedRequestTransformerPluginWrapper(plugins: Seq[NgSimplePluginWrapper[NgRequestTransformer]])
extends NgPluginWrapper[NgRequestTransformer] {
private val reqTransformer = new NgMergedRequestTransformer(plugins)
private val inst = NgPluginInstance("transform-request-merged")
override def instance: NgPluginInstance = inst
override def plugin: NgRequestTransformer = reqTransformer
}
case class NgMergedResponseTransformerPluginWrapper(plugins: Seq[NgSimplePluginWrapper[NgRequestTransformer]])
extends NgPluginWrapper[NgRequestTransformer] {
private val respTransformer = new NgMergedResponseTransformer(plugins)
private val inst = NgPluginInstance("transform-response-merged")
override def instance: NgPluginInstance = inst
override def plugin: NgRequestTransformer = respTransformer
}
case class NgMergedPreRoutingPluginWrapper(plugins: Seq[NgSimplePluginWrapper[NgPreRouting]])
extends NgPluginWrapper[NgPreRouting] {
private val preRouting = new NgMergedPreRouting(plugins)
private val inst = NgPluginInstance("pre-routing-merged")
override def instance: NgPluginInstance = inst
override def plugin: NgPreRouting = preRouting
}
case class NgMergedAccessValidatorPluginWrapper(plugins: Seq[NgSimplePluginWrapper[NgAccessValidator]])
extends NgPluginWrapper[NgAccessValidator] {
private val accessValidator = new NgMergedAccessValidator(plugins)
private val inst = NgPluginInstance("access-validator-merged")
override def instance: NgPluginInstance = inst
override def plugin: NgAccessValidator = accessValidator
}
}
trait NgPreRoutingError {
def result: Result
}
case class NgPreRoutingErrorRaw(
body: ByteString,
code: Int = 500,
contentType: String,
headers: Map[String, String] = Map.empty
) extends NgPreRoutingError {
def result: Result = {
Results.Status(code).apply(body).as(contentType).withHeaders(headers.toSeq: _*)
}
}
case class NgPreRoutingErrorWithResult(result: Result) extends NgPreRoutingError
object NgPreRouting {
val done: Either[NgPreRoutingError, Done] = Right(Done)
val futureDone: Future[Either[NgPreRoutingError, Done]] = done.vfuture
}
trait NgPreRouting extends NgPlugin {
def isPreRouteAsync: Boolean = true
def preRouteSync(ctx: NgPreRoutingContext)(implicit env: Env, ec: ExecutionContext): Either[NgPreRoutingError, Done] =
NgPreRouting.done
def preRoute(
ctx: NgPreRoutingContext
)(implicit env: Env, ec: ExecutionContext): Future[Either[NgPreRoutingError, Done]] = preRouteSync(ctx).vfuture
}
case class NgRouterContext(
request: RequestHeader,
config: JsValue,
attrs: TypedMap
) {
def json: JsValue = Json.obj(
"request" -> JsonHelpers.requestToJson(request),
"config" -> config,
"attrs" -> attrs.json
)
}
trait NgRouter extends NgPlugin {
def findRoute(ctx: NgRouterContext)(implicit env: Env, ec: ExecutionContext): Option[NgMatchedRoute] = None
}
case class NgBeforeRequestContext(
snowflake: String,
route: NgRoute,
request: RequestHeader,
config: JsValue,
attrs: TypedMap,
globalConfig: JsValue = Json.obj(),
idx: Int = 0
) extends NgCachedConfigContext {
def json: JsValue = Json.obj(
"snowflake" -> snowflake,
// "route" -> route.json,
"request" -> JsonHelpers.requestToJson(request),
"config" -> config,
"global_config" -> globalConfig,
"attrs" -> attrs.json
)
}
case class NgAfterRequestContext(
snowflake: String,
route: NgRoute,
request: RequestHeader,
config: JsValue,
attrs: TypedMap,
globalConfig: JsValue = Json.obj(),
idx: Int = 0
) extends NgCachedConfigContext {
def json: JsValue = Json.obj(
"snowflake" -> snowflake,
// "route" -> route.json,
"request" -> JsonHelpers.requestToJson(request),
"config" -> config,
"global_config" -> globalConfig,
"attrs" -> attrs.json
)
}
case class NgTransformerRequestContext(
rawRequest: NgPluginHttpRequest,
otoroshiRequest: NgPluginHttpRequest,
snowflake: String,
route: NgRoute,
apikey: Option[ApiKey],
user: Option[PrivateAppsUser],
request: RequestHeader,
config: JsValue,
attrs: TypedMap,
globalConfig: JsValue = Json.obj(),
report: NgExecutionReport,
sequence: NgReportPluginSequence,
markPluginItem: Function4[NgReportPluginSequenceItem, NgTransformerRequestContext, Boolean, JsValue, Unit],
idx: Int = 0
) extends NgCachedConfigContext {
def json: JsValue = Json.obj(
"snowflake" -> snowflake,
"raw_request" -> rawRequest.json,
"otoroshi_request" -> otoroshiRequest.json,
"apikey" -> apikey.map(_.lightJson).getOrElse(JsNull).as[JsValue],
"user" -> user.map(_.lightJson).getOrElse(JsNull).as[JsValue],
// "route" -> route.json,
"request" -> JsonHelpers.requestToJson(request),
"config" -> config,
"global_config" -> globalConfig,
"attrs" -> attrs.json
)
def wasmJson(implicit env: Env, ec: ExecutionContext): Future[JsValue] = {
implicit val mat = env.otoroshiMaterializer
JsonHelpers.requestBody(otoroshiRequest).map { body =>
json.asObject ++ Json.obj(
"route" -> route.json,
"request_body_bytes" -> body
)
}
}
}
case class NgTransformerResponseContext(
response: Option[WSResponse],
rawResponse: NgPluginHttpResponse,
otoroshiResponse: NgPluginHttpResponse,
snowflake: String,
route: NgRoute,
apikey: Option[ApiKey],
user: Option[PrivateAppsUser],
request: RequestHeader,
config: JsValue,
attrs: TypedMap,
globalConfig: JsValue = Json.obj(),
report: NgExecutionReport,
sequence: NgReportPluginSequence,
markPluginItem: Function4[NgReportPluginSequenceItem, NgTransformerResponseContext, Boolean, JsValue, Unit],
idx: Int = 0
) extends NgCachedConfigContext {
def json: JsValue = Json.obj(
"snowflake" -> snowflake,
"raw_response" -> rawResponse.json,
"otoroshi_response" -> otoroshiResponse.json,
"apikey" -> apikey.map(_.lightJson).getOrElse(JsNull).as[JsValue],
"user" -> user.map(_.lightJson).getOrElse(JsNull).as[JsValue],
// "route" -> route.json,
"request" -> JsonHelpers.requestToJson(request),
"config" -> config,
"global_config" -> globalConfig,
"attrs" -> attrs.json
)
def wasmJson(implicit env: Env, ec: ExecutionContext): Future[JsValue] = {
implicit val mat = env.otoroshiMaterializer
JsonHelpers.responseBody(otoroshiResponse).map { bodyOut =>
json.asObject ++ Json.obj(
"route" -> route.json,
"response_body_bytes" -> bodyOut
)
}
}
}
case class NgTransformerErrorContext(
snowflake: String,
message: String,
otoroshiResponse: NgPluginHttpResponse,
request: RequestHeader,
maybeCauseId: Option[String],
callAttempts: Int,
route: NgRoute,
apikey: Option[ApiKey],
user: Option[PrivateAppsUser],
config: JsValue,
globalConfig: JsValue = Json.obj(),
attrs: TypedMap,
report: NgExecutionReport,
idx: Int = 0
) extends NgCachedConfigContext {
def json: JsValue = Json.obj(
"snowflake" -> snowflake,
"maybe_cause_id" -> maybeCauseId.map(JsString.apply).getOrElse(JsNull).as[JsValue],
"call_attempts" -> callAttempts,
"otoroshi_response" -> otoroshiResponse.json,
// "otoroshi_result" -> Json.obj("status" -> otoroshiResult.header.status, "headers" -> otoroshiResult.header.headers),
"apikey" -> apikey.map(_.lightJson).getOrElse(JsNull).as[JsValue],
"user" -> user.map(_.lightJson).getOrElse(JsNull).as[JsValue],
// "route" -> route.json,
"request" -> JsonHelpers.requestToJson(request),
"config" -> config,
"global_config" -> globalConfig,
"attrs" -> attrs.json
)
def wasmJson(implicit env: Env, ec: ExecutionContext): Future[JsValue] = {
implicit val mat = env.otoroshiMaterializer
JsonHelpers.responseBody(otoroshiResponse).map { bodyOut =>
json.asObject ++ Json.obj(
"route" -> route.json,
"response_body_bytes" -> bodyOut
)
}
}
}
trait NgFakePlugin extends NgPlugin {}
trait NgFakePluginContext extends NgCachedConfigContext
trait NgRequestTransformer extends NgPlugin {
def usesCallbacks: Boolean = true
def transformsRequest: Boolean = true
def transformsResponse: Boolean = true
def transformsError: Boolean = true
def isTransformRequestAsync: Boolean = true
def isTransformResponseAsync: Boolean = true
def beforeRequest(
ctx: NgBeforeRequestContext
)(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Unit] = ().vfuture
def afterRequest(
ctx: NgAfterRequestContext
)(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Unit] = ().vfuture
def transformError(
ctx: NgTransformerErrorContext
)(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[NgPluginHttpResponse] = {
ctx.otoroshiResponse.vfuture
}
def transformRequestSync(
ctx: NgTransformerRequestContext
)(implicit env: Env, ec: ExecutionContext, mat: Materializer): Either[Result, NgPluginHttpRequest] = {
Right(ctx.otoroshiRequest)
}
def transformRequest(
ctx: NgTransformerRequestContext
)(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Either[Result, NgPluginHttpRequest]] = {
transformRequestSync(ctx).vfuture
}
def transformResponseSync(
ctx: NgTransformerResponseContext
)(implicit env: Env, ec: ExecutionContext, mat: Materializer): Either[Result, NgPluginHttpResponse] = {
Right(ctx.otoroshiResponse)
}
def transformResponse(
ctx: NgTransformerResponseContext
)(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Either[Result, NgPluginHttpResponse]] = {
transformResponseSync(ctx).vfuture
}
}
case class NgAccessContext(
snowflake: String,
request: RequestHeader,
route: NgRoute,
user: Option[PrivateAppsUser],
apikey: Option[ApiKey],
config: JsValue,
attrs: TypedMap,
globalConfig: JsValue,
report: NgExecutionReport,
sequence: NgReportPluginSequence,
markPluginItem: Function4[NgReportPluginSequenceItem, NgAccessContext, Boolean, JsValue, Unit],
idx: Int = 0
) extends NgCachedConfigContext {
def json: JsValue = Json.obj(
"snowflake" -> snowflake,
"apikey" -> apikey.map(_.lightJson).getOrElse(JsNull).as[JsValue],
"user" -> user.map(_.lightJson).getOrElse(JsNull).as[JsValue],
// "route" -> route.json,
"request" -> JsonHelpers.requestToJson(request),
"config" -> config,
"global_config" -> globalConfig,
"attrs" -> attrs.json
)
def wasmJson(implicit env: Env, ec: ExecutionContext): JsObject = {
(json.asObject ++ Json.obj(
"route" -> route.json
))
}
}
sealed trait NgAccess
object NgAccess {
case object NgAllowed extends NgAccess
case class NgDenied(result: Result) extends NgAccess
}
trait NgAccessValidator extends NgPlugin {
def isAccessAsync: Boolean = true
def accessSync(ctx: NgAccessContext)(implicit env: Env, ec: ExecutionContext): NgAccess = NgAccess.NgAllowed
def access(ctx: NgAccessContext)(implicit env: Env, ec: ExecutionContext): Future[NgAccess] = accessSync(ctx).vfuture
}
sealed trait NgRequestOrigin {
def name: String
}
object NgRequestOrigin {
case object NgErrorHandler extends NgRequestOrigin { def name: String = "NgErrorHandler" }
case object NgReverseProxy extends NgRequestOrigin { def name: String = "NgReverseProxy" }
}
case class NgRequestSinkContext(
snowflake: String,
request: RequestHeader,
config: JsValue,
attrs: TypedMap,
origin: NgRequestOrigin,
status: Int,
message: String,
body: Source[ByteString, _]
) {
def wasmJson: JsValue = json
def json: JsValue = Json.obj(
"snowflake" -> snowflake,
"request" -> JsonHelpers.requestToJson(request),
"config" -> config,
"attrs" -> attrs.json,
"origin" -> origin.name,
"status" -> status,
"message" -> message
)
}
trait NgRequestSink extends NgPlugin {
def isSinkAsync: Boolean = true
def matches(ctx: NgRequestSinkContext)(implicit env: Env, ec: ExecutionContext): Boolean = false
def handleSync(ctx: NgRequestSinkContext)(implicit env: Env, ec: ExecutionContext): Result =
Results.NotImplemented(Json.obj("error" -> "not implemented yet"))
def handle(ctx: NgRequestSinkContext)(implicit env: Env, ec: ExecutionContext): Future[Result] =
handleSync(ctx).vfuture
}
case class NgRouteMatcherContext(
snowflake: String,
request: RequestHeader,
route: NgRoute,
config: JsValue,
attrs: TypedMap,
idx: Int = 0
) extends NgCachedConfigContext {
def json: JsValue = Json.obj(
"snowflake" -> snowflake,
// "route" -> route.json,
"request" -> JsonHelpers.requestToJson(request),
"config" -> config,
"attrs" -> attrs.json
)
def wasmJson: JsValue = Json.obj(
"snowflake" -> snowflake,
"route" -> route.json,
"request" -> JsonHelpers.requestToJson(request),
"config" -> config,
"attrs" -> attrs.json
)
}
trait NgRouteMatcher extends NgPlugin {
def matches(ctx: NgRouteMatcherContext)(implicit env: Env): Boolean
}
case class NgTunnelHandlerContext(
snowflake: String,
request: RequestHeader,
route: NgRoute,
config: JsValue,
attrs: TypedMap
) {
def json: JsValue = Json.obj(
"snowflake" -> snowflake,
// "route" -> route.json,
"request" -> JsonHelpers.requestToJson(request),
"config" -> config,
"attrs" -> attrs.json
)
}
trait NgTunnelHandler extends NgPlugin with NgAccessValidator {
override def access(ctx: NgAccessContext)(implicit env: Env, ec: ExecutionContext): Future[NgAccess] = {
val isWebsocket = ctx.request.headers.get("Sec-WebSocket-Version").isDefined
if (isWebsocket) {
NgAccess.NgAllowed.vfuture
} else {
NgAccess.NgDenied(Results.NotFound(Json.obj("error" -> "not_found"))).vfuture
}
}
def handle(ctx: NgTunnelHandlerContext)(implicit env: Env, ec: ExecutionContext): Flow[Message, Message, _]
}
case class NgbBackendCallContext(
snowflake: String,
rawRequest: RequestHeader,
request: NgPluginHttpRequest,
route: NgRoute,
backend: NgTarget,
user: Option[PrivateAppsUser],
apikey: Option[ApiKey],
config: JsValue,
globalConfig: JsValue,
attrs: TypedMap,
idx: Int = 0
) extends NgCachedConfigContext {
def json: JsValue = Json.obj(
"snowflake" -> snowflake,
// "route" -> route.json,
"backend" -> backend.json,
"apikey" -> apikey.map(_.lightJson).getOrElse(JsNull).as[JsValue],
"user" -> user.map(_.lightJson).getOrElse(JsNull).as[JsValue],
"raw_request" -> JsonHelpers.requestToJson(rawRequest),
"config" -> config,
"global_config" -> globalConfig,
"attrs" -> attrs.json
)
def wasmJson(implicit env: Env, ec: ExecutionContext): Future[JsValue] = {
implicit val mat = env.otoroshiMaterializer
JsonHelpers.requestBody(request).map { body =>
(json.asObject ++ Json.obj(
"route" -> route.json,
"request_body_bytes" -> body,
"request" -> request.json
))
}
}
}
case class BackendCallResponse(response: NgPluginHttpResponse, rawResponse: Option[WSResponse]) {
import otoroshi.utils.http.Implicits._
def status: Int = rawResponse.map(_.status).getOrElse(response.status)
def contentLengthStr: Option[String] = rawResponse.flatMap(_.contentLengthStr).orElse(response.contentLengthStr)
def contentLength: Option[Long] = rawResponse.map(_.contentLength).getOrElse(response.contentLength)
def headers: Map[String, Seq[String]] = rawResponse.map(_.headers).getOrElse(response.headers.mapValues(v => Seq(v)))
def header(name: String): Option[String] =
rawResponse.map(_.header(name)).getOrElse(response.headers.getIgnoreCase(name))
def isChunked(): Option[Boolean] = rawResponse.map(_.isChunked()).getOrElse(response.isChunked.some)
}
trait NgBackendCall extends NgPlugin {
def useDelegates: Boolean
def sourceBodyResponse(
status: Int,
headers: Map[String, String],
body: Source[ByteString, _]
): Either[NgProxyEngineError, BackendCallResponse] = {
val finalHeaders = headers.getIgnoreCase("Transfer-Encoding") match {
case None =>
headers.getIgnoreCase("Content-Length") match {
case None => headers ++ Map("Transfer-Encoding" -> s"chunked")
case Some(_) => headers ++ Map("Transfer-Encoding" -> s"chunked") - "Content-Length" - "content-length"
}
case Some(_) => headers
}
BackendCallResponse(
NgPluginHttpResponse(status, finalHeaders, Seq.empty, body),
None
).right[NgProxyEngineError]
}
def inMemoryBodyResponse(
status: Int,
headers: Map[String, String],
body: ByteString
): Either[NgProxyEngineError, BackendCallResponse] = {
val finalHeaders = headers.getIgnoreCase("Transfer-Encoding") match {
case None =>
headers.getIgnoreCase("Content-Length") match {
case None => headers ++ Map("Content-Length" -> s"${body.length}")
case Some(_) => headers
}
case Some(_) => headers
}
BackendCallResponse(
NgPluginHttpResponse(status, finalHeaders, Seq.empty, body.chunks(16 * 1024)),
None
).right[NgProxyEngineError]
}
def emptyBodyResponse(
status: Int,
headers: Map[String, String]
): Either[NgProxyEngineError, BackendCallResponse] = {
val finalHeaders = headers ++ Map("Content-Length" -> s"0")
BackendCallResponse(
NgPluginHttpResponse(status, finalHeaders, Seq.empty, Source.empty),
None
).right[NgProxyEngineError]
}
def callBackend(ctx: NgbBackendCallContext, delegates: () => Future[Either[NgProxyEngineError, BackendCallResponse]])(
implicit
env: Env,
ec: ExecutionContext,
mat: Materializer
): Future[Either[NgProxyEngineError, BackendCallResponse]] = {
delegates()
}
}
class NgMergedRequestTransformer(plugins: Seq[NgPluginWrapper.NgSimplePluginWrapper[NgRequestTransformer]])
extends NgRequestTransformer {
override def steps: Seq[NgStep] = Seq(NgStep.TransformRequest)
override def categories: Seq[NgPluginCategory] = Seq(NgPluginCategory.Other)
override def visibility: NgPluginVisibility = NgPluginVisibility.NgInternal
override def defaultConfigObject: Option[NgPluginConfig] = None
override def multiInstance: Boolean = true
override def transformsRequest: Boolean = true
override def isTransformRequestAsync: Boolean = true
override def isTransformResponseAsync: Boolean = true
override def transformRequest(
ctx: NgTransformerRequestContext
)(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Either[Result, NgPluginHttpRequest]] = {
def next(
_ctx: NgTransformerRequestContext,
plugins: Seq[NgPluginWrapper[NgRequestTransformer]],
pluginIndex: Int
): Future[Either[Result, NgPluginHttpRequest]] = {
plugins.headOption match {
case None => Right(_ctx.otoroshiRequest).vfuture
case Some(wrapper) => {
val pluginConfig: JsValue = wrapper.plugin.defaultConfig
.map(dc => dc ++ wrapper.instance.config.raw)
.getOrElse(wrapper.instance.config.raw)
val ctx = _ctx.copy(config = pluginConfig)
val debug = ctx.route.debugFlow || wrapper.instance.debug
val in: JsValue = if (debug) Json.obj("ctx" -> ctx.json) else JsNull
val item = NgReportPluginSequenceItem(
wrapper.instance.plugin,
wrapper.plugin.name,
System.currentTimeMillis(),
System.nanoTime(),
-1L,
-1L,
in,
JsNull
)
Try(wrapper.plugin.transformRequestSync(ctx.copy(idx = pluginIndex))) match {
case Failure(exception) =>
ctx.markPluginItem(
item,
ctx,
debug,
Json.obj("kind" -> "failure", "error" -> JsonHelpers.errToJson(exception))
)
ctx.report.setContext(ctx.sequence.stopSequence().json)
Left(
Results.InternalServerError(
Json.obj(
"error" -> "internal_server_error",
"error_description" -> "an error happened during request-transformation plugins phase",
"error" -> JsonHelpers.errToJson(exception)
)
)
).vfuture
case Success(Left(result)) =>
ctx.markPluginItem(
item,
ctx,
debug,
Json.obj(
"kind" -> "short-circuit",
"status" -> result.header.status,
"headers" -> result.header.headers
)
)
ctx.report.setContext(ctx.sequence.stopSequence().json)
Left(result).vfuture
case Success(Right(req_next)) if plugins.size == 1 =>
ctx.markPluginItem(item, ctx.copy(otoroshiRequest = req_next), debug, Json.obj("kind" -> "successful"))
ctx.report.setContext(ctx.sequence.stopSequence().json)
Right(req_next).vfuture
case Success(Right(req_next)) =>
ctx.markPluginItem(item, ctx.copy(otoroshiRequest = req_next), debug, Json.obj("kind" -> "successful"))
next(_ctx.copy(otoroshiRequest = req_next), plugins.tail, pluginIndex - 1)
}
}
}
}
next(ctx, plugins, plugins.size)
}
}
class NgMergedResponseTransformer(plugins: Seq[NgPluginWrapper.NgSimplePluginWrapper[NgRequestTransformer]])
extends NgRequestTransformer {
override def steps: Seq[NgStep] = Seq(NgStep.TransformResponse)
override def categories: Seq[NgPluginCategory] = Seq(NgPluginCategory.Other)
override def visibility: NgPluginVisibility = NgPluginVisibility.NgInternal
override def defaultConfigObject: Option[NgPluginConfig] = None
override def multiInstance: Boolean = true
override def transformsResponse: Boolean = true
override def isTransformRequestAsync: Boolean = true
override def isTransformResponseAsync: Boolean = true
override def transformResponse(
ctx: NgTransformerResponseContext
)(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Either[Result, NgPluginHttpResponse]] = {
def next(
_ctx: NgTransformerResponseContext,
plugins: Seq[NgPluginWrapper[NgRequestTransformer]],
pluginIndex: Int
): Future[Either[Result, NgPluginHttpResponse]] = {
plugins.headOption match {
case None => Right(_ctx.otoroshiResponse).vfuture
case Some(wrapper) => {
val pluginConfig: JsValue = wrapper.plugin.defaultConfig
.map(dc => dc ++ wrapper.instance.config.raw)
.getOrElse(wrapper.instance.config.raw)
val ctx = _ctx.copy(config = pluginConfig, idx = pluginIndex)
val debug = ctx.route.debugFlow || wrapper.instance.debug
val in: JsValue = if (debug) Json.obj("ctx" -> ctx.json) else JsNull
val item = NgReportPluginSequenceItem(
wrapper.instance.plugin,
wrapper.plugin.name,
System.currentTimeMillis(),
System.nanoTime(),
-1L,
-1L,
in,
JsNull
)
Try(wrapper.plugin.transformResponseSync(ctx)) match {
case Failure(exception) =>
ctx.markPluginItem(
item,
ctx,
debug,
Json.obj("kind" -> "failure", "error" -> JsonHelpers.errToJson(exception))
)
ctx.report.setContext(ctx.sequence.stopSequence().json)
Left(
Results.InternalServerError(
Json.obj(
"error" -> "internal_server_error",
"error_description" -> "an error happened during response-transformation plugins phase",
"error" -> JsonHelpers.errToJson(exception)
)
)
).vfuture
case Success(Left(result)) =>
ctx.markPluginItem(
item,
ctx,
debug,
Json.obj(
"kind" -> "short-circuit",
"status" -> result.header.status,
"headers" -> result.header.headers
)
)
ctx.report.setContext(ctx.sequence.stopSequence().json)
Left(result).vfuture
case Success(Right(resp_next)) if plugins.size == 1 =>
ctx.markPluginItem(item, ctx.copy(otoroshiResponse = resp_next), debug, Json.obj("kind" -> "successful"))
ctx.report.setContext(ctx.sequence.stopSequence().json)
Right(resp_next).vfuture
case Success(Right(resp_next)) =>
ctx.markPluginItem(item, ctx.copy(otoroshiResponse = resp_next), debug, Json.obj("kind" -> "successful"))
next(_ctx.copy(otoroshiResponse = resp_next), plugins.tail, pluginIndex - 1)
}
}
}
}
next(ctx, plugins, plugins.size)
}
}
class NgMergedPreRouting(plugins: Seq[NgPluginWrapper.NgSimplePluginWrapper[NgPreRouting]]) extends NgPreRouting {
override def steps: Seq[NgStep] = Seq(NgStep.PreRoute)
override def categories: Seq[NgPluginCategory] = Seq(NgPluginCategory.Other)
override def visibility: NgPluginVisibility = NgPluginVisibility.NgInternal
override def defaultConfigObject: Option[NgPluginConfig] = None
override def multiInstance: Boolean = true
override def isPreRouteAsync: Boolean = true
override def preRoute(
_ctx: NgPreRoutingContext
)(implicit env: Env, ec: ExecutionContext): Future[Either[NgPreRoutingError, Done]] = {
def next(plugins: Seq[NgPluginWrapper[NgPreRouting]], pluginIndex: Int): Future[Either[NgPreRoutingError, Done]] = {
plugins.headOption match {
case None => Right(Done).vfuture
case Some(wrapper) => {
val pluginConfig: JsValue = wrapper.plugin.defaultConfig
.map(dc => dc ++ wrapper.instance.config.raw)
.getOrElse(wrapper.instance.config.raw)
val ctx = _ctx.copy(config = pluginConfig, idx = pluginIndex)
val debug = ctx.route.debugFlow || wrapper.instance.debug
val in: JsValue = if (debug) Json.obj("ctx" -> ctx.json) else JsNull
val item = NgReportPluginSequenceItem(
wrapper.instance.plugin,
wrapper.plugin.name,
System.currentTimeMillis(),
System.nanoTime(),
-1L,
-1L,
in,
JsNull
)
Try(wrapper.plugin.preRouteSync(ctx)) match {
case Failure(exception) =>
ctx.markPluginItem(
item,
ctx,
debug,
Json.obj("kind" -> "failure", "error" -> JsonHelpers.errToJson(exception))
)
ctx.report.setContext(ctx.sequence.stopSequence().json)
Left(
NgPreRoutingErrorWithResult(
Results.InternalServerError(
Json.obj(
"error" -> "internal_server_error",
"error_description" -> "an error happened during pre-routing plugins phase"
// "error_stack" -> JsonHelpers.errToJson(exception)
)
)
)
).vfuture
case Success(Left(err)) =>
val result = err.result
ctx.markPluginItem(
item,
ctx,
debug,
Json.obj(
"kind" -> "short-circuit",
"status" -> result.header.status,
"headers" -> result.header.headers
)
)
ctx.report.setContext(ctx.sequence.stopSequence().json)
Left(NgPreRoutingErrorWithResult(result)).vfuture
case Success(Right(_)) if plugins.size == 1 =>
ctx.markPluginItem(item, ctx, debug, Json.obj("kind" -> "successful"))
ctx.report.setContext(ctx.sequence.stopSequence().json)
Right(Done).vfuture
case Success(Right(_)) =>
ctx.markPluginItem(item, ctx, debug, Json.obj("kind" -> "successful"))
next(plugins.tail, pluginIndex - 1)
}
}
}
}
next(plugins, plugins.size)
}
}
class NgMergedAccessValidator(plugins: Seq[NgPluginWrapper.NgSimplePluginWrapper[NgAccessValidator]])
extends NgAccessValidator {
override def steps: Seq[NgStep] = Seq(NgStep.ValidateAccess)
override def categories: Seq[NgPluginCategory] = Seq(NgPluginCategory.Other)
override def visibility: NgPluginVisibility = NgPluginVisibility.NgInternal
override def defaultConfigObject: Option[NgPluginConfig] = None
override def multiInstance: Boolean = true
override def isAccessAsync: Boolean = true
override def access(_ctx: NgAccessContext)(implicit env: Env, ec: ExecutionContext): Future[NgAccess] = {
def next(plugins: Seq[NgPluginWrapper[NgAccessValidator]], pluginIndex: Int): Future[NgAccess] = {
plugins.headOption match {
case None => NgAccess.NgAllowed.vfuture
case Some(wrapper) => {
val pluginConfig: JsValue = wrapper.plugin.defaultConfig
.map(dc => dc ++ wrapper.instance.config.raw)
.getOrElse(wrapper.instance.config.raw)
val ctx = _ctx.copy(config = pluginConfig, idx = pluginIndex)
val debug = ctx.route.debugFlow || wrapper.instance.debug
val in: JsValue = if (debug) Json.obj("ctx" -> ctx.json) else JsNull
val item = NgReportPluginSequenceItem(
wrapper.instance.plugin,
wrapper.plugin.name,
System.currentTimeMillis(),
System.nanoTime(),
-1L,
-1L,
in,
JsNull
)
Try(wrapper.plugin.accessSync(ctx)) match {
case Failure(exception) =>
ctx.markPluginItem(
item,
ctx,
debug,
Json.obj("kind" -> "failure", "error" -> JsonHelpers.errToJson(exception))
)
ctx.report.setContext(ctx.sequence.stopSequence().json)
NgAccess
.NgDenied(
Results.InternalServerError(
Json.obj(
"error" -> "internal_server_error",
"error_description" -> "an error happened during access plugins phase"
// "error_stack" -> JsonHelpers.errToJson(exception)
)
)
)
.vfuture
case Success(NgAccess.NgDenied(result)) =>
ctx.markPluginItem(item, ctx, debug, Json.obj("kind" -> "denied", "status" -> result.header.status))
ctx.report.setContext(ctx.sequence.stopSequence().json)
NgAccess.NgDenied(result).vfuture
case Success(NgAccess.NgAllowed) if plugins.size == 1 =>
ctx.markPluginItem(item, ctx, debug, Json.obj("kind" -> "allowed"))
ctx.report.setContext(ctx.sequence.stopSequence().json)
NgAccess.NgAllowed.vfuture
case Success(NgAccess.NgAllowed) =>
ctx.markPluginItem(item, ctx, debug, Json.obj("kind" -> "allowed"))
next(plugins.tail, pluginIndex - 1)
}
}
}
}
next(plugins, plugins.size)
}
}
case class NgWebsocketPluginContext(
config: JsValue,
snowflake: String,
idx: Int = 0,
request: RequestHeader,
route: NgRoute,
attrs: TypedMap,
target: Target
) extends NgCachedConfigContext {
def wasmJson: JsValue = json.asObject ++ Json.obj("route" -> route.json)
def json: JsValue = Json.obj(
"snowflake" -> snowflake,
"idx" -> idx,
"request" -> JsonHelpers.requestToJson(request),
"config" -> config,
"target" -> target.json,
"attrs" -> attrs.json
)
}
sealed trait WebsocketMessage {
def bytes()(implicit m: Materializer, ec: ExecutionContext): Future[ByteString]
def str()(implicit m: Materializer, ec: ExecutionContext): Future[String]
def size()(implicit m: Materializer, ec: ExecutionContext): Future[Int]
def isBinary: Boolean
def isText: Boolean = !isBinary
def asPlay(implicit env: Env): Future[play.api.http.websocket.Message]
def asAkka(implicit env: Env): Future[akka.http.scaladsl.model.ws.Message]
}
object WebsocketMessage {
case class AkkaMessage(data: akka.http.scaladsl.model.ws.Message) extends WebsocketMessage {
override def bytes()(implicit m: Materializer, ec: ExecutionContext): Future[ByteString] = data match {
case akka.http.scaladsl.model.ws.TextMessage.Strict(text) => text.byteString.future
case akka.http.scaladsl.model.ws.TextMessage.Streamed(source) =>
source.runFold(ByteString.empty)((concat, str) => concat ++ str.byteString)
case akka.http.scaladsl.model.ws.BinaryMessage.Strict(data) => data.future
case akka.http.scaladsl.model.ws.BinaryMessage.Streamed(source) =>
source
.runFold(ByteString.empty)((concat, str) => concat ++ str)
case _ => ByteString.empty.future
}
override def str()(implicit m: Materializer, ec: ExecutionContext): Future[String] = data match {
case akka.http.scaladsl.model.ws.TextMessage.Strict(text) => text.future
case akka.http.scaladsl.model.ws.TextMessage.Streamed(source) =>
source.runFold("")((concat, str) => concat + str)
case akka.http.scaladsl.model.ws.BinaryMessage.Strict(data) => data.utf8String.future
case akka.http.scaladsl.model.ws.BinaryMessage.Streamed(source) =>
source
.runFold(ByteString.empty)((concat, str) => concat ++ str)
.map(_.utf8String)
case _ => "".future
}
override def size()(implicit m: Materializer, ec: ExecutionContext): Future[Int] = data match {
case akka.http.scaladsl.model.ws.TextMessage.Strict(text) => text.length.future
case akka.http.scaladsl.model.ws.TextMessage.Streamed(source) =>
source.runFold("")((concat, str) => concat + str).map(_.length)
case akka.http.scaladsl.model.ws.BinaryMessage.Strict(data) => data.size.future
case akka.http.scaladsl.model.ws.BinaryMessage.Streamed(source) =>
source
.runFold(ByteString.empty)((concat, str) => concat ++ str)
.map(_.size)
case _ => 0.future
}
override def isBinary: Boolean = !data.isText
override def asPlay(implicit env: Env): Future[play.api.http.websocket.Message] = {
implicit val ec = env.otoroshiExecutionContext
implicit val mat = env.otoroshiMaterializer
data match {
case akka.http.scaladsl.model.ws.TextMessage.Strict(text) => PlayWSTextMessage(text).vfuture
case akka.http.scaladsl.model.ws.TextMessage.Streamed(source) =>
source.runFold("")((concat, str) => concat + str).map(text => PlayWSTextMessage(text))
case akka.http.scaladsl.model.ws.BinaryMessage.Strict(data) => PlayWSBinaryMessage(data).vfuture
case akka.http.scaladsl.model.ws.BinaryMessage.Streamed(source) =>
source
.runFold(ByteString.empty)((concat, str) => concat ++ str)
.map(data => PlayWSBinaryMessage(data))
case other => throw new RuntimeException(s"Unkown message type ${other}")
}
}
override def asAkka(implicit env: Env): Future[akka.http.scaladsl.model.ws.Message] = {
data.vfuture
}
}
case class PlayMessage(data: play.api.http.websocket.Message) extends WebsocketMessage {
override def bytes()(implicit m: Materializer, ec: ExecutionContext): Future[ByteString] = data match {
case PlayWSTextMessage(data) => data.byteString.vfuture
case PlayWSBinaryMessage(data) => data.vfuture
case CloseMessage(_, _) => ByteString.empty.vfuture
case PingMessage(data) => data.vfuture
case PongMessage(data) => data.vfuture
}
override def str()(implicit m: Materializer, ec: ExecutionContext): Future[String] = (data match {
case PlayWSTextMessage(data) => data
case PlayWSBinaryMessage(data) => data.utf8String
case CloseMessage(_, _) => ""
case PingMessage(data) => data.utf8String
case PongMessage(data) => data.utf8String
}).future
override def size()(implicit m: Materializer, ec: ExecutionContext): Future[Int] = (data match {
case PlayWSTextMessage(data) => data.length
case PlayWSBinaryMessage(data) => data.size
case CloseMessage(_, _) => 0
case PingMessage(data) => data.size
case PongMessage(data) => data.size
}).future
override def isBinary: Boolean = data.isInstanceOf[play.api.http.websocket.BinaryMessage]
override def asPlay(implicit env: Env): Future[play.api.http.websocket.Message] = {
data.vfuture
}
override def asAkka(implicit env: Env): Future[akka.http.scaladsl.model.ws.Message] = {
data match {
case msg: PlayWSBinaryMessage => akka.http.scaladsl.model.ws.BinaryMessage(msg.data).vfuture
case msg: PlayWSTextMessage => akka.http.scaladsl.model.ws.TextMessage(msg.data).vfuture
case msg: PingMessage => akka.http.scaladsl.model.ws.BinaryMessage(msg.data).vfuture
case msg: PongMessage => akka.http.scaladsl.model.ws.BinaryMessage(msg.data).vfuture
case other => throw new RuntimeException(s"Unkown message type ${other}")
}
}
}
}
case class NgWebsocketResponse(
result: NgAccess = NgAccess.NgAllowed,
statusCode: Option[Int] = None,
reason: Option[String] = None
)
object NgWebsocketResponse {
def default: Future[NgWebsocketResponse] = NgWebsocketResponse().future
def error(ctx: NgWebsocketPluginContext, message: WebsocketMessage, statusCode: Int, reason: String)(implicit
env: Env,
ec: ExecutionContext
): Future[NgWebsocketResponse] = {
implicit val m: Materializer = env.otoroshiMaterializer
(for {
frame <- message.str
size <- message.size()
} yield (frame, size))
.collect { case (frame, frameSize) =>
NgWebsocketResponse.denied(
Errors
.craftWebsocketResponseResultSync(
frame = frame,
frameSize = frameSize,
statusCode = statusCode.some,
reason = reason.some,
req = ctx.request,
route = ctx.route,
target = ctx.target
),
statusCode,
reason
)
}
}
private def denied(result: Result, statusCode: Int, reason: String) =
NgWebsocketResponse(NgAccess.NgDenied(result), statusCode.some, reason.some)
}
trait NgWebsocketPlugin extends NgPlugin {
def rejectStrategy(ctx: NgWebsocketPluginContext): RejectStrategy = RejectStrategy.Drop
def onRequestFlow: Boolean = false
def onResponseFlow: Boolean = false
def onRequestMessage(ctx: NgWebsocketPluginContext, message: WebsocketMessage)(implicit
env: Env,
ec: ExecutionContext
): Future[Either[NgWebsocketError, WebsocketMessage]] = message.rightf
def onResponseMessage(ctx: NgWebsocketPluginContext, message: WebsocketMessage)(implicit
env: Env,
ec: ExecutionContext
): Future[Either[NgWebsocketError, WebsocketMessage]] = message.rightf
}
trait NgWebsocketBackendPlugin extends NgPlugin {
import play.api.http.websocket.{Message => PlayWSMessage}
def callBackendOrError(
ctx: NgWebsocketPluginContext
)(implicit
env: Env,
ec: ExecutionContext
): Future[Either[NgProxyEngineError, Flow[PlayWSMessage, PlayWSMessage, _]]] = {
callBackend(ctx).rightf
}
def callBackend(
ctx: NgWebsocketPluginContext
)(implicit env: Env, ec: ExecutionContext): Flow[PlayWSMessage, PlayWSMessage, _] = {
Flow.fromSinkAndSource(Sink.ignore, Source.empty)
}
}
trait NgWebsocketValidatorPlugin extends NgWebsocketPlugin {}
case class NgWebsocketError(
statusCode: Option[Int] = None,
reason: Option[String] = None,
rejectStrategy: Option[RejectStrategy]
)
object NgWebsocketError {
def apply(statusCode: Int, reason: String): NgWebsocketError = NgWebsocketError(statusCode.some, reason.some, None)
}
class YesWebsocketBackend extends NgWebsocketBackendPlugin {
private val logger = Logger("otoroshi-yes-websocket-plugin")
override def name: String = "Yes"
override def description: Option[String] = "Outputs Ys to the client".some
override def core: Boolean = false
override def visibility: NgPluginVisibility = NgPluginVisibility.NgUserLand
override def categories: Seq[NgPluginCategory] = Seq(NgPluginCategory.Websocket)
override def steps: Seq[NgStep] = Seq(NgStep.CallBackend)
override def defaultConfigObject: Option[NgPluginConfig] = None
override def noJsForm: Boolean = true
override def callBackendOrError(
ctx: NgWebsocketPluginContext
)(implicit env: Env, ec: ExecutionContext): Future[Either[NgProxyEngineError, Flow[Message, Message, _]]] = {
implicit val mat = env.otoroshiMaterializer
ctx.request.getQueryString("fail") match {
case Some("yes") =>
NgProxyEngineError.NgResultProxyEngineError(Results.InternalServerError(Json.obj("error" -> "fail !"))).leftf
case _ => {
Flow
.fromSinkAndSource[Message, Message](
Sink.foreach { m =>
val message = WebsocketMessage.PlayMessage(m)
message.str().map { str =>
logger.info(s"from client: ${str}")
}
},
Source
.tick(0.second, 300.milliseconds, ())
.map(_ => PlayWSTextMessage("y"))
)
.rightf
}
}
}
}
trait NgIncomingRequestValidator extends NgPlugin {
def access(ctx: NgIncomingRequestValidatorContext)(implicit env: Env, ec: ExecutionContext): Future[NgAccess] =
NgAccess.NgAllowed.vfuture
}
case class NgIncomingRequestValidatorContext(
snowflake: String,
request: RequestHeader,
config: JsValue,
attrs: TypedMap,
globalConfig: JsValue,
report: NgExecutionReport,
sequence: NgReportPluginSequence,
markPluginItem: Function4[NgReportPluginSequenceItem, NgIncomingRequestValidatorContext, Boolean, JsValue, Unit],
idx: Int = 0
) {
def json: JsValue = Json.obj(
"snowflake" -> snowflake,
// "route" -> route.json,
"request" -> JsonHelpers.requestToJson(request),
"config" -> config,
"global_config" -> globalConfig,
"attrs" -> attrs.json
)
def wasmJson(implicit env: Env, ec: ExecutionContext): JsObject = json.asObject
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy