next.proxy.engine.scala Maven / Gradle / Ivy
package otoroshi.next.proxy
import akka.Done
import akka.stream.Materializer
import akka.stream.scaladsl.{Flow, Sink, Source}
import akka.util.ByteString
import com.github.blemale.scaffeine.{Cache, Scaffeine}
import org.joda.time.DateTime
import otoroshi.el.TargetExpressionLanguage
import otoroshi.env.Env
import otoroshi.events._
import otoroshi.gateway._
import otoroshi.models._
import otoroshi.netty.{NettyHttpClient, NettyRequestKeys}
import otoroshi.next.events.TrafficCaptureEvent
import otoroshi.next.extensions.HttpListenerNames
import otoroshi.next.models._
import otoroshi.next.plugins.{HeaderTooLongAlert, Keys}
import otoroshi.next.plugins.api._
import otoroshi.next.proxy.NgProxyEngineError._
import otoroshi.next.utils.{FEither, JsonHelpers}
import otoroshi.script.RequestHandler
import otoroshi.security.IdGenerator
import otoroshi.utils.http.Implicits._
import otoroshi.utils.http.RequestImplicits._
import otoroshi.utils.http.WSCookieWithSameSite
import otoroshi.utils.streams.MaxLengthLimiter
import otoroshi.utils.syntax.implicits._
import otoroshi.utils.{RegexPool, TypedMap, UrlSanitizer}
import play.api.{mvc, Logger}
import play.api.http.{HttpChunk, HttpEntity}
import play.api.http.websocket.{Message => PlayWSMessage}
import play.api.libs.json._
import play.api.libs.streams.ActorFlow
import play.api.libs.ws.WSRequest
import play.api.mvc.Results.Status
import play.api.mvc._
import play.api.mvc.request.RequestAttrKey
import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger, AtomicLong, AtomicReference}
import scala.concurrent.duration.DurationInt
import scala.concurrent.{Await, ExecutionContext, Future, Promise}
import scala.util.{Failure, Success}
case class ProxyEngineConfig(
enabled: Boolean,
domains: Seq[String],
denyDomains: Seq[String],
reporting: Boolean,
pluginMerge: Boolean,
exportReporting: Boolean,
debug: Boolean,
debugHeaders: Boolean,
applyLegacyChecks: Boolean,
capture: Boolean,
captureMaxEntitySize: Long,
routingStrategy: RoutingStrategy
) {
lazy val useTree: Boolean = routingStrategy == RoutingStrategy.Tree
def json: JsValue = Json.obj(
"enabled" -> enabled,
"domains" -> domains,
"deny_domains" -> denyDomains,
"reporting" -> reporting,
"merge_sync_steps" -> pluginMerge,
"export_reporting" -> exportReporting,
"apply_legacy_checks" -> applyLegacyChecks,
"debug" -> debug,
"capture" -> capture,
"captureMaxEntitySize" -> captureMaxEntitySize,
"debug_headers" -> debugHeaders,
"routing_strategy" -> routingStrategy.json
)
}
object ProxyEngineConfig {
lazy val default: ProxyEngineConfig = ProxyEngineConfig(
enabled = true,
domains = Seq("*"),
denyDomains = Seq.empty,
reporting = true,
pluginMerge = true,
exportReporting = false,
debug = false,
debugHeaders = false,
applyLegacyChecks = true,
capture = false,
captureMaxEntitySize = 4 * 1024 * 1024,
routingStrategy = RoutingStrategy.Tree
)
def parse(config: JsValue, env: Env): ProxyEngineConfig = {
val enabled = config.select("enabled").asOpt[Boolean].getOrElse(true)
val domains =
if (enabled) config.select("domains").asOpt[Seq[String]].getOrElse(Seq("*"))
else Seq.empty[String]
val denyDomains =
if (enabled) config.select("deny_domains").asOpt[Seq[String]].getOrElse(Seq.empty) else Seq.empty[String]
val reporting = config.select("reporting").asOpt[Boolean].getOrElse(true)
val pluginMerge = config
.select("merge_sync_steps")
.asOpt[Boolean]
.orElse(
env.configuration.getOptionalWithFileSupport[Boolean]("otoroshi.next.plugins.merge-sync-steps")
)
.getOrElse(true)
val applyLegacyChecks = config
.select("apply_legacy_checks")
.asOpt[Boolean]
.orElse(
env.configuration.getOptionalWithFileSupport[Boolean]("otoroshi.next.plugins.apply-legacy-checks")
)
.getOrElse(true)
val routingStrategy = config.select("routing_strategy").asOpt[String].getOrElse("tree")
val exportReporting = config
.select("export_reporting")
.asOpt[Boolean]
.orElse(
env.configuration.getOptionalWithFileSupport[Boolean]("otoroshi.next.export-reporting")
)
.getOrElse(false)
val debug = config.select("debug").asOpt[Boolean].getOrElse(false)
val capture = config.select("capture").asOpt[Boolean].getOrElse(false)
val captureMaxEntitySize: Long = config.select("captureMaxEntitySize").asOpt[Long].getOrElse(4 * 1024 * 1024)
val debugHeaders = config.select("debug_headers").asOpt[Boolean].getOrElse(false)
ProxyEngineConfig(
enabled = enabled,
domains = domains,
denyDomains = denyDomains,
reporting = reporting,
pluginMerge = pluginMerge,
exportReporting = exportReporting,
debug = debug,
capture = capture,
captureMaxEntitySize = captureMaxEntitySize,
debugHeaders = debugHeaders,
applyLegacyChecks = applyLegacyChecks,
routingStrategy = RoutingStrategy.parse(routingStrategy)
)
}
}
object ProxyEngine {
def configRoot: String = "NextGenProxyEngine"
}
class ProxyEngine() extends RequestHandler {
def badDefaultRoutingHttp(req: Request[Source[ByteString, _]]): Future[Result] =
Results.InternalServerError("bad default routing").vfuture
def badDefaultRoutingWs(req: RequestHeader): Future[Either[Result, Flow[PlayWSMessage, PlayWSMessage, _]]] =
Results.InternalServerError("bad default routing").left.vfuture
private val logger = Logger("otoroshi-next-gen-proxy-engine")
private val fakeFailureIndicator = new AtomicBoolean(false)
private val reqCounter = new AtomicInteger(0)
private val enabledRef = new AtomicBoolean(true)
private val enabledDomains = new AtomicReference(Seq.empty[String])
private val configCache: Cache[String, ProxyEngineConfig] = Scaffeine()
.expireAfterWrite(10.seconds)
.maximumSize(2)
.build()
private val headersOutStatic = Seq(
"Keep-Alive",
"Transfer-Encoding",
"Content-Length",
"Content-Type",
"Raw-Request-Uri",
"Remote-Address",
"Timeout-Access",
"Tls-Session-Info",
"Set-Cookie"
)
private val headersInStatic = Seq(
"X-Forwarded-For",
"X-Forwarded-Proto",
"X-Forwarded-Protocol",
"Raw-Request-Uri",
"Remote-Address",
"Timeout-Access",
"Tls-Session-Info"
)
override def name: String = "Otoroshi next proxy engine (experimental)"
override def visibility: NgPluginVisibility = NgPluginVisibility.NgUserLand
override def categories: Seq[NgPluginCategory] = Seq(NgPluginCategory.Experimental)
override def steps: Seq[NgStep] = Seq(NgStep.HandlesRequest)
override def description: Option[String] =
"""
|This plugin holds the next generation otoroshi proxy engine implementation. This engine is **experimental** and may not work as expected !
|
|You can active this plugin only on some domain names so you can easily A/B test the new engine.
|The new proxy engine is designed to be more reactive and more efficient generally.
|It is also designed to be very efficient on path routing where it wasn't the old engines strong suit.
|
|The idea is to only rely on plugins to work and avoid losing time with features that are not used in service descriptors.
|An automated conversion happens for every service descriptor. If the exposed domain is handled by this plugin, it will be served by this plugin.
|This plugin introduces new entities that will replace (one day maybe) service descriptors:
|
| - `routes`: a unique routing rule based on hostname, path, method and headers that will execute a bunch of plugins
| - `route-compositions`: multiple routing rules based on hostname, path, method and headers that will execute the same list of plugins
| - `backends`: a list of targets to contact a backend
|
|as an example, let say you want to use the new engine on your service exposed on `api.foo.bar/api`.
|To do that, just add the plugin in the `global plugins` section of the danger zone, inject the default configuration,
|enabled it and in `domains` add the value `api.foo.bar` (it is possible to use `*.foo.bar` if that's what you want to do).
|The next time a request hits the `api.foo.bar` domain, the new engine will handle it instead of the old one.
|
|```json
|{
| "NextGenProxyEngine" : {
| "enabled" : true,
| "debug" : false,
| "debug_headers" : false,
| "reporting": true,
| "routing_strategy" : "tree",
| "merge_sync_steps" : true,
| "domains" : [ "api.foo.bar" ]
| }
|}
|```
|
|""".stripMargin.some
override def configRoot: Option[String] = ProxyEngine.configRoot.some
override def defaultConfig: Option[JsObject] = {
Json
.obj(
ProxyEngine.configRoot -> ProxyEngineConfig.default.json
)
.some
}
@inline
def getConfig()(implicit ec: ExecutionContext, env: Env): ProxyEngineConfig = {
configCache.get(
"config",
_ => {
val config = env.datastores.globalConfigDataStore
.latest()
.plugins
.config
.select(configRoot.get)
.asOpt[JsObject]
.map(v => ProxyEngineConfig.parse(v, env))
.getOrElse(ProxyEngineConfig.default)
enabledRef.set(config.enabled)
enabledDomains.set(config.domains)
config
}
)
}
private def otoroshiJsonError(
error: JsObject,
status: Results.Status,
route: Option[NgRoute],
attrs: TypedMap,
req: RequestHeader
)(implicit env: Env, ec: ExecutionContext): Result = {
if (env.isDev) {
logger.error(s"proxy engine error on route '${route.map(_.id).getOrElse("")}/${route
.map(_.name)
.getOrElse("")}' - ${req.method} ${req.path}: ${error.prettify}")
}
Errors.craftResponseResultSync(
message = error.select("error_description").asOpt[String].getOrElse("an error occurred !"),
status = status,
req = req,
maybeCauseId = error.select("error").asOpt[String],
attrs = attrs,
maybeRoute = route
)
}
override def handledDomains(implicit ec: ExecutionContext, env: Env): Seq[String] = {
val config = getConfig()
enabledDomains.get()
}
override def handle(
request: Request[Source[ByteString, _]],
defaultRouting: Request[Source[ByteString, _]] => Future[Result]
)(implicit ec: ExecutionContext, env: Env): Future[Result] = {
handleWithListener(request, defaultRouting, false)
}
override def handleWs(
request: RequestHeader,
defaultRouting: RequestHeader => Future[Either[Result, Flow[PlayWSMessage, PlayWSMessage, _]]]
)(implicit ec: ExecutionContext, env: Env): Future[Either[Result, Flow[PlayWSMessage, PlayWSMessage, _]]] = {
handleWsWithListener(request, defaultRouting, false)
}
def handleWithListener(
request: Request[Source[ByteString, _]],
defaultRouting: Request[Source[ByteString, _]] => Future[Result],
forCurrentListenerOnly: Boolean
)(implicit ec: ExecutionContext, env: Env): Future[Result] = {
implicit val globalConfig = env.datastores.globalConfigDataStore.latest()
val config = getConfig()
val shouldNotHandle =
if (config.denyDomains.isEmpty) false
else config.denyDomains.exists(d => RegexPool.apply(d).matches(request.theDomain))
if (enabledRef.get() && !shouldNotHandle) {
handleRequest(request, config, forCurrentListenerOnly)
} else {
defaultRouting(request)
}
}
def handleWsWithListener(
request: RequestHeader,
defaultRouting: RequestHeader => Future[Either[Result, Flow[PlayWSMessage, PlayWSMessage, _]]],
forCurrentListenerOnly: Boolean
)(implicit ec: ExecutionContext, env: Env): Future[Either[Result, Flow[PlayWSMessage, PlayWSMessage, _]]] = {
implicit val globalConfig = env.datastores.globalConfigDataStore.latest()
val config = getConfig()
val shouldNotHandle =
if (config.denyDomains.isEmpty) false
else config.denyDomains.exists(d => RegexPool.apply(d).matches(request.theDomain))
if (enabledRef.get() && !shouldNotHandle) {
handleWsRequest(request, config, forCurrentListenerOnly)
} else {
defaultRouting(request)
}
}
@inline
def handleRequest(
request: Request[Source[ByteString, _]],
_config: ProxyEngineConfig,
forCurrentListenerOnly: Boolean
)(implicit
ec: ExecutionContext,
env: Env,
globalConfig: GlobalConfig
): Future[Result] = {
val start = System.currentTimeMillis()
val tryItId = request.headers.get("Otoroshi-Try-It-Request-Id")
val tryIt = tryItId.exists(id => env.proxyState.isReportEnabledFor(id))
val requestId = IdGenerator.uuid
val ProxyEngineConfig(_, _, _, reporting, pluginMerge, exportReporting, debug, debugHeaders, _, _, _, _) = _config
val useTree = _config.useTree
implicit val report = NgExecutionReport(requestId, reporting)
report.start("start-handling")
implicit val mat = env.otoroshiMaterializer
val snowflake = env.snowflakeGenerator.nextIdStr()
val callDate = DateTime.now()
val requestTimestamp = callDate.toString("yyyy-MM-dd'T'HH:mm:ss.SSSZZ")
val reqNumber = reqCounter.incrementAndGet()
val counterIn = new AtomicLong(0L)
val counterOut = new AtomicLong(0L)
val responseEndPromise = Promise[Done]()
val currentListener = request.attrs.get(NettyRequestKeys.ListenerIdKey).getOrElse(HttpListenerNames.Standard)
implicit val attrs = TypedMap.empty.put(
otoroshi.next.plugins.Keys.ReportKey -> report,
otoroshi.plugins.Keys.RequestKey -> request,
otoroshi.plugins.Keys.RequestNumberKey -> reqNumber,
otoroshi.plugins.Keys.SnowFlakeKey -> snowflake,
otoroshi.plugins.Keys.RequestTimestampKey -> callDate,
otoroshi.plugins.Keys.RequestStartKey -> start,
otoroshi.plugins.Keys.RequestWebsocketKey -> false,
otoroshi.plugins.Keys.RequestCounterInKey -> counterIn,
otoroshi.plugins.Keys.RequestCounterOutKey -> counterOut,
otoroshi.plugins.Keys.ResponseEndPromiseKey -> responseEndPromise,
otoroshi.plugins.Keys.ForCurrentListenerOnlyKey -> forCurrentListenerOnly,
otoroshi.plugins.Keys.CurrentListenerKey -> currentListener
)
val elCtx: Map[String, String] = Map(
"requestId" -> snowflake,
"requestSnowflake" -> snowflake,
"requestTimestamp" -> requestTimestamp
)
attrs.put(otoroshi.plugins.Keys.ElCtxKey -> elCtx)
val global_plugins__ = NgPlugins.readFrom(globalConfig.plugins.config.select("ng"))
report.markDoneAndStart("check-concurrent-requests")
(for {
_ <- handleConcurrentRequest(request)
_ = report.markDoneAndStart("validate-incoming-request")
_ <- applyIncomingRequestValidation(request, snowflake)
_ = report.markDoneAndStart("find-route")
route <- findRoute(useTree, request, request.body, global_plugins__, tryIt)
_ <- handleRelayTraffic(route, request, request.body)
config = (route.metadata.get("otoroshi-core-apply-legacy-checks") match {
case Some("false") => _config.copy(applyLegacyChecks = false)
case Some("true") => _config.copy(applyLegacyChecks = true)
case _ => _config
}).applyOnIf(route.capture)(_.copy(capture = true))
_ = report.markDoneAndStart("compute-plugins")
gplugs = global_plugins__
ctxPlugins = route.contextualPlugins(gplugs, pluginMerge, request).seffectOn(_.allPlugins)
_ = attrs.put(Keys.ContextualPluginsKey -> ctxPlugins)
_ = report.markDoneAndStart(
"tenant-check",
Json
.obj(
"disabled_plugins" -> ctxPlugins.disabledPlugins.map(p => JsString(p.plugin)),
"excluded_plugins" -> ctxPlugins.filteredPlugins.map(p => JsString(p.plugin)),
"included_plugins" -> ctxPlugins.allPlugins.map(p => JsString(p.plugin))
)
.some
)
_ <- handleTenantCheck(route, request)
_ = report.markDoneAndStart("check-global-maintenance")
_ <- checkGlobalMaintenance(route, request, config)
_ = report.markDoneAndStart("call-before-request-callbacks")
_ <- callPluginsBeforeRequestCallback(snowflake, request, route, ctxPlugins)
_ = report.markDoneAndStart("extract-tracking-id")
_ = extractTrackingId(snowflake, request, reqNumber, route)
_ = report.markDoneAndStart("call-pre-route-plugins")
_ <- callPreRoutePlugins(snowflake, request, route, ctxPlugins)
_ = report.markDoneAndStart("call-access-validator-plugins")
_ <- callAccessValidatorPlugins(snowflake, request, route, ctxPlugins)
// _ = report.markDoneAndStart("update-apikey-quotas")
// _ <- updateApikeyQuotas(config)
_ = report.markDoneAndStart(
"handle-legacy-checks",
attrs
.get(otoroshi.plugins.Keys.ApiKeyRemainingQuotasKey)
.map(remQuotas => Json.obj("remaining_quotas" -> remQuotas.toJson))
)
_ <- handleLegacyChecks(request, route, config)
_ = report.markDoneAndStart("choose-backend")
result <- callTarget(snowflake, reqNumber, request, route) {
case sb @ NgSelectedBackendTarget(backend, attempts, alreadyFailed, cbStart) =>
report.markDoneAndStart("transform-request", Json.obj("backend" -> backend.json).some)
for {
finalRequest <-
callRequestTransformer(snowflake, request, request.body, route, backend, ctxPlugins)
_ = report.markDoneAndStart("call-backend")
response <- callBackend(
snowflake,
noBackendCallerPlugin = false,
request,
finalRequest,
route,
backend,
ctxPlugins,
config
)
_ = report.markDoneAndStart("transform-response")
finalResp <-
callResponseTransformer(snowflake, request, response, route, backend, ctxPlugins)
_ = report.markDoneAndStart("stream-response")
clientResp <-
streamResponse(snowflake, request, finalRequest, response, finalResp, route, backend, config)
_ = report.markDoneAndStart("trigger-analytics")
_ <- triggerProxyDone(snowflake, request, response, finalRequest, finalResp, route, backend, sb)
} yield clientResp
}
} yield {
result
}).value
.flatMap {
case Left(error) =>
report.markDoneAndStart("rendering-intermediate-result").markSuccess()
attrs.get(otoroshi.next.plugins.Keys.ResponseAddHeadersKey) match {
case None => error.asResult()
case Some(addHeaders) => error.asResult().map(r => r.withHeaders(addHeaders: _*))
}
case Right(result) =>
report.markSuccess()
result.vfuture
}
.recover { case t: Throwable =>
logger.error("last-recover", t)
report.markFailure("last-recover", t)
otoroshiJsonError(
Json
.obj("error" -> "internal_server_error", "error_description" -> t.getMessage)
.applyOnIf(env.isDev) { obj => obj ++ Json.obj("report" -> report.json) },
Results.InternalServerError,
attrs.get(otoroshi.next.plugins.Keys.RouteKey),
attrs,
request
)
}
.applyOnWithOpt(attrs.get(Keys.ResultTransformerKey)) { case (future, transformer) =>
future.flatMap(transformer)
}
.andThen { case _ =>
report.markOverheadOut()
report.markDurations()
closeCurrentRequest(env)
attrs.get(Keys.RouteKey).foreach { route =>
attrs
.get(Keys.ContextualPluginsKey)
.foreach(ctxplgs => callPluginsAfterRequestCallback(snowflake, request, route, ctxplgs))
handleHighOverhead(request, route.some)
if (tryIt) {
tryItId.foreach(id => env.proxyState.addReport(id, report))
}
if (exportReporting || route.exportReporting) {
RequestFlowReport(report, route).toAnalytics()
}
}
}
.applyOnIf( /*env.isDev && */ (debug || debugHeaders))(_.map { res =>
val addHeaders =
if (reporting && debugHeaders)
Seq(
"x-otoroshi-request-overhead" -> report.overheadStr,
"x-otoroshi-request-overhead-in" -> report.overheadInStr,
"x-otoroshi-request-overhead-out" -> report.overheadOutStr,
"x-otoroshi-request-duration" -> report.gdurationStr,
"x-otoroshi-request-call-duration" -> report.getStep("call-backend").map(_.durationStr).getOrElse("--"),
"x-otoroshi-request-find-route-duration" -> report
.getStep("find-route")
.map(_.durationStr)
.getOrElse("--"),
"x-otoroshi-request-state" -> report.state.name,
"x-otoroshi-request-creation" -> report.creation.toString,
"x-otoroshi-request-termination" -> report.termination.toString
).applyOnIf(report.state == NgExecutionReportState.Failed) { seq =>
seq :+ (
"x-otoroshi-request-failure" ->
report
.getStep("request-failure")
.flatMap(_.ctx.select("error").select("message").asOpt[String])
.getOrElse("--")
)
}
else Seq.empty
// if (debug) logger.info(report.json.prettify)
// if (reporting && report.getStep("find-route").flatMap(_.ctx.select("found_route").select("debug_flow").asOpt[Boolean]).getOrElse(false)) {
// java.nio.file.Files.writeString(new java.io.File("./request-debug.json").toPath, report.json.prettify)
// }
res.withHeaders(addHeaders: _*)
})
.map { result =>
result.copy(body = result.body match {
case HttpEntity.NoEntity =>
responseEndPromise.trySuccess(Done)
HttpEntity.NoEntity
case b @ HttpEntity.Strict(_, _) =>
responseEndPromise.trySuccess(Done)
b
case HttpEntity.Streamed(source, length, typ) =>
if (length.contains(0)) {
responseEndPromise.trySuccess(Done)
HttpEntity.NoEntity
} else {
HttpEntity.Streamed(source.alsoTo(Sink.onComplete(_ => responseEndPromise.trySuccess(Done))), length, typ)
}
case HttpEntity.Chunked(source, typ) =>
HttpEntity.Chunked(source.alsoTo(Sink.onComplete(_ => responseEndPromise.trySuccess(Done))), typ)
})
}
}
@inline
def handleWsRequest(request: RequestHeader, _config: ProxyEngineConfig, forCurrentListenerOnly: Boolean)(implicit
ec: ExecutionContext,
env: Env,
globalConfig: GlobalConfig
): Future[Either[Result, Flow[PlayWSMessage, PlayWSMessage, _]]] = {
val start = System.currentTimeMillis()
val tryItId = request.headers.get("Otoroshi-Try-It-Request-Id")
val tryIt = tryItId.exists(id => env.proxyState.isReportEnabledFor(id))
val requestId = IdGenerator.uuid
val ProxyEngineConfig(_, _, _, reporting, pluginMerge, exportReporting, _, _, _, _, _, _) = _config
val useTree = _config.useTree
implicit val report = NgExecutionReport(requestId, reporting)
report.start("start-handling")
implicit val mat = env.otoroshiMaterializer
val snowflake = env.snowflakeGenerator.nextIdStr()
val callDate = DateTime.now()
val requestTimestamp = callDate.toString("yyyy-MM-dd'T'HH:mm:ss.SSSZZ")
val reqNumber = reqCounter.incrementAndGet()
val counterIn = new AtomicLong(0L)
val counterOut = new AtomicLong(0L)
implicit val attrs = TypedMap.empty.put(
otoroshi.next.plugins.Keys.ReportKey -> report,
otoroshi.plugins.Keys.RequestKey -> request,
otoroshi.plugins.Keys.RequestNumberKey -> reqNumber,
otoroshi.plugins.Keys.SnowFlakeKey -> snowflake,
otoroshi.plugins.Keys.RequestTimestampKey -> callDate,
otoroshi.plugins.Keys.RequestStartKey -> start,
otoroshi.plugins.Keys.RequestWebsocketKey -> false,
otoroshi.plugins.Keys.RequestCounterInKey -> counterIn,
otoroshi.plugins.Keys.RequestCounterOutKey -> counterOut,
otoroshi.plugins.Keys.ForCurrentListenerOnlyKey -> forCurrentListenerOnly
)
val elCtx: Map[String, String] = Map(
"requestId" -> snowflake,
"requestSnowflake" -> snowflake,
"requestTimestamp" -> requestTimestamp
)
attrs.put(otoroshi.plugins.Keys.ElCtxKey -> elCtx)
val fakeBody = Source.empty[ByteString]
val global_plugins__ = NgPlugins.readFrom(globalConfig.plugins.config.select("ng"))
report.markDoneAndStart("check-concurrent-requests")
(for {
_ <- handleConcurrentRequest(request)
_ = report.markDoneAndStart("validate-incoming-request")
_ <- applyIncomingRequestValidation(request, snowflake)
_ = report.markDoneAndStart("find-route")
route <- findRoute(useTree, request, fakeBody, global_plugins__, tryIt)
config = route.metadata.get("otoroshi-core-apply-legacy-checks") match {
case Some("false") => _config.copy(applyLegacyChecks = false)
case Some("true") => _config.copy(applyLegacyChecks = true)
case _ => _config
}
_ = report.markDoneAndStart("compute-plugins")
ctxPlugins = route.contextualPlugins(global_plugins__, pluginMerge, request).seffectOn(_.allPlugins)
_ = attrs.put(Keys.ContextualPluginsKey -> ctxPlugins)
_ = report.markDoneAndStart(
"tenant-check",
Json
.obj(
"disabled_plugins" -> ctxPlugins.disabledPlugins.map(p => JsString(p.plugin)),
"excluded_plugins" -> ctxPlugins.filteredPlugins.map(p => JsString(p.plugin)),
"included_plugins" -> ctxPlugins.allPlugins.map(p => JsString(p.plugin))
)
.some
)
_ <- handleTenantCheck(route, request)
_ = report.markDoneAndStart("check-global-maintenance")
_ <- checkGlobalMaintenance(route, request, config)
_ = report.markDoneAndStart("call-before-request-callbacks")
_ <- callPluginsBeforeRequestCallback(snowflake, request, route, ctxPlugins)
_ = report.markDoneAndStart("extract-tracking-id")
_ = extractTrackingId(snowflake, request, reqNumber, route)
_ = report.markDoneAndStart("call-pre-route-plugins")
_ <- callPreRoutePlugins(snowflake, request, route, ctxPlugins)
_ = report.markDoneAndStart("call-access-validator-plugins")
_ <- callAccessValidatorPlugins(snowflake, request, route, ctxPlugins)
// _ = report.markDoneAndStart("update-apikey-quotas")
// _ <- updateApikeyQuotas(config)
_ = report.markDoneAndStart("handle-legacy-checks")
_ <- handleLegacyChecks(request, route, config)
_ = report.markDoneAndStart(
"choose-backend",
attrs
.get(otoroshi.plugins.Keys.ApiKeyRemainingQuotasKey)
.map(remQuotas => Json.obj("remaining_quotas" -> remQuotas.toJson))
)
result <- callWsTarget(snowflake, reqNumber, request, route) {
case sb @ NgSelectedBackendTarget(backend, attempts, alreadyFailed, cbStart) =>
report.markDoneAndStart("transform-requests", Json.obj("backend" -> backend.json).some)
for {
finalRequest <- callRequestTransformer(snowflake, request, fakeBody, route, backend, ctxPlugins)
_ = report.markDoneAndStart("call-backend")
flow <- callWsBackend(snowflake, request, finalRequest, route, backend, ctxPlugins)
_ = report.markDoneAndStart("trigger-analytics")
_ <- triggerWsProxyDone(snowflake, request, finalRequest, route, backend, sb)
} yield flow
}
} yield {
result
}).value
.flatMap {
case Left(error) =>
report.markDoneAndStart("rendering-intermediate-result").markSuccess()
error.asResult().map(r => Left(r))
case Right(flow) =>
report.markSuccess()
Right(flow).vfuture
}
.recover { case t: Throwable =>
logger.error("last-recover", t)
report.markFailure("last-recover", t)
otoroshiJsonError(
Json
.obj("error" -> "internal_server_error", "error_description" -> t.getMessage)
.applyOnIf(env.isDev) { obj => obj ++ Json.obj("report" -> report.json) },
Results.InternalServerError,
attrs.get(otoroshi.next.plugins.Keys.RouteKey),
attrs,
request
).left
}
.applyOnWithOpt(attrs.get(Keys.ResultTransformerKey)) { case (future, transformer) =>
future.flatMap {
case Left(r) => transformer(r).map(Left.apply)
case r @ Right(_) => r.vfuture
}
}
.andThen { case _ =>
report.markOverheadOut()
report.markDurations()
closeCurrentRequest(env)
attrs.get(Keys.RouteKey).foreach { route =>
callPluginsAfterRequestCallback(snowflake, request, route, attrs.get(Keys.ContextualPluginsKey).get)
handleHighOverhead(request, route.some)
if (tryIt) {
tryItId.foreach(id => env.proxyState.addReport(id, report))
}
if (exportReporting || route.exportReporting) {
RequestFlowReport(report, route).toAnalytics()
}
}
}
}
def handleRelayTraffic(route: NgRoute, req: RequestHeader, body: Source[ByteString, _])(implicit
ec: ExecutionContext,
env: Env,
report: NgExecutionReport,
globalConfig: GlobalConfig,
attrs: TypedMap,
mat: Materializer
): FEither[NgProxyEngineError, Done] = {
if (env.clusterConfig.relay.enabled) {
val location = env.clusterConfig.relay.location
val matchRack: Boolean = if (route.hasDeploymentRacks) route.deploymentRacks.contains(location.rack) else true
val matchDatacenter: Boolean =
if (route.hasDeploymentDatacenters) route.deploymentDatacenters.contains(location.datacenter) else true
val matchZone: Boolean = if (route.hasDeploymentZones) route.deploymentZones.contains(location.zone) else true
val matchRegion: Boolean =
if (route.hasDeploymentRegions) route.deploymentRegions.contains(location.region) else true
val matchProvider: Boolean =
if (route.hasDeploymentProviders) route.deploymentProviders.contains(location.provider) else true
val matching: Boolean = matchRack && matchDatacenter && matchZone && matchRegion && matchProvider
if (matching) {
FEither.right(Done)
} else {
// Here, choose zone leader and forward the call
FEither(env.datastores.clusterStateDataStore.getMembers().flatMap { members =>
val possibleLeaders = new PossibleLeaders(members, route)
val leader = possibleLeaders.chooseNext(reqCounter)
leader.call(req, body)
})
}
} else {
FEither.right(Done)
}
}
def extractTrackingId(snowflake: String, req: RequestHeader, reqNumber: Int, route: NgRoute)(implicit
attrs: TypedMap
): Unit = {
if (route.backend.loadBalancing.needTrackingCookie) {
val trackingId: String = req.cookies
.get("otoroshi-tracking")
.map(_.value)
.getOrElse(IdGenerator.uuid + "-" + reqNumber)
attrs.put(otoroshi.plugins.Keys.RequestTrackingIdKey -> trackingId)
}
}
def handleHighOverhead(req: RequestHeader, route: Option[NgRoute])(implicit
ec: ExecutionContext,
env: Env,
report: NgExecutionReport,
globalConfig: GlobalConfig,
attrs: TypedMap,
mat: Materializer
): FEither[NgProxyEngineError, Done] = {
val overhead = report.getOverheadNow()
if (overhead > env.overheadThreshold) {
HighOverheadAlert(
`@id` = env.snowflakeGenerator.nextIdStr(),
limitOverhead = env.overheadThreshold,
currentOverhead = overhead,
serviceDescriptor = route.map(_.serviceDescriptor),
target = Location(
scheme = req.theProtocol,
host = req.theHost,
uri = req.relativeUri
)
).toAnalytics()
}
FEither.right(Done)
}
def applyIncomingRequestValidation(request: RequestHeader, snowflake: String)(implicit
ec: ExecutionContext,
env: Env,
report: NgExecutionReport,
globalConfig: GlobalConfig,
attrs: TypedMap,
mat: Materializer
): FEither[NgProxyEngineError, Done] = {
if (globalConfig.incomingRequestValidators.nonEmpty) {
val all_plugins = globalConfig.incomingRequestValidators.incomingRequestValidatorPlugins(request)
var sequence = NgReportPluginSequence(
size = all_plugins.size,
kind = "incoming-request-validator-plugins",
start = System.currentTimeMillis(),
stop = 0L,
start_ns = System.nanoTime(),
stop_ns = 0L,
plugins = Seq.empty
)
def markPluginItem(
item: NgReportPluginSequenceItem,
ctx: NgIncomingRequestValidatorContext,
debug: Boolean,
result: JsValue
): Unit = {
sequence = sequence.copy(
plugins = sequence.plugins :+ item.copy(
stop = System.currentTimeMillis(),
stop_ns = System.nanoTime(),
out = Json
.obj(
"result" -> result
)
.applyOnIf(debug)(_ ++ Json.obj("ctx" -> ctx.json))
)
)
}
val _ctx = NgIncomingRequestValidatorContext(
snowflake = snowflake,
request = request,
config = Json.obj(),
globalConfig = globalConfig.plugins.config,
attrs = attrs,
report = report,
sequence = sequence,
markPluginItem = markPluginItem
)
if (all_plugins.size == 1) {
val wrapper = all_plugins.head
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 = 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
)
FEither(wrapper.plugin.access(ctx).transform {
case Failure(exception) =>
markPluginItem(item, ctx, debug, Json.obj("kind" -> "failure", "error" -> JsonHelpers.errToJson(exception)))
report.setContext(sequence.stopSequence().json)
Success(
Left(
NgResultProxyEngineError(
otoroshiJsonError(
Json
.obj(
"error" -> "internal_server_error",
"error_description" -> "an error happened during access plugins phase"
)
.applyOnIf(env.isDev) { obj => obj ++ Json.obj("jvm_error" -> JsonHelpers.errToJson(exception)) },
Results.InternalServerError,
attrs.get(otoroshi.next.plugins.Keys.RouteKey),
attrs,
request
)
)
)
)
case Success(NgAccess.NgDenied(result)) =>
markPluginItem(item, ctx, debug, Json.obj("kind" -> "denied", "status" -> result.header.status))
report.setContext(sequence.stopSequence().json)
Success(Left(NgResultProxyEngineError(result)))
case Success(NgAccess.NgAllowed) =>
markPluginItem(item, ctx, debug, Json.obj("kind" -> "allowed"))
report.setContext(sequence.stopSequence().json)
Success(Right(Done))
})
} else {
val promise = Promise[Either[NgProxyEngineError, Done]]()
def next(plugins: Seq[NgPluginWrapper[NgIncomingRequestValidator]]): Unit = {
plugins.headOption match {
case None => promise.trySuccess(Right(Done))
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 = wrapper.instance.instanceId
)
val debug = 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
)
wrapper.plugin.access(ctx).andThen {
case Failure(exception) =>
markPluginItem(
item,
ctx,
debug,
Json.obj("kind" -> "failure", "error" -> JsonHelpers.errToJson(exception))
)
report.setContext(sequence.stopSequence().json)
promise.trySuccess(
Left(
NgResultProxyEngineError(
otoroshiJsonError(
Json
.obj(
"error" -> "internal_server_error",
"error_description" -> "an error happened during access plugins phase"
)
.applyOnIf(env.isDev) { obj =>
obj ++ Json.obj("jvm_error" -> JsonHelpers.errToJson(exception))
},
Results.InternalServerError,
attrs.get(otoroshi.next.plugins.Keys.RouteKey),
attrs,
request
)
)
)
)
case Success(NgAccess.NgDenied(result)) =>
markPluginItem(item, ctx, debug, Json.obj("kind" -> "denied", "status" -> result.header.status))
report.setContext(sequence.stopSequence().json)
promise.trySuccess(Left(NgResultProxyEngineError(result)))
case Success(NgAccess.NgAllowed) if plugins.size == 1 =>
markPluginItem(item, ctx, debug, Json.obj("kind" -> "allowed"))
report.setContext(sequence.stopSequence().json)
promise.trySuccess(Right(Done))
case Success(NgAccess.NgAllowed) =>
markPluginItem(item, ctx, debug, Json.obj("kind" -> "allowed"))
next(plugins.tail)
}
}
}
}
next(all_plugins)
FEither.apply(promise.future)
}
} else {
FEither.right(Done)
}
}
def handleConcurrentRequest(request: RequestHeader)(implicit
ec: ExecutionContext,
env: Env,
report: NgExecutionReport,
globalConfig: GlobalConfig,
attrs: TypedMap,
mat: Materializer
): FEither[NgProxyEngineError, Done] = {
val currentHandledRequests = env.datastores.requestsDataStore.incrementHandledRequests()
env.metrics.markLong(s"${env.snowflakeSeed}.concurrent-requests", currentHandledRequests)
if (currentHandledRequests > globalConfig.maxConcurrentRequests) {
Audit.send(
MaxConcurrentRequestReachedEvent(
env.snowflakeGenerator.nextIdStr(),
env.env,
globalConfig.maxConcurrentRequests,
currentHandledRequests
)
)
Alerts.send(
MaxConcurrentRequestReachedAlert(
env.snowflakeGenerator.nextIdStr(),
env.env,
globalConfig.maxConcurrentRequests,
currentHandledRequests
)
)
}
if (globalConfig.limitConcurrentRequests && currentHandledRequests > globalConfig.maxConcurrentRequests) {
FEither.apply(
Errors
.craftResponseResult(
s"Cannot process more request",
Results.TooManyRequests,
request,
None,
Some("errors.cant.process.more.request"),
attrs = attrs
)
.map(r => Left(NgResultProxyEngineError(r)))
)
} else {
FEither.right(Done)
}
}
def closeCurrentRequest(env: Env): Unit = {
val requests = env.datastores.requestsDataStore.decrementHandledRequests()
env.metrics.markLong(s"${env.snowflakeSeed}.concurrent-requests", requests)
}
def handleTenantCheck(route: NgRoute, request: RequestHeader)(implicit
ec: ExecutionContext,
env: Env,
report: NgExecutionReport,
globalConfig: GlobalConfig,
attrs: TypedMap,
mat: Materializer
): FEither[NgProxyEngineError, Done] = {
if (
env.clusterConfig.mode.isWorker
&& env.clusterConfig.worker.tenants.nonEmpty
&& !env.clusterConfig.worker.tenants.contains(route.location.tenant)
) {
report.markFailure(s"this worker cannot serve tenant '${route.location.tenant.value}'")
FEither.left(
NgResultProxyEngineError(
otoroshiJsonError(
Json.obj("error" -> "not_found", "error_description" -> "no route found !"),
Results.NotFound,
attrs.get(otoroshi.next.plugins.Keys.RouteKey),
attrs,
request
)
)
)
} else {
FEither.right(Done)
}
}
def findRoute(
useTree: Boolean,
request: RequestHeader,
body: Source[ByteString, _],
global_plugins: NgPlugins,
tryIt: Boolean
)(implicit
ec: ExecutionContext,
env: Env,
report: NgExecutionReport,
globalConfig: GlobalConfig,
attrs: TypedMap,
mat: Materializer
): FEither[NgProxyEngineError, NgRoute] = {
val routers = global_plugins.routerPlugins(request)
val pluginRoute =
if (routers.nonEmpty)
routers.findFirstSome(p =>
p.plugin.findRoute(
NgRouterContext(
request = request,
config = p.instance.config.raw,
attrs = attrs
)
)
)
else None
val maybeRoute: Option[NgMatchedRoute] = pluginRoute.orElse {
if (useTree) {
env.proxyState.findRoute(request, attrs)
} else {
env.proxyState
.getDomainRoutes(request.theDomain)
.flatMap(
_.find(
_.matches(
request,
attrs,
"/",
scala.collection.mutable.HashMap.empty,
noMoreSegments = false,
skipDomainVerif = true,
skipPathVerif = false
)
)
)
.map(r => NgMatchedRoute(r))
}
}
maybeRoute match {
case Some(_route) =>
val route = if (tryIt) {
val nroute: NgRoute = _route.route.copy(debugFlow = true)
_route.copy(route = nroute)
} else {
_route
}
attrs.put(Keys.RouteKey -> route.route)
attrs.put(Keys.MatchedRouteKey -> route)
val rts: Seq[String] = attrs.get(Keys.MatchedRoutesKey).getOrElse(Seq.empty[String])
report.setContext(
Json.obj(
"found_route" -> route.route.json,
"matched_path" -> route.path,
"exact" -> route.noMoreSegments,
"params" -> route.pathParams,
"matched_routes" -> rts
)
)
FEither.right(route.route)
case None => callRequestSinkPlugins(request, body, global_plugins)
}
}
def callRequestSinkPlugins(request: RequestHeader, body: Source[ByteString, _], global_plugins: NgPlugins)(implicit
ec: ExecutionContext,
env: Env,
report: NgExecutionReport,
globalConfig: GlobalConfig,
attrs: TypedMap,
mat: Materializer
): FEither[NgProxyEngineError, NgRoute] = {
def failure(): FEither[NgProxyEngineError, NgRoute] = {
report.markFailure(s"route not found for domain: '${request.theDomain}${request.thePath}'")
FEither.left(
NgResultProxyEngineError(
otoroshiJsonError(
Json.obj("error" -> "not_found", "error_description" -> "no route found !"),
Results.NotFound,
attrs.get(otoroshi.next.plugins.Keys.RouteKey),
attrs,
request
)
)
)
}
val all_plugins = global_plugins.requestSinkPlugins(request)
// TODO - async version
// if (all_plugins.nonEmpty) {
// val wrappers = all_plugins
// .map { wrapper =>
// val ctx = NgRequestSinkContext(
// snowflake = attrs.get(otoroshi.plugins.Keys.SnowFlakeKey).get,
// request = request,
// config = wrapper.instance.config.raw,
// attrs = attrs,
// origin = NgRequestOrigin.NgReverseProxy,
// status = 404,
// message = s"route not found",
// body = body
// )
// (wrapper, ctx)
// }
// FEither.apply[NgProxyEngineError, NgRoute] (
// Future.sequence(wrappers.map { case (wrapper, ctx) => wrapper.plugin.matches(ctx)})
// .flatMap(results => {
// results.indexWhere(matched => matched) match {
// case -1 => failure().value
// case n =>
// val (wrapper, ctx) = wrappers(n)
// FEither.apply[NgProxyEngineError, NgRoute] (
// wrapper.plugin.handle (ctx).map (r => Left (NgResultProxyEngineError (r) ) )
// ).value
// }
// })
// )
// } else {
// failure()
// }
if (all_plugins.nonEmpty) {
all_plugins
.map { wrapper =>
val ctx = NgRequestSinkContext(
snowflake = attrs.get(otoroshi.plugins.Keys.SnowFlakeKey).get,
request = request,
config = wrapper.instance.config.raw,
attrs = attrs,
origin = NgRequestOrigin.NgReverseProxy,
status = 404,
message = s"route not found",
body = body
)
(wrapper, ctx)
}
.find { case (wrapper, ctx) =>
wrapper.plugin.matches(ctx)
}
.map { case (wrapper, ctx) =>
FEither.apply[NgProxyEngineError, NgRoute](
wrapper.plugin.handle(ctx).map(r => Left(NgResultProxyEngineError(r)))
)
}
.getOrElse {
failure()
}
} else {
failure()
}
}
def checkGlobalMaintenance(route: NgRoute, request: RequestHeader, config: ProxyEngineConfig)(implicit
ec: ExecutionContext,
env: Env,
report: NgExecutionReport,
globalConfig: GlobalConfig,
attrs: TypedMap,
mat: Materializer
): FEither[NgProxyEngineError, Done] = {
if (config.applyLegacyChecks) {
if (route.id != env.backOfficeServiceId && globalConfig.maintenanceMode) {
report.markFailure(s"global maintenance activated")
FEither.left(
NgResultProxyEngineError(
otoroshiJsonError(
Json
.obj("error" -> "errors.service.in.maintenance", "error_description" -> "Service in maintenance mode"),
Results.ServiceUnavailable,
attrs.get(otoroshi.next.plugins.Keys.RouteKey),
attrs,
request
)
)
)
} else {
FEither.right(Done)
}
} else {
FEither.right(Done)
}
}
def callPluginsBeforeRequestCallback(
snowflake: String,
request: RequestHeader,
route: NgRoute,
plugins: NgContextualPlugins
)(implicit
ec: ExecutionContext,
env: Env,
report: NgExecutionReport,
globalConfig: GlobalConfig,
attrs: TypedMap,
mat: Materializer
): FEither[NgProxyEngineError, Done] = {
val all_plugins = plugins.transformerPluginsWithCallbacks
if (all_plugins.nonEmpty) {
var sequence = NgReportPluginSequence(
size = all_plugins.size,
kind = "before-request-plugins",
start = System.currentTimeMillis(),
stop = 0L,
start_ns = System.nanoTime(),
stop_ns = 0L,
plugins = Seq.empty
)
val _ctx = NgBeforeRequestContext(
snowflake = snowflake,
request = request,
route = route,
config = Json.obj(),
globalConfig = globalConfig.plugins.config,
attrs = attrs
)
def markPluginItem(
item: NgReportPluginSequenceItem,
ctx: NgBeforeRequestContext,
debug: Boolean,
result: JsValue
): Unit = {
sequence = sequence.copy(
plugins = sequence.plugins :+ item.copy(
stop = System.currentTimeMillis(),
stop_ns = System.nanoTime(),
out = Json
.obj(
"not_triggered" -> plugins.tpwoCallbacks.map(_.instance.plugin),
"result" -> result
)
.applyOnIf(debug)(_ ++ Json.obj("ctx" -> ctx.json))
)
)
}
if (all_plugins.size == 1) {
val wrapper = all_plugins.head
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 = 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
)
FEither(
wrapper.plugin
.beforeRequest(ctx)
.map { _ =>
markPluginItem(item, ctx, debug, Json.obj("kind" -> "successful"))
report.setContext(sequence.stopSequence().json)
Right(Done)
}
.recover { case exception: Throwable =>
markPluginItem(
item,
ctx,
debug,
Json.obj("kind" -> "failure", "error" -> JsonHelpers.errToJson(exception))
)
report.setContext(sequence.stopSequence().json)
Left(
NgResultProxyEngineError(
otoroshiJsonError(
Json
.obj(
"error" -> "internal_server_error",
"error_description" -> "an error happened during before-request plugins phase"
)
.applyOnIf(env.isDev) { obj => obj ++ Json.obj("jvm_error" -> JsonHelpers.errToJson(exception)) },
Results.InternalServerError,
attrs.get(otoroshi.next.plugins.Keys.RouteKey),
attrs,
request
)
)
)
}
)
} else {
val promise = Promise[Either[NgProxyEngineError, Done]]()
def next(plugins: Seq[NgPluginWrapper[NgRequestTransformer]]): Unit = {
plugins.headOption match {
case None => promise.trySuccess(Right(Done))
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 = wrapper.instance.instanceId)
val debug = 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
)
wrapper.plugin.beforeRequest(ctx).andThen {
case Failure(exception) =>
markPluginItem(
item,
ctx,
debug,
Json.obj("kind" -> "failure", "error" -> JsonHelpers.errToJson(exception))
)
report.setContext(sequence.stopSequence().json)
promise.trySuccess(
Left(
NgResultProxyEngineError(
otoroshiJsonError(
Json
.obj(
"error" -> "internal_server_error",
"error_description" -> "an error happened during before-request plugins phase"
)
.applyOnIf(env.isDev) { obj =>
obj ++ Json.obj("jvm_error" -> JsonHelpers.errToJson(exception))
},
Results.InternalServerError,
attrs.get(otoroshi.next.plugins.Keys.RouteKey),
attrs,
request
)
)
)
)
case Success(_) if plugins.size == 1 =>
markPluginItem(item, ctx, debug, Json.obj("kind" -> "successful"))
report.setContext(sequence.stopSequence().json)
promise.trySuccess(Right(Done))
case Success(_) =>
markPluginItem(item, ctx, debug, Json.obj("kind" -> "successful"))
next(plugins.tail)
}
}
}
}
next(all_plugins)
FEither.apply(promise.future)
}
} else {
FEither.right(Done)
}
}
def callPluginsAfterRequestCallback(
snowflake: String,
request: RequestHeader,
route: NgRoute,
plugins: NgContextualPlugins
)(implicit
ec: ExecutionContext,
env: Env,
report: NgExecutionReport,
globalConfig: GlobalConfig,
attrs: TypedMap,
mat: Materializer
): FEither[NgProxyEngineError, Done] = {
val all_plugins = plugins.transformerPluginsWithCallbacks
if (all_plugins.nonEmpty) {
var sequence = NgReportPluginSequence(
size = all_plugins.size,
kind = "after-request-plugins",
start = System.currentTimeMillis(),
stop = 0L,
start_ns = System.nanoTime(),
stop_ns = 0L,
plugins = Seq.empty
)
val _ctx = NgAfterRequestContext(
snowflake = snowflake,
request = request,
route = route,
config = Json.obj(),
globalConfig = globalConfig.plugins.config,
attrs = attrs
)
def markPluginItem(
item: NgReportPluginSequenceItem,
ctx: NgAfterRequestContext,
debug: Boolean,
result: JsValue
): Unit = {
sequence = sequence.copy(
plugins = sequence.plugins :+ item.copy(
stop = System.currentTimeMillis(),
stop_ns = System.nanoTime(),
out = Json
.obj(
"not_triggered" -> plugins.tpwoCallbacks.map(_.instance.plugin),
"result" -> result
)
.applyOnIf(debug)(_ ++ Json.obj("ctx" -> ctx.json))
)
)
}
if (all_plugins.size == 1) {
val wrapper = all_plugins.head
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 = 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
)
FEither(
wrapper.plugin
.afterRequest(ctx)
.map { _ =>
markPluginItem(item, ctx, debug, Json.obj("kind" -> "successful"))
report.setContext(sequence.stopSequence().json)
Right(Done)
}
.recover { case exception: Throwable =>
markPluginItem(
item,
ctx,
debug,
Json.obj("kind" -> "failure", "error" -> JsonHelpers.errToJson(exception))
)
report.setContext(sequence.stopSequence().json)
Left(
NgResultProxyEngineError(
otoroshiJsonError(
Json
.obj(
"error" -> "internal_server_error",
"error_description" -> "an error happened during after-request plugins phase"
)
.applyOnIf(env.isDev) { obj => obj ++ Json.obj("jvm_error" -> JsonHelpers.errToJson(exception)) },
Results.InternalServerError,
attrs.get(otoroshi.next.plugins.Keys.RouteKey),
attrs,
request
)
)
)
}
)
} else {
val promise = Promise[Either[NgProxyEngineError, Done]]()
def next(plugins: Seq[NgPluginWrapper[NgRequestTransformer]]): Unit = {
plugins.headOption match {
case None => promise.trySuccess(Right(Done))
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 = wrapper.instance.instanceId)
val debug = 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
)
wrapper.plugin.afterRequest(ctx).andThen {
case Failure(exception) =>
markPluginItem(
item,
ctx,
debug,
Json.obj("kind" -> "failure", "error" -> JsonHelpers.errToJson(exception))
)
report.setContext(sequence.stopSequence().json)
promise.trySuccess(
Left(
NgResultProxyEngineError(
otoroshiJsonError(
Json
.obj(
"error" -> "internal_server_error",
"error_description" -> "an error happened during after-request plugins phase"
)
.applyOnIf(env.isDev) { obj =>
obj ++ Json.obj("jvm_error" -> JsonHelpers.errToJson(exception))
},
Results.InternalServerError,
attrs.get(otoroshi.next.plugins.Keys.RouteKey),
attrs,
request
)
)
)
)
case Success(_) if plugins.size == 1 =>
markPluginItem(item, ctx, debug, Json.obj("kind" -> "successful"))
report.setContext(sequence.stopSequence().json)
promise.trySuccess(Right(Done))
case Success(_) =>
markPluginItem(item, ctx, debug, Json.obj("kind" -> "successful"))
next(plugins.tail)
}
}
}
}
next(all_plugins)
FEither.apply(promise.future)
}
} else {
FEither.right(Done)
}
}
def callPreRoutePlugins(snowflake: String, request: RequestHeader, route: NgRoute, plugins: NgContextualPlugins)(
implicit
ec: ExecutionContext,
env: Env,
report: NgExecutionReport,
globalConfig: GlobalConfig,
attrs: TypedMap,
mat: Materializer
): FEither[NgProxyEngineError, Done] = {
val all_plugins = plugins.preRoutePlugins
if (all_plugins.nonEmpty) {
var sequence = NgReportPluginSequence(
size = all_plugins.size,
kind = "pre-route-plugins",
start = System.currentTimeMillis(),
stop = 0L,
start_ns = System.nanoTime(),
stop_ns = 0L,
plugins = Seq.empty
)
def markPluginItem(
item: NgReportPluginSequenceItem,
ctx: NgPreRoutingContext,
debug: Boolean,
result: JsValue
): Unit = {
sequence = sequence.copy(
plugins = sequence.plugins :+ item.copy(
stop = System.currentTimeMillis(),
stop_ns = System.nanoTime(),
out = Json
.obj(
"result" -> result
)
.applyOnIf(debug)(_ ++ Json.obj("ctx" -> ctx.json))
)
)
}
val _ctx = NgPreRoutingContext(
snowflake = snowflake,
request = request,
route = route,
config = Json.obj(),
globalConfig = globalConfig.plugins.config,
attrs = attrs,
report = report,
sequence = sequence,
markPluginItem = markPluginItem
)
if (all_plugins.size == 1) {
val wrapper = all_plugins.head
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 = 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
)
FEither(
wrapper.plugin
.preRoute(ctx)
.transform {
case Failure(exception) =>
markPluginItem(
item,
ctx,
debug,
Json.obj("kind" -> "failure", "error" -> JsonHelpers.errToJson(exception))
)
report.setContext(sequence.stopSequence().json)
Success(
Left(
NgResultProxyEngineError(
otoroshiJsonError(
Json
.obj(
"error" -> "internal_server_error",
"error_description" -> "an error happened during pre-routing plugins phase"
)
.applyOnIf(env.isDev) { obj =>
obj ++ Json.obj("jvm_error" -> JsonHelpers.errToJson(exception))
},
Results.InternalServerError,
attrs.get(otoroshi.next.plugins.Keys.RouteKey),
attrs,
request
)
)
)
)
case Success(Left(err)) =>
val result = err.result
markPluginItem(
item,
ctx,
debug,
Json.obj(
"kind" -> "short-circuit",
"status" -> result.header.status,
"headers" -> result.header.headers
)
)
report.setContext(sequence.stopSequence().json)
Success(Left(NgResultProxyEngineError(result)))
case Success(Right(_)) =>
markPluginItem(item, ctx, debug, Json.obj("kind" -> "successful"))
report.setContext(sequence.stopSequence().json)
Success(Right(Done))
}
)
} else {
val promise = Promise[Either[NgProxyEngineError, Done]]()
def next(plugins: Seq[NgPluginWrapper[NgPreRouting]]): Unit = {
plugins.headOption match {
case None => promise.trySuccess(Right(Done))
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 = 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
)
wrapper.plugin.preRoute(ctx).andThen {
case Failure(exception) =>
markPluginItem(
item,
ctx,
debug,
Json.obj("kind" -> "failure", "error" -> JsonHelpers.errToJson(exception))
)
report.setContext(sequence.stopSequence().json)
promise.trySuccess(
Left(
NgResultProxyEngineError(
otoroshiJsonError(
Json
.obj(
"error" -> "internal_server_error",
"error_description" -> "an error happened during pre-routing plugins phase"
)
.applyOnIf(env.isDev) { obj =>
obj ++ Json.obj("jvm_error" -> JsonHelpers.errToJson(exception))
},
Results.InternalServerError,
attrs.get(otoroshi.next.plugins.Keys.RouteKey),
attrs,
request
)
)
)
)
case Success(Left(err)) =>
val result = err.result
markPluginItem(
item,
ctx,
debug,
Json.obj(
"kind" -> "short-circuit",
"status" -> result.header.status,
"headers" -> result.header.headers
)
)
report.setContext(sequence.stopSequence().json)
promise.trySuccess(Left(NgResultProxyEngineError(result)))
case Success(Right(_)) if plugins.size == 1 =>
markPluginItem(item, ctx, debug, Json.obj("kind" -> "successful"))
report.setContext(sequence.stopSequence().json)
promise.trySuccess(Right(Done))
case Success(Right(_)) =>
markPluginItem(item, ctx, debug, Json.obj("kind" -> "successful"))
next(plugins.tail)
}
}
}
}
next(all_plugins)
FEither.apply(promise.future)
}
} else {
FEither.right(Done)
}
}
def callAccessValidatorPlugins(
snowflake: String,
request: RequestHeader,
route: NgRoute,
plugins: NgContextualPlugins
)(implicit
ec: ExecutionContext,
env: Env,
report: NgExecutionReport,
globalConfig: GlobalConfig,
attrs: TypedMap,
mat: Materializer
): FEither[NgProxyEngineError, Done] = {
val all_plugins = plugins.accessValidatorPlugins
if (all_plugins.nonEmpty) {
var sequence = NgReportPluginSequence(
size = all_plugins.size,
kind = "access-validator-plugins",
start = System.currentTimeMillis(),
stop = 0L,
start_ns = System.nanoTime(),
stop_ns = 0L,
plugins = Seq.empty
)
def markPluginItem(
item: NgReportPluginSequenceItem,
ctx: NgAccessContext,
debug: Boolean,
result: JsValue
): Unit = {
sequence = sequence.copy(
plugins = sequence.plugins :+ item.copy(
stop = System.currentTimeMillis(),
stop_ns = System.nanoTime(),
out = Json
.obj(
"result" -> result
)
.applyOnIf(debug)(_ ++ Json.obj("ctx" -> ctx.json))
)
)
}
val _ctx = NgAccessContext(
snowflake = snowflake,
request = request,
route = route,
config = Json.obj(),
globalConfig = globalConfig.plugins.config,
attrs = attrs,
apikey = attrs.get(otoroshi.plugins.Keys.ApiKeyKey),
user = attrs.get(otoroshi.plugins.Keys.UserKey),
report = report,
sequence = sequence,
markPluginItem = markPluginItem
)
if (all_plugins.size == 1) {
val wrapper = all_plugins.head
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 = 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
)
FEither(wrapper.plugin.access(ctx).transform {
case Failure(exception) =>
markPluginItem(item, ctx, debug, Json.obj("kind" -> "failure", "error" -> JsonHelpers.errToJson(exception)))
report.setContext(sequence.stopSequence().json)
Success(
Left(
NgResultProxyEngineError(
otoroshiJsonError(
Json
.obj(
"error" -> "internal_server_error",
"error_description" -> "an error happened during access plugins phase"
)
.applyOnIf(env.isDev) { obj => obj ++ Json.obj("jvm_error" -> JsonHelpers.errToJson(exception)) },
Results.InternalServerError,
attrs.get(otoroshi.next.plugins.Keys.RouteKey),
attrs,
request
)
)
)
)
case Success(NgAccess.NgDenied(result)) =>
markPluginItem(item, ctx, debug, Json.obj("kind" -> "denied", "status" -> result.header.status))
report.setContext(sequence.stopSequence().json)
Success(Left(NgResultProxyEngineError(result)))
case Success(NgAccess.NgAllowed) =>
markPluginItem(item, ctx, debug, Json.obj("kind" -> "allowed"))
report.setContext(sequence.stopSequence().json)
Success(Right(Done))
})
} else {
val promise = Promise[Either[NgProxyEngineError, Done]]()
def next(plugins: Seq[NgPluginWrapper[NgAccessValidator]]): Unit = {
plugins.headOption match {
case None => promise.trySuccess(Right(Done))
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,
apikey = _ctx.apikey.orElse(attrs.get(otoroshi.plugins.Keys.ApiKeyKey)),
user = _ctx.user.orElse(attrs.get(otoroshi.plugins.Keys.UserKey)),
idx = wrapper.instance.instanceId
)
val debug = 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
)
wrapper.plugin.access(ctx).andThen {
case Failure(exception) =>
markPluginItem(
item,
ctx,
debug,
Json.obj("kind" -> "failure", "error" -> JsonHelpers.errToJson(exception))
)
report.setContext(sequence.stopSequence().json)
promise.trySuccess(
Left(
NgResultProxyEngineError(
otoroshiJsonError(
Json
.obj(
"error" -> "internal_server_error",
"error_description" -> "an error happened during access plugins phase"
)
.applyOnIf(env.isDev) { obj =>
obj ++ Json.obj("jvm_error" -> JsonHelpers.errToJson(exception))
},
Results.InternalServerError,
attrs.get(otoroshi.next.plugins.Keys.RouteKey),
attrs,
request
)
)
)
)
case Success(NgAccess.NgDenied(result)) =>
markPluginItem(item, ctx, debug, Json.obj("kind" -> "denied", "status" -> result.header.status))
report.setContext(sequence.stopSequence().json)
promise.trySuccess(Left(NgResultProxyEngineError(result)))
case Success(NgAccess.NgAllowed) if plugins.size == 1 =>
markPluginItem(item, ctx, debug, Json.obj("kind" -> "allowed"))
report.setContext(sequence.stopSequence().json)
promise.trySuccess(Right(Done))
case Success(NgAccess.NgAllowed) =>
markPluginItem(item, ctx, debug, Json.obj("kind" -> "allowed"))
next(plugins.tail)
}
}
}
}
next(all_plugins)
FEither.apply(promise.future)
}
} else {
FEither.right(Done)
}
}
// def updateApikeyQuotas(config: ProxyEngineConfig)(implicit
// ec: ExecutionContext,
// env: Env,
// report: NgExecutionReport,
// globalConfig: GlobalConfig,
// attrs: TypedMap,
// mat: Materializer
// ): FEither[NgProxyEngineError, RemainingQuotas] = {
// FEither.right(attrs.get(otoroshi.plugins.Keys.ApiKeyRemainingQuotasKey).getOrElse(RemainingQuotas()))
// // if (config.applyLegacyChecks) {
// // // increments calls for apikey
// // val quotas = attrs
// // .get(otoroshi.plugins.Keys.ApiKeyKey)
// // .map(_.updateQuotas())
// // .getOrElse(RemainingQuotas().vfuture)
// // .andThen { case Success(value) =>
// // attrs.put(otoroshi.plugins.Keys.ApiKeyRemainingQuotasKey -> value)
// // }
// // .map(rq => Right.apply[NgProxyEngineError, RemainingQuotas](rq))
// // FEither(quotas)
// // } else {
// // FEither.right(RemainingQuotas())
// // }
// }
def handleLegacyChecks(request: RequestHeader, route: NgRoute, config: ProxyEngineConfig)(implicit
ec: ExecutionContext,
env: Env,
report: NgExecutionReport,
globalConfig: GlobalConfig,
attrs: TypedMap,
mat: Materializer
): FEither[NgProxyEngineError, Done] = {
if (config.applyLegacyChecks) {
// generic.scala (1269)
val remoteAddress = request.theIpAddress
def errorResult(
status: Results.Status,
message: String,
code: String
): Future[Either[NgProxyEngineError, Done]] = {
Errors
.craftResponseResult(
message,
status,
request,
None,
Some(code),
duration = report.getDurationNow(),
overhead = report.getOverheadInNow(),
attrs = attrs,
maybeRoute = route.some
)
.map(e => Left(NgResultProxyEngineError(e)))
}
// quotasValidationFor increments calls for ip address
FEither(env.datastores.globalConfigDataStore.quotasValidationFor(remoteAddress).flatMap { r =>
val (within, secCalls, maybeQuota) = r
val quota = maybeQuota.getOrElse(globalConfig.perIpThrottlingQuota)
if (secCalls > (quota * 10L)) {
errorResult(Results.TooManyRequests, "[IP] You performed too much requests", "errors.too.much.requests")
} else {
if (!within) {
errorResult(Results.TooManyRequests, "[GLOBAL] You performed too much requests", "errors.too.much.requests")
} else if (globalConfig.ipFiltering.notMatchesWhitelist(remoteAddress)) {
errorResult(
Results.Forbidden,
"Your IP address is not allowed",
"errors.ip.address.not.allowed"
) // global whitelist
} else if (globalConfig.ipFiltering.matchesBlacklist(remoteAddress)) {
errorResult(
Results.Forbidden,
"Your IP address is not allowed",
"errors.ip.address.not.allowed"
) // global blacklist
} else if (globalConfig.matchesEndlessIpAddresses(remoteAddress)) {
val gigas: Long = 128L * 1024L * 1024L * 1024L
val middleFingers = ByteString.fromString(
"\uD83D\uDD95\uD83D\uDD95\uD83D\uDD95\uD83D\uDD95\uD83D\uDD95\uD83D\uDD95\uD83D\uDD95\uD83D\uDD95\uD83D\uDD95\uD83D\uDD95\uD83D\uDD95\uD83D\uDD95\uD83D\uDD95\uD83D\uDD95\uD83D\uDD95\uD83D\uDD95"
)
val zeros =
ByteString.fromInts(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
val characters: ByteString =
if (!globalConfig.middleFingers) middleFingers else zeros
val expected: Long = (gigas / characters.size) + 1L
Left(
NgResultProxyEngineError(
Results
.Status(200)
.sendEntity(
HttpEntity.Streamed(
Source
.repeat(characters)
.take(expected), // 128 Go of zeros or middle fingers
None,
Some("application/octet-stream")
)
)
)
).vfuture
} else {
Done.right.vfuture
}
}
})
} else {
FEither.right(Done)
}
}
def getBackend(target: Target, route: NgRoute, attrs: TypedMap)(implicit env: Env): NgTarget = {
attrs
.get(otoroshi.plugins.Keys.PreExtractedRequestTargetsKey)
.getOrElse(route.backend.allTargets)
.find(b => b.id == target.tags.head)
.get
}
def callTarget(snowflake: String, reqNumber: Long, request: Request[Source[ByteString, _]], _route: NgRoute)(
f: NgSelectedBackendTarget => FEither[NgProxyEngineError, Result]
)(implicit
ec: ExecutionContext,
env: Env,
report: NgExecutionReport,
globalConfig: GlobalConfig,
attrs: TypedMap,
mat: Materializer
): FEither[NgProxyEngineError, Result] = {
val cbStart = System.currentTimeMillis()
val route =
attrs.get(otoroshi.next.plugins.Keys.PossibleBackendsKey).map(b => _route.copy(backend = b)).getOrElse(_route)
val trackingId = attrs.get(otoroshi.plugins.Keys.RequestTrackingIdKey).getOrElse(IdGenerator.uuid)
val bodyAlreadyConsumed = new AtomicBoolean(false)
attrs.put(Keys.BodyAlreadyConsumedKey -> bodyAlreadyConsumed)
if (globalConfig.useCircuitBreakers) {
val counter = new AtomicInteger(0)
val relUri = request.relativeUri
val cachedPath: String =
route.backend.client.legacy
.timeouts(relUri)
.map(_ => relUri)
.getOrElse("")
def callF(target: Target, attempts: Int, alreadyFailed: AtomicBoolean): Future[Either[Result, Result]] = {
val backend = getBackend(target, route, attrs)
attrs.put(Keys.BackendKey -> backend)
f(NgSelectedBackendTarget(backend, attempts, alreadyFailed, cbStart)).value.flatMap {
case Left(err) => err.asResult().map(Left.apply)
case r @ Right(value) => Right(value).vfuture
}
}
def handleError(t: Throwable): Future[Either[Result, Result]] = {
t match {
case BodyAlreadyConsumedException =>
Errors
.craftResponseResult(
s"Something went wrong, the backend service does not respond quickly enough but consumed all the request body, you should try later. Thanks for your understanding",
Results.GatewayTimeout,
request,
None,
Some("errors.request.timeout"),
duration = report.getDurationNow(),
overhead = report.getOverheadInNow(),
cbDuration = System.currentTimeMillis - cbStart,
callAttempts = counter.get(),
attrs = attrs,
maybeRoute = route.some
)
.map(Left.apply)
case RequestTimeoutException =>
Errors
.craftResponseResult(
s"Something went wrong, the backend service does not respond quickly enough, you should try later. Thanks for your understanding",
Results.GatewayTimeout,
request,
None,
Some("errors.request.timeout"),
duration = report.getDurationNow(),
overhead = report.getOverheadInNow(),
cbDuration = System.currentTimeMillis - cbStart,
callAttempts = counter.get(),
attrs = attrs,
maybeRoute = route.some
)
.map(Left.apply)
case _: scala.concurrent.TimeoutException =>
Errors
.craftResponseResult(
s"Something went wrong, the backend service does not respond quickly enough, you should try later. Thanks for your understanding",
Results.GatewayTimeout,
request,
None,
Some("errors.request.timeout"),
duration = report.getDurationNow(),
overhead = report.getOverheadInNow(),
cbDuration = System.currentTimeMillis - cbStart,
callAttempts = counter.get(),
attrs = attrs,
maybeRoute = route.some
)
.map(Left.apply)
case AllCircuitBreakersOpenException =>
Errors
.craftResponseResult(
s"Something went wrong, the backend service seems a little bit overwhelmed, you should try later. Thanks for your understanding",
Results.ServiceUnavailable,
request,
None,
Some("errors.circuit.breaker.open"),
duration = report.getDurationNow(),
overhead = report.getOverheadInNow(),
cbDuration = System.currentTimeMillis - cbStart,
callAttempts = counter.get(),
attrs = attrs,
maybeRoute = route.some
)
.map(Left.apply)
case error
if error != null && error.getMessage != null && error.getMessage
.toLowerCase()
.contains("connection refused") =>
Errors
.craftResponseResult(
s"Something went wrong, the connection to backend service was refused, you should try later. Thanks for your understanding",
Results.BadGateway,
request,
None,
Some("errors.connection.refused"),
duration = report.getDurationNow(),
overhead = report.getOverheadInNow(),
cbDuration = System.currentTimeMillis - cbStart,
callAttempts = counter.get(),
attrs = attrs,
maybeRoute = route.some
)
.map(Left.apply)
case error if error != null && error.getMessage != null =>
logger.error(
s"Something went wrong, you should try later",
error
)
Errors
.craftResponseResult(
s"Something went wrong, you should try later. Thanks for your understanding.",
Results.BadGateway,
request,
None,
Some("errors.proxy.error"),
duration = report.getDurationNow(),
overhead = report.getOverheadInNow(),
cbDuration = System.currentTimeMillis - cbStart,
callAttempts = counter.get(),
attrs = attrs,
maybeRoute = route.some
)
.map(Left.apply)
case error =>
logger.error(
s"Something went wrong, you should try later",
error
)
Errors
.craftResponseResult(
s"Something went wrong, you should try later. Thanks for your understanding",
Results.BadGateway,
request,
None,
Some("errors.proxy.error"),
duration = report.getDurationNow(),
overhead = report.getOverheadInNow(),
cbDuration = System.currentTimeMillis - cbStart,
callAttempts = counter.get(),
attrs = attrs,
maybeRoute = route.some
)
.map(Left.apply)
}
}
implicit val scheduler = env.otoroshiScheduler
FEither(
env.circuitBeakersHolder
.get(
route.cacheableId + cachedPath,
() => new ServiceDescriptorCircuitBreaker()
)
.callGenNg[Result](
route.cacheableId,
route.name,
attrs
.get(otoroshi.plugins.Keys.PreExtractedRequestTargetsKey)
.getOrElse(route.backend.allTargets)
.map(_.toTarget),
route.backend.loadBalancing,
route.backend.client.legacy,
reqNumber.toString,
trackingId,
request.relativeUri,
request,
bodyAlreadyConsumed,
s"${request.method} ${request.relativeUri}",
counter,
attrs,
callF
) recoverWith { case t: Throwable =>
handleError(t)
} map {
case Left(res) => Left(NgResultProxyEngineError(res))
case Right(value) => Right(value)
}
)
} else {
val target = attrs
.get(otoroshi.plugins.Keys.PreExtractedRequestTargetKey)
.getOrElse {
val targets: Seq[Target] = attrs
.get(otoroshi.plugins.Keys.PreExtractedRequestTargetsKey)
.getOrElse(route.backend.allTargets)
.map(_.toTarget)
.filter(_.predicate.matches(reqNumber.toString, request, attrs))
.flatMap(t => Seq.fill(t.weight)(t))
route.backend.loadBalancing
.select(
reqNumber.toString,
trackingId,
request,
targets,
route.cacheableId
)
}
//val index = reqCounter.get() % (if (targets.nonEmpty) targets.size else 1)
// Round robin loadbalancing is happening here !!!!!
//val target = targets.apply(index.toInt)
val backend = getBackend(target, route, attrs)
attrs.put(Keys.BackendKey -> backend)
f(NgSelectedBackendTarget(backend, 1, new AtomicBoolean(false), cbStart))
}
}
def callWsTarget(snowflake: String, reqNumber: Long, request: RequestHeader, _route: NgRoute)(
f: NgSelectedBackendTarget => FEither[NgProxyEngineError, Flow[PlayWSMessage, PlayWSMessage, _]]
)(implicit
ec: ExecutionContext,
env: Env,
report: NgExecutionReport,
globalConfig: GlobalConfig,
attrs: TypedMap,
mat: Materializer
): FEither[NgProxyEngineError, Flow[PlayWSMessage, PlayWSMessage, _]] = {
val cbStart = System.currentTimeMillis()
val route =
attrs.get(otoroshi.next.plugins.Keys.PossibleBackendsKey).map(b => _route.copy(backend = b)).getOrElse(_route)
val trackingId = attrs.get(otoroshi.plugins.Keys.RequestTrackingIdKey).getOrElse(IdGenerator.uuid)
val bodyAlreadyConsumed = new AtomicBoolean(false)
attrs.put(Keys.BodyAlreadyConsumedKey -> bodyAlreadyConsumed)
if (globalConfig.useCircuitBreakers) {
val counter = new AtomicInteger(0)
val relUri = request.relativeUri
val cachedPath: String =
route.backend.client.legacy
.timeouts(relUri)
.map(_ => relUri)
.getOrElse("")
def callF(
target: Target,
attempts: Int,
alreadyFailed: AtomicBoolean
): Future[Either[Result, Flow[PlayWSMessage, PlayWSMessage, _]]] = {
val backend = getBackend(target, route, attrs)
attrs.put(Keys.BackendKey -> backend)
f(NgSelectedBackendTarget(backend, attempts, alreadyFailed, cbStart)).value.flatMap {
case Left(err) => err.asResult().map(Left.apply)
case r @ Right(value) => Right(value).vfuture
}
}
def handleError(t: Throwable): Future[Either[Result, Flow[PlayWSMessage, PlayWSMessage, _]]] = {
t match {
case BodyAlreadyConsumedException =>
Errors
.craftResponseResult(
s"Something went wrong, the backend service does not respond quickly enough but consumed all the request body, you should try later. Thanks for your understanding",
Results.GatewayTimeout,
request,
None,
Some("errors.request.timeout"),
duration = report.getDurationNow(),
overhead = report.getOverheadInNow(),
cbDuration = System.currentTimeMillis - cbStart,
callAttempts = counter.get(),
attrs = attrs,
maybeRoute = route.some
)
.map(Left.apply)
case RequestTimeoutException =>
Errors
.craftResponseResult(
s"Something went wrong, the backend service does not respond quickly enough, you should try later. Thanks for your understanding",
Results.GatewayTimeout,
request,
None,
Some("errors.request.timeout"),
duration = report.getDurationNow(),
overhead = report.getOverheadInNow(),
cbDuration = System.currentTimeMillis - cbStart,
callAttempts = counter.get(),
attrs = attrs,
maybeRoute = route.some
)
.map(Left.apply)
case _: scala.concurrent.TimeoutException =>
Errors
.craftResponseResult(
s"Something went wrong, the backend service does not respond quickly enough, you should try later. Thanks for your understanding",
Results.GatewayTimeout,
request,
None,
Some("errors.request.timeout"),
duration = report.getDurationNow(),
overhead = report.getOverheadInNow(),
cbDuration = System.currentTimeMillis - cbStart,
callAttempts = counter.get(),
attrs = attrs,
maybeRoute = route.some
)
.map(Left.apply)
case AllCircuitBreakersOpenException =>
Errors
.craftResponseResult(
s"Something went wrong, the backend service seems a little bit overwhelmed, you should try later. Thanks for your understanding",
Results.ServiceUnavailable,
request,
None,
Some("errors.circuit.breaker.open"),
duration = report.getDurationNow(),
overhead = report.getOverheadInNow(),
cbDuration = System.currentTimeMillis - cbStart,
callAttempts = counter.get(),
attrs = attrs,
maybeRoute = route.some
)
.map(Left.apply)
case error
if error != null && error.getMessage != null && error.getMessage
.toLowerCase()
.contains("connection refused") =>
Errors
.craftResponseResult(
s"Something went wrong, the connection to backend service was refused, you should try later. Thanks for your understanding",
Results.BadGateway,
request,
None,
Some("errors.connection.refused"),
duration = report.getDurationNow(),
overhead = report.getOverheadInNow(),
cbDuration = System.currentTimeMillis - cbStart,
callAttempts = counter.get(),
attrs = attrs,
maybeRoute = route.some
)
.map(Left.apply)
case error if error != null && error.getMessage != null =>
logger.error(
s"Something went wrong, you should try later",
error
)
Errors
.craftResponseResult(
s"Something went wrong, you should try later. Thanks for your understanding.",
Results.BadGateway,
request,
None,
Some("errors.proxy.error"),
duration = report.getDurationNow(),
overhead = report.getOverheadInNow(),
cbDuration = System.currentTimeMillis - cbStart,
callAttempts = counter.get(),
attrs = attrs,
maybeRoute = route.some
)
.map(Left.apply)
case error =>
logger.error(
s"Something went wrong, you should try later",
error
)
Errors
.craftResponseResult(
s"Something went wrong, you should try later. Thanks for your understanding",
Results.BadGateway,
request,
None,
Some("errors.proxy.error"),
duration = report.getDurationNow(),
overhead = report.getOverheadInNow(),
cbDuration = System.currentTimeMillis - cbStart,
callAttempts = counter.get(),
attrs = attrs,
maybeRoute = route.some
)
.map(Left.apply)
}
}
implicit val scheduler = env.otoroshiScheduler
FEither(
env.circuitBeakersHolder
.get(
route.cacheableId + cachedPath,
() => new ServiceDescriptorCircuitBreaker()
)
.callGenNg[Flow[PlayWSMessage, PlayWSMessage, _]](
route.cacheableId,
route.name,
route.backend.allTargets.map(_.toTarget),
route.backend.loadBalancing,
route.backend.client.legacy,
reqNumber.toString,
trackingId,
request.relativeUri,
request,
bodyAlreadyConsumed,
s"${request.method} ${request.relativeUri}",
counter,
attrs,
callF
) recoverWith { case t: Throwable =>
handleError(t)
} map {
case Left(res) => Left(NgResultProxyEngineError(res))
case Right(value) => Right(value)
}
)
} else {
val target: Target = attrs
.get(otoroshi.plugins.Keys.PreExtractedRequestTargetKey)
.getOrElse {
val targets: Seq[Target] = attrs
.get(otoroshi.plugins.Keys.PreExtractedRequestTargetsKey)
.getOrElse(route.backend.allTargets)
.map(_.toTarget)
.filter(_.predicate.matches(reqNumber.toString, request, attrs))
.flatMap(t => Seq.fill(t.weight)(t))
route.backend.loadBalancing
.select(
reqNumber.toString,
trackingId,
request,
targets,
route.cacheableId
)
}
//val index = reqCounter.get() % (if (targets.nonEmpty) targets.size else 1)
// Round robin loadbalancing is happening here !!!!!
//val target = targets.apply(index.toInt)
val backend = getBackend(target, route, attrs)
attrs.put(Keys.BackendKey -> backend)
f(NgSelectedBackendTarget(backend, 1, new AtomicBoolean(false), cbStart))
}
}
def maybeStrippedUri(req: RequestHeader, rawUri: String, route: NgRoute, attrs: TypedMap): String = {
if (route.frontend.stripPath) {
attrs.get(Keys.MatchedRouteKey) match {
case Some(mroute) =>
if (mroute.path.nonEmpty) {
val allPaths = route.frontend.domains.map(_.path)
val containsWildcard = allPaths.exists(_.contains("*"))
val containsNamedParams = allPaths.exists(_.contains("/:"))
val containsRegexNamedParams = allPaths.exists(_.contains("/$"))
if (
!containsWildcard && !containsNamedParams && !containsRegexNamedParams && allPaths.size == 1 && allPaths
.contains("/")
) {
if (logger.isDebugEnabled) logger.debug("cleanup uri stripping")
rawUri
} else {
// WARNING: this one can cause issue as here path segments can be stripped for the bad reasons
val mpath = mroute.path.substring(1)
attrs.put(otoroshi.plugins.Keys.StrippedPathKey -> mroute.path)
rawUri.replaceFirst(mpath, "") // handles wildcard
}
} else {
rawUri
}
case None => {
val allPaths = route.frontend.domains.map(_.path)
val root = req.relativeUri
val rootMatched = allPaths match { //rootMatched was this.matchingRoot
case ps if ps.isEmpty => None
case ps => ps.find(p => root.startsWith(p))
}
rootMatched
.filter(m => route.frontend.stripPath && root.startsWith(m))
.map { m =>
val replaced = m.replace(".", "\\.")
attrs.put(otoroshi.plugins.Keys.StrippedPathKey -> replaced)
root.replaceFirst(replaced, "")
}
.getOrElse(rawUri)
}
}
} else {
rawUri
}
}
def callRequestTransformer(
snowflake: String,
request: RequestHeader,
body: Source[ByteString, _],
route: NgRoute,
backend: NgTarget,
plugins: NgContextualPlugins
)(implicit
ec: ExecutionContext,
env: Env,
report: NgExecutionReport,
globalConfig: GlobalConfig,
attrs: TypedMap,
mat: Materializer
): FEither[NgProxyEngineError, NgPluginHttpRequest] = {
val wsCookiesIn = request.cookies.toSeq.map(c =>
WSCookieWithSameSite(
name = c.name,
value = c.value,
domain = c.domain,
path = Option(c.path),
maxAge = c.maxAge.map(_.toLong),
secure = c.secure,
httpOnly = c.httpOnly,
sameSite = c.sameSite
)
)
val target = backend.toTarget
val root = route.backend.root
val rawUri = request.relativeUri.substring(1)
val uri = maybeStrippedUri(request, rawUri, route, attrs)
// TODO: Not sure it's the right place for that here
val lazySource = Source.single(ByteString.empty).flatMapConcat { _ =>
attrs.get(Keys.BodyAlreadyConsumedKey).foreach(_.compareAndSet(false, true))
body
}
val headersInFiltered = Seq(
env.Headers.OtoroshiState,
env.Headers.OtoroshiClaim,
env.Headers.OtoroshiRequestId,
env.Headers.OtoroshiClientId,
env.Headers.OtoroshiClientSecret,
env.Headers.OtoroshiAuthorization,
"Otoroshi-Try-It-Request-Id"
).++(headersInStatic).map(_.toLowerCase)
val headers = request.headers.toSimpleMap
.filterNot { case (key, _) =>
headersInFiltered.contains(key.toLowerCase())
}
val rawRequest = NgPluginHttpRequest(
url = s"${request.theProtocol}://${request.theHost}${request.relativeUri}",
method = request.method,
headers = request.headers.toSimpleMap,
cookies = wsCookiesIn,
version = request.version,
clientCertificateChain = () => request.clientCertificateChain,
body = lazySource,
backend = None
)
val targetUrlRaw =
if (route.backend.rewrite) s"${target.scheme}://${target.host}$root"
else s"${target.scheme}://${target.host}$root$uri"
val targetUrl = TargetExpressionLanguage(
targetUrlRaw,
request.some,
route.serviceDescriptor.some,
route.some,
attrs.get(otoroshi.plugins.Keys.ApiKeyKey),
attrs.get(otoroshi.plugins.Keys.UserKey),
attrs.get(otoroshi.plugins.Keys.ElCtxKey).get,
attrs,
env
)
val otoroshiRequest = NgPluginHttpRequest(
url = targetUrl,
method = request.method,
headers = headers,
cookies = wsCookiesIn,
version = request.version,
clientCertificateChain = () => request.clientCertificateChain,
body = lazySource,
backend = backend.some
)
val all_plugins = plugins.transformerPluginsThatTransformsRequest
if (all_plugins.nonEmpty) {
var sequence = NgReportPluginSequence(
size = all_plugins.size,
kind = "request-transformer-plugins",
start = System.currentTimeMillis(),
stop = 0L,
start_ns = System.nanoTime(),
stop_ns = 0L,
plugins = Seq.empty
)
def markPluginItem(
item: NgReportPluginSequenceItem,
ctx: NgTransformerRequestContext,
debug: Boolean,
result: JsValue
): Unit = {
sequence = sequence.copy(
plugins = sequence.plugins :+ item.copy(
stop = System.currentTimeMillis(),
stop_ns = System.nanoTime(),
out = Json
.obj(
"not_triggered" -> plugins.tpwoRequest.map(_.instance.plugin),
"result" -> result
)
.applyOnIf(debug)(_ ++ Json.obj("ctx" -> ctx.json))
)
)
}
val __ctx = NgTransformerRequestContext(
snowflake = snowflake,
request = request,
rawRequest = rawRequest,
otoroshiRequest = otoroshiRequest,
apikey = attrs.get(otoroshi.plugins.Keys.ApiKeyKey),
user = attrs.get(otoroshi.plugins.Keys.UserKey),
route = route,
config = Json.obj(),
globalConfig = globalConfig.plugins.config,
attrs = attrs,
report = report,
sequence = sequence,
markPluginItem = markPluginItem
)
if (all_plugins.size == 1) {
val wrapper = all_plugins.head
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 = 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
)
FEither(wrapper.plugin.transformRequest(ctx).transform {
case Failure(exception) =>
markPluginItem(item, ctx, debug, Json.obj("kind" -> "failure", "error" -> JsonHelpers.errToJson(exception)))
report.setContext(sequence.stopSequence().json)
Success(
Left(
NgResultProxyEngineError(
otoroshiJsonError(
Json
.obj(
"error" -> "internal_server_error",
"error_description" -> "an error happened during request-transformation plugins phase"
)
.applyOnIf(env.isDev) { obj => obj ++ Json.obj("jvm_error" -> JsonHelpers.errToJson(exception)) },
Results.InternalServerError,
attrs.get(otoroshi.next.plugins.Keys.RouteKey),
attrs,
request
)
)
)
)
case Success(Left(result)) =>
markPluginItem(
item,
ctx,
debug,
Json.obj("kind" -> "short-circuit", "status" -> result.header.status, "headers" -> result.header.headers)
)
report.setContext(sequence.stopSequence().json)
Success(Left(NgResultProxyEngineError(result)))
case Success(Right(req_next)) =>
markPluginItem(item, ctx.copy(otoroshiRequest = req_next), debug, Json.obj("kind" -> "successful"))
report.setContext(sequence.stopSequence().json)
Success(Right(req_next))
})
} else {
val promise = Promise[Either[NgProxyEngineError, NgPluginHttpRequest]]()
def next(_ctx: NgTransformerRequestContext, plugins: Seq[NgPluginWrapper[NgRequestTransformer]]): Unit = {
plugins.headOption match {
case None => promise.trySuccess(Right(_ctx.otoroshiRequest))
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,
apikey = _ctx.apikey.orElse(attrs.get(otoroshi.plugins.Keys.ApiKeyKey)),
user = _ctx.user.orElse(attrs.get(otoroshi.plugins.Keys.UserKey)),
idx = wrapper.instance.instanceId
)
val debug = 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
)
wrapper.plugin.transformRequest(ctx).andThen {
case Failure(exception) =>
markPluginItem(
item,
ctx,
debug,
Json.obj("kind" -> "failure", "error" -> JsonHelpers.errToJson(exception))
)
report.setContext(sequence.stopSequence().json)
promise.trySuccess(
Left(
NgResultProxyEngineError(
otoroshiJsonError(
Json
.obj(
"error" -> "internal_server_error",
"error_description" -> "an error happened during request-transformation plugins phase"
)
.applyOnIf(env.isDev) { obj =>
obj ++ Json.obj("jvm_error" -> JsonHelpers.errToJson(exception))
},
Results.InternalServerError,
attrs.get(otoroshi.next.plugins.Keys.RouteKey),
attrs,
request
)
)
)
)
case Success(Left(result)) =>
markPluginItem(
item,
ctx,
debug,
Json.obj(
"kind" -> "short-circuit",
"status" -> result.header.status,
"headers" -> result.header.headers
)
)
report.setContext(sequence.stopSequence().json)
promise.trySuccess(Left(NgResultProxyEngineError(result)))
case Success(Right(req_next)) if plugins.size == 1 =>
markPluginItem(item, ctx.copy(otoroshiRequest = req_next), debug, Json.obj("kind" -> "successful"))
report.setContext(sequence.stopSequence().json)
promise.trySuccess(Right(req_next))
case Success(Right(req_next)) =>
markPluginItem(item, ctx.copy(otoroshiRequest = req_next), debug, Json.obj("kind" -> "successful"))
next(_ctx.copy(otoroshiRequest = req_next), plugins.tail)
}
}
}
}
next(__ctx, all_plugins)
FEither.apply(promise.future)
}
} else {
FEither.right(otoroshiRequest)
}
}
def callWsBackend(
snowflake: String,
rawRequest: RequestHeader,
request: NgPluginHttpRequest,
route: NgRoute,
backend: NgTarget,
ctxPlugins: NgContextualPlugins
)(implicit
ec: ExecutionContext,
env: Env,
report: NgExecutionReport,
globalConfig: GlobalConfig,
attrs: TypedMap,
mat: Materializer
): FEither[NgProxyEngineError, Flow[PlayWSMessage, PlayWSMessage, _]] = {
val finalTarget: Target = request.backend.getOrElse(backend).toTarget
attrs.put(otoroshi.plugins.Keys.RequestTargetKey -> finalTarget)
val all_tunnel_handlers = ctxPlugins.tunnelHandlerPlugins
if (all_tunnel_handlers.nonEmpty) {
val handler = all_tunnel_handlers.head
if (request.relativeUri.startsWith("/.well-known/otoroshi/tunnel")) {
val ctx = NgTunnelHandlerContext(
snowflake = snowflake,
request = rawRequest,
route = route,
config = handler.instance.config.raw,
attrs = attrs
)
FEither(handler.plugin.handle(ctx).right.vfuture)
} else {
FEither(
Errors
.craftResponseResult(
s"Resource not found",
Results.NotFound,
rawRequest,
None,
Some("errors.resource.not.found"),
attrs = attrs,
maybeRoute = route.some
)
.map(r => Left(NgResultProxyEngineError(r)))
)
}
} else if (ctxPlugins.hasWebsocketBackendPlugins) {
val handler = ctxPlugins.websocketBackendPlugins.head
val wsEngine = if (ctxPlugins.hasWebsocketPlugins) {
new WebsocketEngine(route, ctxPlugins, rawRequest, finalTarget, attrs)
} else {
new WebsocketEngine(NgRoute.empty, NgContextualPlugins.empty(rawRequest), rawRequest, finalTarget, attrs)
}
val ctx = NgWebsocketPluginContext(
idx = 0,
snowflake = snowflake,
request = rawRequest,
route = route,
config = handler.instance.config.raw,
attrs = attrs,
target = finalTarget
)
FEither(handler.plugin.callBackendOrError(ctx).flatMap {
case Left(proxyError) => proxyError.leftf
case Right(flow) => {
val outFlow: Flow[PlayWSMessage, PlayWSMessage, _] = flow
.mapAsync(1) { mess =>
WebsocketMessage.PlayMessage(mess).asAkka.flatMap { m =>
wsEngine.handleResponse(m)(_ => ())
}
}
.takeWhile(_.isRight)
.collect { case Right(message) =>
message
}
.mapAsync(1) { message =>
message.asPlay
}
val inFlow = Flow
.fromFunction[PlayWSMessage, PlayWSMessage](identity)
.mapAsync(1) { mess =>
wsEngine.handleRequest(mess)(_ => ())
}
.takeWhile(_.isRight)
.collect { case Right(message) =>
message
}
.mapAsync(1) { message =>
message.asPlay
}
val finalFlow = inFlow.via(outFlow)
finalFlow.right.vfuture
}
})
} else {
if (route.useAkkaHttpWsClient && ctxPlugins.hasNoWebsocketPlugins) {
FEither(
WebSocketProxyActor
.wsCall(
UrlSanitizer.sanitize(request.url),
request.headers.toSeq,
route.serviceDescriptor,
target = finalTarget,
rawRequest = rawRequest, // TODO: custom header size
route = route.some
)
.right
.vfuture
)
} else {
FEither(
ActorFlow
.actorRef(out =>
WebSocketProxyActor.props(
UrlSanitizer.sanitize(request.url),
out,
request.headers.toSeq,
rawRequest, // TODO: custom header size
route.serviceDescriptor,
route.some,
ctxPlugins.some,
finalTarget,
attrs,
env
)
)(env.otoroshiActorSystem, env.otoroshiMaterializer)
.right
.vfuture
)
}
}
}
def callBackend(
snowflake: String,
noBackendCallerPlugin: Boolean,
rawRequest: Request[Source[ByteString, _]],
request: NgPluginHttpRequest,
route: NgRoute,
backend: NgTarget,
plugins: NgContextualPlugins,
engineConfig: ProxyEngineConfig
)(implicit
ec: ExecutionContext,
env: Env,
report: NgExecutionReport,
globalConfig: GlobalConfig,
attrs: TypedMap,
mat: Materializer
): FEither[NgProxyEngineError, BackendCallResponse] = {
if (!noBackendCallerPlugin && plugins.hasBackendCallPlugin) {
val pluginInstance = plugins.backendCallPlugin
val ctx = NgbBackendCallContext(
snowflake = snowflake,
rawRequest = rawRequest,
request = request,
route = route,
backend = backend,
apikey = attrs.get(otoroshi.plugins.Keys.ApiKeyKey),
user = attrs.get(otoroshi.plugins.Keys.UserKey),
config = pluginInstance.instance.config.json,
globalConfig = globalConfig.plugins.config,
attrs = attrs
)
FEither.fromEitherT(
pluginInstance.plugin.callBackend(
ctx,
() =>
callBackend(
snowflake,
noBackendCallerPlugin = true,
rawRequest,
request,
route,
backend,
plugins,
engineConfig
).value
)
)
} else {
val finalTarget: Target = request.backend.getOrElse(backend).toTarget
attrs.put(otoroshi.plugins.Keys.RequestTargetKey -> finalTarget)
val contentLengthIn: Option[Long] = request.contentLengthStr
.orElse(rawRequest.contentLengthStr)
.map(_.toLong)
val counterIn = attrs.get(otoroshi.plugins.Keys.RequestCounterInKey).get
counterIn.addAndGet(contentLengthIn.getOrElse(0L))
val (currentReqHasBody, shouldInjectContentLength) = request.hasBodyWithoutLength
val wsCookiesIn = request.cookies
val clientConfig = route.backend.client
val clientReq: WSRequest = if (route.useNettyClient || finalTarget.protocol.isHttp2OrHttp3) {
env.reactorClientGateway
.url(UrlSanitizer.sanitize(request.url))
.withTarget(finalTarget)
.withClientConfig(clientConfig.legacy)
} else {
route.useAkkaHttpClient match {
case _ if finalTarget.mtlsConfig.mtls =>
env.gatewayClient.akkaUrlWithTarget(
UrlSanitizer.sanitize(request.url),
finalTarget,
clientConfig.legacy
)
case true =>
env.gatewayClient.akkaUrlWithTarget(
UrlSanitizer.sanitize(request.url),
finalTarget,
clientConfig.legacy
)
case false =>
env.gatewayClient.urlWithTarget(
UrlSanitizer.sanitize(request.url),
finalTarget,
clientConfig.legacy
)
}
}
val host = request.headers.get("Host").orElse(request.headers.get("host")).getOrElse(rawRequest.theHost)
val extractedTimeout =
route.backend.client.legacy
.extractTimeout(rawRequest.relativeUri, _.callAndStreamTimeout, _.callAndStreamTimeout)
val isTargetHttp1 =
finalTarget.protocol == HttpProtocols.HTTP_1_0 || finalTarget.protocol == HttpProtocols.HTTP_1_1
val isTargetHttp2 = finalTarget.protocol == HttpProtocols.HTTP_2_0
val version = rawRequest.version.toLowerCase
val isRequestAboveHttp1 =
(!version.startsWith("http/1")) && (version.startsWith("http/2") || version.startsWith("http3"))
val isRequestAboveHttp2 =
(!version.startsWith("http/1") && !version.startsWith("http/2")) && version.startsWith("http3")
val requestHeaders = request.headers
.filterNot(_._1.toLowerCase == "cookie")
.+("Host" -> host)
.toSeq
.applyOnIf(isTargetHttp1 && isRequestAboveHttp1) { s =>
s
.filterNot(_._1.toLowerCase().startsWith("x-http2"))
.filterNot(_._1.toLowerCase().startsWith("x-http3"))
}
.applyOnIf(isTargetHttp2 && isRequestAboveHttp2) { s =>
s.filterNot(_._1.toLowerCase().startsWith("x-http3"))
}
.applyOnWithOpt(env.maxHeaderSizeToBackend) {
case (hdrs, max) => {
hdrs.filter {
case (key, value) if key.length > max => {
HeaderTooLongAlert(key, value, "", "remove", "backend", "engine", request, route, env).toAnalytics()
logger.error(
s"removing header '${key}' from request to backend because it's too long. route is ${route.name} / ${route.id}. header value length is '${value.length}' and value is '${value}'"
)
false
}
case _ => true
}
}
}
.applyOnWithOpt(env.limitHeaderSizeToBackend) {
case (hdrs, max) => {
hdrs.map {
case (key, value) if key.length > max => {
val newValue = value.substring(0, max.toInt - 1)
HeaderTooLongAlert(key, value, newValue, "limit", "backend", "plugin", request, route, env)
.toAnalytics()
logger.error(
s"limiting header '${key}' from request to backend because it's too long. route is ${route.name} / ${route.id}. header value length is '${value.length}' and value is '${value}', new value is '${newValue}'"
)
(key, newValue)
}
case (key, value) => (key, value)
}
}
}
val builder = clientReq
.withRequestTimeout(extractedTimeout)
.withFailureIndicator(fakeFailureIndicator)
.withMethod(request.method)
.withHttpHeaders(requestHeaders: _*)
.withCookies(wsCookiesIn: _*)
.withFollowRedirects(false)
.withMaybeProxyServer(
route.backend.client.proxy.orElse(globalConfig.proxies.services)
)
val theBody = request.body
.applyOnIf(env.dynamicBodySizeCompute && contentLengthIn.isEmpty) { body =>
body.map { chunk =>
counterIn.addAndGet(chunk.size)
chunk
}
}
.applyOnIf(request.hasBody && engineConfig.capture) { source =>
var requestChunks = ByteString("")
source
.map { chunk =>
if ((requestChunks.size + chunk.size) <= engineConfig.captureMaxEntitySize) {
requestChunks = requestChunks ++ chunk
}
chunk
}
.alsoTo(Sink.onComplete { case _ =>
attrs.put(otoroshi.plugins.Keys.CaptureRequestBodyKey -> requestChunks)
})
}
// because writeableOf_WsBody always add a 'Content-Type: application/octet-stream' header
val builderWithBody = if (currentReqHasBody) {
if (shouldInjectContentLength) {
builder.addHttpHeaders("Content-Length" -> "0").withBody(theBody)
} else {
builder.withBody(theBody)
}
} else {
builder
}
report.markOverheadIn()
val start = System.currentTimeMillis()
val fu: Future[BackendCallResponse] = builderWithBody
.stream()
.map { response =>
attrs.put(otoroshi.plugins.Keys.BackendDurationKey -> (System.currentTimeMillis() - start))
val idOpt = rawRequest.attrs.get(otoroshi.netty.NettyRequestKeys.TrailerHeadersIdKey)
val hasTrailerHeaders =
rawRequest.headers.get("te").contains("trailers") || response.headers.containsIgnoreCase("trailer")
val shouldHaveTrailers =
(route.useNettyClient || finalTarget.protocol.isHttp2OrHttp3) && rawRequest.attrs // trailers works for http/1.1, h2 and h3
.get(RequestAttrKey.Server)
.contains("netty-experimental") && hasTrailerHeaders
if (shouldHaveTrailers) {
val id = idOpt.get
response match {
case r: otoroshi.netty.TrailerSupport =>
val future = r.trailingHeaders()
otoroshi.netty.NettyRequestAwaitingTrailers.add(id, Left(future))
future.map(trls => otoroshi.netty.NettyRequestAwaitingTrailers.add(id, Right(trls)))
case _ =>
}
}
BackendCallResponse(
NgPluginHttpResponse(
status = response.status,
headers = response.headers.mapValues(_.last).applyOnIf(shouldHaveTrailers) { hds =>
idOpt match {
case Some(id) if shouldHaveTrailers => hds ++ Map("otoroshi-netty-trailers" -> id)
case _ => hds
}
},
cookies = response.cookies,
body = response.bodyAsSource
),
response.some
)
}
.andThen { case _ =>
report.startOverheadOut()
}
FEither.fright(fu)
}
}
def callResponseTransformer(
snowflake: String,
rawRequest: Request[Source[ByteString, _]],
response: BackendCallResponse,
route: NgRoute,
backend: NgTarget,
plugins: NgContextualPlugins
)(implicit
ec: ExecutionContext,
env: Env,
report: NgExecutionReport,
globalConfig: GlobalConfig,
attrs: TypedMap,
mat: Materializer
): FEither[NgProxyEngineError, NgPluginHttpResponse] = {
val rawResponse = response.response.copy() /*NgPluginHttpResponse(
status = response.status,
headers = response.headers.mapValues(_.last),
cookies = response.cookies,
body = response.bodyAsSource
)*/
val otoroshiResponse = response.response.copy() /*NgPluginHttpResponse(
status = response.status,
headers = response.headers.mapValues(_.last),
cookies = response.cookies,
body = response.bodyAsSource
)*/
val all_plugins = plugins.transformerPluginsThatTransformsResponse
if (all_plugins.nonEmpty) {
var sequence = NgReportPluginSequence(
size = all_plugins.size,
kind = "response-transformer-plugins",
start = System.currentTimeMillis(),
stop = 0L,
start_ns = System.nanoTime(),
stop_ns = 0L,
plugins = Seq.empty
)
def markPluginItem(
item: NgReportPluginSequenceItem,
ctx: NgTransformerResponseContext,
debug: Boolean,
result: JsValue
): Unit = {
sequence = sequence.copy(
plugins = sequence.plugins :+ item.copy(
stop = System.currentTimeMillis(),
stop_ns = System.nanoTime(),
out = Json
.obj(
"not_triggered" -> plugins.tpwoResponse.map(_.instance.plugin),
"result" -> result
)
.applyOnIf(debug)(_ ++ Json.obj("ctx" -> ctx.json))
)
)
}
val __ctx = NgTransformerResponseContext(
snowflake = snowflake,
request = rawRequest,
response = response.rawResponse,
rawResponse = rawResponse,
otoroshiResponse = otoroshiResponse,
apikey = attrs.get(otoroshi.plugins.Keys.ApiKeyKey),
user = attrs.get(otoroshi.plugins.Keys.UserKey),
route = route,
config = Json.obj(),
globalConfig = globalConfig.plugins.config,
attrs = attrs,
report = report,
sequence = sequence,
markPluginItem = markPluginItem
)
if (all_plugins.size == 1) {
val wrapper = all_plugins.head
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 = 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
)
FEither(wrapper.plugin.transformResponse(ctx).transform {
case Failure(exception) =>
markPluginItem(item, ctx, debug, Json.obj("kind" -> "failure", "error" -> JsonHelpers.errToJson(exception)))
report.setContext(sequence.stopSequence().json)
Success(
Left(
NgResultProxyEngineError(
otoroshiJsonError(
Json
.obj(
"error" -> "internal_server_error",
"error_description" -> "an error happened during response-transformation plugins phase"
)
.applyOnIf(env.isDev) { obj => obj ++ Json.obj("jvm_error" -> JsonHelpers.errToJson(exception)) },
Results.InternalServerError,
attrs.get(otoroshi.next.plugins.Keys.RouteKey),
attrs,
rawRequest
)
)
)
)
case Success(Left(result)) =>
markPluginItem(
item,
ctx,
debug,
Json.obj("kind" -> "short-circuit", "status" -> result.header.status, "headers" -> result.header.headers)
)
report.setContext(sequence.stopSequence().json)
Success(Left(NgResultProxyEngineError(result)))
case Success(Right(resp_next)) =>
markPluginItem(item, ctx.copy(otoroshiResponse = resp_next), debug, Json.obj("kind" -> "successful"))
report.setContext(sequence.stopSequence().json)
Success(Right(resp_next))
})
} else {
val promise = Promise[Either[NgProxyEngineError, NgPluginHttpResponse]]()
def next(_ctx: NgTransformerResponseContext, plugins: Seq[NgPluginWrapper[NgRequestTransformer]]): Unit = {
plugins.headOption match {
case None => promise.trySuccess(Right(_ctx.otoroshiResponse))
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,
apikey = _ctx.apikey.orElse(attrs.get(otoroshi.plugins.Keys.ApiKeyKey)),
user = _ctx.user.orElse(attrs.get(otoroshi.plugins.Keys.UserKey)),
idx = wrapper.instance.instanceId
)
val debug = 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
)
wrapper.plugin.transformResponse(ctx).andThen {
case Failure(exception) =>
markPluginItem(
item,
ctx,
debug,
Json.obj("kind" -> "failure", "error" -> JsonHelpers.errToJson(exception))
)
report.setContext(sequence.stopSequence().json)
promise.trySuccess(
Left(
NgResultProxyEngineError(
otoroshiJsonError(
Json
.obj(
"error" -> "internal_server_error",
"error_description" -> "an error happened during response-transformation plugins phase"
)
.applyOnIf(env.isDev) { obj =>
obj ++ Json.obj("jvm_error" -> JsonHelpers.errToJson(exception))
},
Results.InternalServerError,
attrs.get(otoroshi.next.plugins.Keys.RouteKey),
attrs,
rawRequest
)
)
)
)
case Success(Left(result)) =>
markPluginItem(
item,
ctx,
debug,
Json.obj(
"kind" -> "short-circuit",
"status" -> result.header.status,
"headers" -> result.header.headers
)
)
report.setContext(sequence.stopSequence().json)
promise.trySuccess(Left(NgResultProxyEngineError(result)))
case Success(Right(resp_next)) if plugins.size == 1 =>
markPluginItem(item, ctx.copy(otoroshiResponse = resp_next), debug, Json.obj("kind" -> "successful"))
report.setContext(sequence.stopSequence().json)
promise.trySuccess(Right(resp_next))
case Success(Right(resp_next)) =>
markPluginItem(item, ctx.copy(otoroshiResponse = resp_next), debug, Json.obj("kind" -> "successful"))
next(_ctx.copy(otoroshiResponse = resp_next), plugins.tail)
}
}
}
}
next(__ctx, all_plugins)
FEither.apply(promise.future)
}
} else {
FEither.right(otoroshiResponse)
}
}
def streamResponse(
snowflake: String,
rawRequest: Request[Source[ByteString, _]],
request: NgPluginHttpRequest,
rawResponse: BackendCallResponse,
response: NgPluginHttpResponse,
route: NgRoute,
backend: NgTarget,
engineConfig: ProxyEngineConfig
)(implicit
ec: ExecutionContext,
env: Env,
report: NgExecutionReport,
globalConfig: GlobalConfig,
attrs: TypedMap,
mat: Materializer
): FEither[NgProxyEngineError, Result] = {
val counterOut = attrs.get(otoroshi.plugins.Keys.RequestCounterOutKey).get
val contentType: Option[String] = response.header("Content-Type")
val contentLength: Option[Long] = response
.header("Content-Length")
.orElse(rawResponse.contentLengthStr) // legit
.map(_.toLong)
val contentLengthOut: Option[Long] = response
.header("Content-Length")
.map(_.toLong)
counterOut.addAndGet(contentLength.getOrElse(0L))
val _cookies = response.cookies.map {
case c: WSCookieWithSameSite =>
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.sameSite
)
case c => {
val sameSite: Option[Cookie.SameSite] =
rawResponse.headers.get("Set-Cookie").orElse(rawResponse.headers.get("set-cookie")).flatMap {
values => // legit
values
.find { sc =>
sc.startsWith(s"${c.name}=${c.value}")
}
.flatMap { sc =>
sc.split(";")
.map(_.trim)
.find(p => p.toLowerCase.startsWith("samesite="))
.map(_.replace("samesite=", "").replace("SameSite=", ""))
.flatMap(Cookie.SameSite.parse)
}
}
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 = sameSite
)
}
}
val cookies = attrs.get(otoroshi.plugins.Keys.RequestTrackingIdKey) match {
case None => _cookies
case Some(trackingId) => {
_cookies :+ play.api.mvc.Cookie(
name = "otoroshi-tracking",
value = trackingId,
maxAge = Some(2592000),
path = "/",
domain = Some(rawRequest.theDomain),
httpOnly = false
)
}
}
val isContentLengthZero: Boolean = response.headers.getIgnoreCase("Content-Length").contains("0")
val noContentLengthHeader: Boolean = !response.hasLength // rawResponse.contentLength.isEmpty
val hasChunkedHeader: Boolean = response.isChunked /*rawResponse
.header("Transfer-Encoding")
.orElse(response.headers.get("Transfer-Encoding"))
.exists(h => h.toLowerCase().contains("chunked"))*/
val isChunked: Boolean = rawResponse.isChunked() match { // don't know if actualy legit ...
case _ if isContentLengthZero => false
// case Some(true) => true
// case Some(false) if !env.emptyContentLengthIsChunked => hasChunkedHeader
// case Some(false) if env.emptyContentLengthIsChunked && noContentLengthHeader => true
// case Some(false) if env.emptyContentLengthIsChunked && !hasChunkedHeader && noContentLengthHeader => true
// case Some(false) => false
case Some(chunked) => chunked
case None if !env.emptyContentLengthIsChunked =>
hasChunkedHeader // false
case None if env.emptyContentLengthIsChunked && hasChunkedHeader =>
true
case None if env.emptyContentLengthIsChunked && !hasChunkedHeader && noContentLengthHeader =>
true
case _ => false
}
val status = attrs.get(otoroshi.plugins.Keys.StatusOverrideKey).getOrElse(response.status)
val isHttp10 = rawRequest.version == "HTTP/1.0"
val willStream = if (isHttp10) false else (!isChunked)
val headersOutFiltered = Seq(
env.Headers.OtoroshiStateResp
).++(headersOutStatic).map(_.toLowerCase)
val headers: Seq[(String, String)] = response.headers
.filterNot { case (key, _) =>
headersOutFiltered.contains(key.toLowerCase())
}
.applyOnIf(!isHttp10)(_.filterNot(h => h._1.toLowerCase() == "content-length"))
.applyOnWithOpt(env.maxHeaderSizeToClient) {
case (hdrs, max) => {
hdrs.filter {
case (key, value) if key.length > max => {
HeaderTooLongAlert(key, value, "", "remove", "client", "engine", request, route, env).toAnalytics()
logger.error(
s"removing header '${key}' from response because it's too long. route is ${route.name} / ${route.id}. header value length is '${value.length}' and value is '${value}'"
)
false
}
case _ => true
}
}
}
.applyOnWithOpt(env.limitHeaderSizeToClient) {
case (hdrs, max) => {
hdrs.map {
case (key, value) if key.length > max => {
val newValue = value.substring(0, max.toInt - 1)
HeaderTooLongAlert(key, value, newValue, "limit", "client", "plugin", request, route, env).toAnalytics()
logger.error(
s"limiting header '${key}' from response to client because it's too long. route is ${route.name} / ${route.id}. header value length is '${value.length}' and value is '${value}', new value is '${newValue}'"
)
(key, newValue)
}
case (key, value) => (key, value)
}
}
}
.toSeq // ++ Seq(("Connection" -> "keep-alive"), ("X-Connection" -> "keep-alive"))
val theBody = response.body
.applyOnIf(env.dynamicBodySizeCompute && contentLength.isEmpty) { body =>
body.map { chunk =>
counterOut.addAndGet(chunk.size)
chunk
}
}
.applyOnIf(engineConfig.capture) { source =>
var responseChunks = ByteString("")
source
.map { chunk =>
if ((responseChunks.size + chunk.size) <= engineConfig.captureMaxEntitySize) {
responseChunks = responseChunks ++ chunk
}
chunk
}
.alsoTo(Sink.onComplete { case _ =>
TrafficCaptureEvent(route, rawRequest, request, rawResponse.response, response, responseChunks, attrs)
.toAnalytics()
})
} /*.map { bs =>
counterOut.addAndGet(bs.length)
bs
}*/
// TODO: should we enforce it as specified in rfc7230 ?
// * https://datatracker.ietf.org/doc/html/rfc7230#section-3.3
// * https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.1
// * https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2
// * https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.3
// if (response.status == 204) {
// FEither.right(Results
// .Status(status)
// .sendEntity(HttpEntity.NoEntity)
// .withHeaders(headers: _*)
// .withCookies(cookies: _*))
// } else
if (isHttp10) {
logger.warn(
s"HTTP/1.0 request, storing temporary result in memory :( (${rawRequest.theProtocol}://${rawRequest.theHost}${rawRequest.relativeUri})"
)
FEither(
theBody
.via(
MaxLengthLimiter(
globalConfig.maxHttp10ResponseSize.toInt,
str => logger.warn(str)
)
)
.runWith(
Sink.reduce[ByteString]((bs, n) => bs.concat(n))
)
.map { body =>
val response: Result = Results
.Status(status)
.sendEntity(HttpEntity.Strict(body, contentType))
.withHeaders(headers: _*)
.withCookies(cookies: _*)
//Status(status)(body)
//.withHeaders(headers: _*)
//.withCookies(cookies: _*)
contentType match {
case None => Right(response)
case Some(ctp) => Right(response.as(ctp))
}
}
)
} else {
isChunked match {
case true => {
// stream out
// val res = Status(status)
// .chunked(theBody)
// .withHeaders(headers: _*)
// .withCookies(cookies: _*)
val res = Results
.Status(status)
.sendEntity(
HttpEntity.Chunked(
theBody
.map(bs => HttpChunk.Chunk(bs))
.concat(Source.single(HttpChunk.LastChunk(Headers.create()))),
contentType
)
)
.withHeaders(headers: _*)
.withCookies(cookies: _*)
contentType match {
case None => FEither.right(res)
case Some(ctp) => FEither.right(res.as(ctp))
}
}
case false => {
val res = Results
.Status(status)
.sendEntity(
HttpEntity.Streamed(
theBody,
contentLengthOut,
contentType
)
)
.withHeaders(headers: _*)
.withCookies(cookies: _*)
contentType match {
case None => FEither.right(res)
case Some(ctp) => FEither.right(res.as(ctp))
}
}
}
}
}
def triggerWsProxyDone(
snowflake: String,
rawRequest: RequestHeader,
request: NgPluginHttpRequest,
route: NgRoute,
backend: NgTarget,
sb: NgSelectedBackendTarget
)(implicit
ec: ExecutionContext,
env: Env,
report: NgExecutionReport,
globalConfig: GlobalConfig,
attrs: TypedMap,
mat: Materializer
): FEither[NgProxyEngineError, Done] = {
Future {
val actualDuration: Long = report.getDurationNow()
val overhead: Long = report.getOverheadNow()
val upstreamLatency: Long = report.getStep("call-backend").map(_.duration).getOrElse(-1L)
val apiKey = attrs.get(otoroshi.plugins.Keys.ApiKeyKey)
val paUsr = attrs.get(otoroshi.plugins.Keys.UserKey)
val callDate = attrs.get(otoroshi.plugins.Keys.RequestTimestampKey).get
val counterIn = attrs.get(otoroshi.plugins.Keys.RequestCounterInKey).get
val counterOut = attrs.get(otoroshi.plugins.Keys.RequestCounterOutKey).get
val fromOtoroshi = rawRequest.headers
.get(env.Headers.OtoroshiRequestId)
.orElse(rawRequest.headers.get(env.Headers.OtoroshiGatewayParentRequest))
val duration: Long = {
if (route.id == env.backOfficeServiceId && actualDuration > 300L)
300L
else actualDuration
}
// increments calls for service and globally
env.analyticsQueue ! AnalyticsQueueEvent(
route.serviceDescriptor,
duration,
overhead,
counterIn.get(),
counterOut.get(),
upstreamLatency,
globalConfig
)
route.backend.loadBalancing match {
case BestResponseTime =>
BestResponseTime.incrementAverage(route.cacheableId, backend.toTarget, duration)
case WeightedBestResponseTime(_) =>
BestResponseTime.incrementAverage(route.cacheableId, backend.toTarget, duration)
case _ =>
}
val fromLbl =
rawRequest.headers
.get(env.Headers.OtoroshiVizFromLabel)
.getOrElse("internet")
val viz: OtoroshiViz = OtoroshiViz(
to = route.id,
toLbl = route.name,
from = rawRequest.headers
.get(env.Headers.OtoroshiVizFrom)
.getOrElse("internet"),
fromLbl = fromLbl,
fromTo = s"$fromLbl###${route.name}"
)
val cbDuration = System.currentTimeMillis() - sb.cbStart
val evt = GatewayEvent(
`@id` = env.snowflakeGenerator.nextIdStr(),
reqId = snowflake,
parentReqId = fromOtoroshi,
`@timestamp` = DateTime.now(),
`@calledAt` = callDate,
protocol = rawRequest.version,
to = Location(
scheme = rawRequest.theProtocol,
host = rawRequest.theHost,
uri = rawRequest.relativeUri
),
target = Location(
scheme = backend.toTarget.scheme,
host = backend.toTarget.host,
uri = rawRequest.relativeUri
),
backendDuration = attrs.get(otoroshi.plugins.Keys.BackendDurationKey).getOrElse(-1L),
duration = duration,
overhead = overhead,
cbDuration = cbDuration,
overheadWoCb = Math.abs(overhead - cbDuration),
callAttempts = sb.attempts,
url = rawRequest.theUrl,
method = rawRequest.method,
from = rawRequest.theIpAddress,
env = route.metadata.get("otoroshi-core-env").getOrElse("prod"),
data = DataInOut(
dataIn = counterIn.get(),
dataOut = counterOut.get()
),
status = 200,
headers = rawRequest.headers.toSimpleMap.toSeq.map(Header.apply),
headersOut = Seq.empty,
otoroshiHeadersIn = request.headers.toSeq.map(Header.apply),
otoroshiHeadersOut = Seq.empty,
extraInfos = attrs.get(otoroshi.plugins.Keys.GatewayEventExtraInfosKey),
identity = apiKey
.map(k =>
Identity(
identityType = "APIKEY",
identity = k.clientId,
label = k.clientName,
tags = k.tags,
metadata = k.metadata
)
)
.orElse(
paUsr.map(k =>
Identity(
identityType = "PRIVATEAPP",
identity = k.email,
label = k.name,
tags = k.tags,
metadata = k.metadata
)
)
),
responseChunked = false,
`@serviceId` = route.id,
`@service` = route.name,
descriptor = Some(route.legacy),
route = Some(route),
`@product` = route.metadata.getOrElse("product", "--"),
remainingQuotas = attrs.get(otoroshi.plugins.Keys.ApiKeyRemainingQuotasKey).getOrElse(RemainingQuotas()),
viz = Some(viz),
clientCertChain = rawRequest.clientCertChainPem,
err = attrs.get(otoroshi.plugins.Keys.GwErrorKey).isDefined,
gwError = attrs.get(otoroshi.plugins.Keys.GwErrorKey).map(_.message),
userAgentInfo = attrs.get[JsValue](otoroshi.plugins.Keys.UserAgentInfoKey),
geolocationInfo = attrs.get[JsValue](otoroshi.plugins.Keys.GeolocationInfoKey),
extraAnalyticsData = attrs.get[JsValue](otoroshi.plugins.Keys.ExtraAnalyticsDataKey)
)
evt.toAnalytics()
}(env.analyticsExecutionContext)
FEither.right(Done)
}
def triggerProxyDone(
snowflake: String,
rawRequest: Request[Source[ByteString, _]],
rawResponse: BackendCallResponse,
request: NgPluginHttpRequest,
response: NgPluginHttpResponse,
route: NgRoute,
backend: NgTarget,
sb: NgSelectedBackendTarget
)(implicit
ec: ExecutionContext,
env: Env,
report: NgExecutionReport,
globalConfig: GlobalConfig,
attrs: TypedMap,
mat: Materializer
): FEither[NgProxyEngineError, Done] = {
attrs
.get(otoroshi.plugins.Keys.ResponseEndPromiseKey)
.foreach(_.future.andThen { case _ =>
val actualDuration: Long = report.getDurationNow()
val overhead: Long = report.getOverheadNow()
val upstreamLatency: Long = report.getStep("call-backend").map(_.duration).getOrElse(-1L)
val apiKey = attrs.get(otoroshi.plugins.Keys.ApiKeyKey)
val paUsr = attrs.get(otoroshi.plugins.Keys.UserKey)
val callDate = attrs.get(otoroshi.plugins.Keys.RequestTimestampKey).get
val counterIn = attrs.get(otoroshi.plugins.Keys.RequestCounterInKey).get
val counterOut = attrs.get(otoroshi.plugins.Keys.RequestCounterOutKey).get
val fromOtoroshi = rawRequest.headers
.get(env.Headers.OtoroshiRequestId)
.orElse(rawRequest.headers.get(env.Headers.OtoroshiGatewayParentRequest))
val noContentLengthHeader: Boolean =
rawResponse.contentLength.isEmpty
val hasChunkedHeader: Boolean = rawResponse
.header("Transfer-Encoding")
.exists(h => h.toLowerCase().contains("chunked"))
val isContentLengthZero: Boolean = rawResponse.header("Content-Length").contains("0")
val isChunked: Boolean = rawResponse.isChunked() match {
case _ if isContentLengthZero => false
case Some(chunked) => chunked
case None if !env.emptyContentLengthIsChunked =>
hasChunkedHeader // false
case None if env.emptyContentLengthIsChunked && hasChunkedHeader =>
true
case None if env.emptyContentLengthIsChunked && !hasChunkedHeader && noContentLengthHeader =>
true
case _ => false
}
val duration: Long = {
if (route.id == env.backOfficeServiceId && actualDuration > 300L)
300L
else actualDuration
}
// increments calls for service and globally
env.analyticsQueue ! AnalyticsQueueEvent(
route.serviceDescriptor,
duration,
overhead,
counterIn.get(),
counterOut.get(),
upstreamLatency,
globalConfig
)
route.backend.loadBalancing match {
case BestResponseTime =>
BestResponseTime.incrementAverage(route.cacheableId, backend.toTarget, duration)
case WeightedBestResponseTime(_) =>
BestResponseTime.incrementAverage(route.cacheableId, backend.toTarget, duration)
case _ =>
}
val fromLbl =
rawRequest.headers
.get(env.Headers.OtoroshiVizFromLabel)
.getOrElse("internet")
val viz: OtoroshiViz = OtoroshiViz(
to = route.id,
toLbl = route.name,
from = rawRequest.headers
.get(env.Headers.OtoroshiVizFrom)
.getOrElse("internet"),
fromLbl = fromLbl,
fromTo = s"$fromLbl###${route.name}"
)
val cbDuration = System.currentTimeMillis() - sb.cbStart
val evt = GatewayEvent(
`@id` = env.snowflakeGenerator.nextIdStr(),
reqId = snowflake,
parentReqId = fromOtoroshi,
`@timestamp` = DateTime.now(),
`@calledAt` = callDate,
protocol = rawRequest.version,
to = Location(
scheme = rawRequest.theProtocol,
host = rawRequest.theHost,
uri = rawRequest.relativeUri
),
target = Location(
scheme = backend.toTarget.scheme,
host = backend.toTarget.host,
uri = rawRequest.relativeUri
),
backendDuration = attrs.get(otoroshi.plugins.Keys.BackendDurationKey).getOrElse(-1L),
duration = duration,
overhead = overhead,
cbDuration = cbDuration,
overheadWoCb = Math.abs(overhead - cbDuration),
callAttempts = sb.attempts,
url = rawRequest.theUrl,
method = rawRequest.method,
from = rawRequest.theIpAddress,
env = route.metadata.get("otoroshi-core-env").getOrElse("prod"),
data = DataInOut(
dataIn = counterIn.get(),
dataOut = counterOut.get()
),
status = rawResponse.status,
headers = rawRequest.headers.toSimpleMap.toSeq.map(Header.apply),
headersOut = rawResponse.headers.mapValues(_.last).toSeq.map(Header.apply),
otoroshiHeadersIn = request.headers.toSeq.map(Header.apply),
otoroshiHeadersOut = response.headers.toSeq.map(Header.apply),
extraInfos = attrs.get(otoroshi.plugins.Keys.GatewayEventExtraInfosKey),
identity = apiKey
.map(k =>
Identity(
identityType = "APIKEY",
identity = k.clientId,
label = k.clientName,
tags = k.tags,
metadata = k.metadata
)
)
.orElse(
paUsr.map(k =>
Identity(
identityType = "PRIVATEAPP",
identity = k.email,
label = k.name,
tags = k.tags,
metadata = k.metadata
)
)
),
responseChunked = isChunked,
`@serviceId` = route.id,
`@service` = route.name,
descriptor = Some(route.legacy),
route = Some(route),
`@product` = route.metadata.getOrElse("product", "--"),
remainingQuotas = attrs.get(otoroshi.plugins.Keys.ApiKeyRemainingQuotasKey).getOrElse(RemainingQuotas()),
viz = Some(viz),
clientCertChain = rawRequest.clientCertChainPem,
err = attrs.get(otoroshi.plugins.Keys.GwErrorKey).isDefined,
gwError = attrs.get(otoroshi.plugins.Keys.GwErrorKey).map(_.message),
userAgentInfo = attrs.get[JsValue](otoroshi.plugins.Keys.UserAgentInfoKey),
geolocationInfo = attrs.get[JsValue](otoroshi.plugins.Keys.GeolocationInfoKey),
extraAnalyticsData = attrs.get[JsValue](otoroshi.plugins.Keys.ExtraAnalyticsDataKey)
)
evt.toAnalytics()
}(env.analyticsExecutionContext))
FEither.right(Done)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy