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

gateway.http.scala Maven / Gradle / Ivy

The newest version!
package otoroshi.gateway

import akka.actor.ActorRef
import akka.http.scaladsl.util.FastFuture
import akka.http.scaladsl.util.FastFuture._
import akka.stream.scaladsl.{Sink, Source}
import akka.util.ByteString
import otoroshi.env.Env
import otoroshi.events._
import otoroshi.models.{BestResponseTime, ClientConfig, RemainingQuotas, SecComVersion, WeightedBestResponseTime}
import org.joda.time.DateTime
import otoroshi.el.TargetExpressionLanguage
import otoroshi.script.Implicits._
import otoroshi.script.{
  TransformerRequestBodyContext,
  TransformerRequestContext,
  TransformerResponseBodyContext,
  TransformerResponseContext
}
import otoroshi.utils.UrlSanitizer
import play.api.Logger
import play.api.http.HttpEntity
import play.api.libs.json.{JsArray, JsObject, JsString, JsValue, Json}
import play.api.libs.streams.Accumulator
import play.api.libs.ws.{DefaultWSCookie, EmptyBody, SourceBody}
import play.api.mvc.Results.{BadGateway, Forbidden, HttpVersionNotSupported, NotFound, Status}
import play.api.mvc._
import otoroshi.security.{IdGenerator, OtoroshiClaim}
import otoroshi.utils.http.RequestImplicits._
import otoroshi.utils.http.{HeadersHelper, WSCookieWithSameSite}
import otoroshi.utils.http.Implicits._
import otoroshi.utils.streams.MaxLengthLimiter
import otoroshi.utils.syntax.implicits.BetterSyntax

import scala.concurrent.{Future, Promise}
import scala.util.{Failure, Success}

class HttpHandler()(implicit env: Env) {

  implicit lazy val currentEc           = env.otoroshiExecutionContext
  implicit lazy val currentScheduler    = env.otoroshiScheduler
  implicit lazy val currentSystem       = env.otoroshiActorSystem
  implicit lazy val currentMaterializer = env.otoroshiMaterializer

  lazy val logger = Logger("otoroshi-http-handler")

  val sourceBodyParser = BodyParser("Http BodyParser") { _ =>
    Accumulator.source[ByteString].map(Right.apply)
  }

  def rawForwardCall(
      reverseProxyAction: ReverseProxyAction,
      analyticsQueue: ActorRef,
      snowMonkey: SnowMonkey,
      headersInFiltered: Seq[String],
      headersOutFiltered: Seq[String]
  ): (RequestHeader, Source[ByteString, _]) => Future[Result] = { (req, body) =>
    {
      reverseProxyAction
        .async[Result](
          ReverseProxyActionContext(req, body, snowMonkey, logger),
          false,
          c => actuallyCallDownstream(c, analyticsQueue, headersInFiltered, headersOutFiltered)
        )
        .map {
          case Left(r)  => r
          case Right(r) => r
        }
    }
  }

  def forwardCall(
      actionBuilder: ActionBuilder[Request, AnyContent],
      reverseProxyAction: ReverseProxyAction,
      analyticsQueue: ActorRef,
      snowMonkey: SnowMonkey,
      headersInFiltered: Seq[String],
      headersOutFiltered: Seq[String]
  ) =
    actionBuilder.async(sourceBodyParser) { req =>
      env.metrics.withTimerAsync("handle-request")(
        reverseProxyAction
          .async[Result](
            ReverseProxyActionContext(req, req.body, snowMonkey, logger),
            false,
            c => actuallyCallDownstream(c, analyticsQueue, headersInFiltered, headersOutFiltered)
          )
          .map {
            case Left(r)  => r
            case Right(r) => r
          }
      )
    }

  def forwardAction(
      reverseProxyAction: ReverseProxyAction,
      analyticsQueue: ActorRef,
      snowMonkey: SnowMonkey,
      headersInFiltered: Seq[String],
      headersOutFiltered: Seq[String]
  ): Request[Source[ByteString, _]] => Future[Result] = (req: Request[Source[ByteString, _]]) => {
    reverseProxyAction
      .async[Result](
        ReverseProxyActionContext(req, req.body, snowMonkey, logger),
        false,
        c => actuallyCallDownstream(c, analyticsQueue, headersInFiltered, headersOutFiltered)
      )
      .map {
        case Left(r)  => r
        case Right(r) => r
      }
  }

  def actuallyCallDownstream(
      ctx: ActualCallContext,
      analyticsQueue: ActorRef,
      headersInFiltered: Seq[String],
      headersOutFiltered: Seq[String]
  ): Future[Either[Result, Result]] = {

    val ActualCallContext(
      req,
      descriptor,
      _target,
      apiKey,
      paUsr,
      jwtInjection,
      snowMonkeyContext,
      snowflake,
      attrs,
      elCtx,
      globalConfig,
      withTrackingCookies,
      bodyAlreadyConsumed,
      requestBody,
      secondStart,
      firstOverhead,
      cbDuration,
      callAttempts,
      attempts,
      alreadyFailed
    ) = ctx

    val counterIn  = attrs.get(otoroshi.plugins.Keys.RequestCounterInKey).get
    val counterOut = attrs.get(otoroshi.plugins.Keys.RequestCounterOutKey).get
    val canaryId   = attrs.get(otoroshi.plugins.Keys.RequestCanaryIdKey).get
    val callDate   = attrs.get(otoroshi.plugins.Keys.RequestTimestampKey).get
    val start      = attrs.get(otoroshi.plugins.Keys.RequestStartKey).get

    val requestTimestamp                                    = callDate.toString("yyyy-MM-dd'T'HH:mm:ss.SSSZZ")
    val jti                                                 = IdGenerator.uuid
    val stateValue                                          = IdGenerator.extendedToken(128)
    val stateToken: String                                  = descriptor.secComVersion match {
      case SecComVersion.V1 => stateValue
      case SecComVersion.V2 =>
        OtoroshiClaim(
          iss = env.Headers.OtoroshiIssuer,
          sub = env.Headers.OtoroshiIssuer,
          aud = descriptor.name,
          exp = DateTime
            .now()
            .plus(descriptor.secComTtl.toMillis)
            .toDate
            .getTime,
          iat = DateTime.now().toDate.getTime,
          jti = jti
        ).withClaim("state", stateValue)
          .serialize(descriptor.algoChallengeFromOtoToBack)
    }
    val rawUri                                              = req.relativeUri.substring(1)
    val uriParts                                            = rawUri.split("/").toSeq
    val uri: String                                         = descriptor.maybeStrippedUri(req, rawUri)
    val scheme                                              =
      if (descriptor.redirectToLocal) descriptor.localScheme else _target.scheme
    val host                                                = TargetExpressionLanguage(
      if (descriptor.redirectToLocal)
        descriptor.localHost
      else _target.host,
      Some(req),
      Some(descriptor),
      None,
      apiKey,
      paUsr,
      elCtx,
      attrs,
      env
    )
    val root                                                = descriptor.root
    val url                                                 = TargetExpressionLanguage(
      s"$scheme://$host$root$uri",
      Some(req),
      Some(descriptor),
      None,
      apiKey,
      paUsr,
      elCtx,
      attrs,
      env
    )
    lazy val (currentReqHasBody, shouldInjectContentLength) = req.theHasBodyWithoutLength
    // val queryString = req.queryString.toSeq.flatMap { case (key, values) => values.map(v => (key, v)) }
    val fromOtoroshi                                        = req.headers
      .get(env.Headers.OtoroshiRequestId)
      .orElse(req.headers.get(env.Headers.OtoroshiGatewayParentRequest))
    val promise                                             = Promise[ProxyDone]

    val claim = descriptor.generateInfoToken(apiKey, paUsr, Some(req))
    if (logger.isTraceEnabled) logger.trace(s"Claim is : $claim")
    attrs.put(otoroshi.plugins.Keys.OtoTokenKey -> claim.payload)

    val stateResponseHeaderName = descriptor.secComHeaders.stateResponseName
      .getOrElse(env.Headers.OtoroshiStateResp)

    val headersIn: Seq[(String, String)] = HeadersHelper.composeHeadersIn(
      descriptor = descriptor,
      req = req,
      apiKey = apiKey,
      paUsr = paUsr,
      elCtx = elCtx,
      currentReqHasBody = currentReqHasBody,
      headersInFiltered = headersInFiltered,
      snowflake = snowflake,
      requestTimestamp = requestTimestamp,
      host = host,
      claim = claim,
      stateToken = stateToken,
      fromOtoroshi = fromOtoroshi,
      snowMonkeyContext = snowMonkeyContext,
      jwtInjection = jwtInjection,
      attrs = attrs
    )

    val lazySource = Source.single(ByteString.empty).flatMapConcat { _ =>
      bodyAlreadyConsumed.compareAndSet(false, true)
      requestBody
        .concat(snowMonkeyContext.trailingRequestBodyStream)
        .map(bs => {
          // meterIn.mark(bs.length)
          counterIn.addAndGet(bs.length)
          bs
        })
    }

    val overhead                        = (System.currentTimeMillis() - secondStart) + firstOverhead
    if (overhead > env.overheadThreshold) {
      HighOverheadAlert(
        `@id` = env.snowflakeGenerator.nextIdStr(),
        limitOverhead = env.overheadThreshold,
        currentOverhead = overhead,
        serviceDescriptor = descriptor.some,
        target = Location(
          scheme = req.theProtocol,
          host = req.theHost,
          uri = req.relativeUri
        )
      ).toAnalytics()
    }
    val quotas: Future[RemainingQuotas] =
      apiKey
        .map(_.updateQuotas())
        .getOrElse(FastFuture.successful(RemainingQuotas()))
    promise.future.andThen {
      case Success(resp) => {

        val actualDuration: Long = System.currentTimeMillis() - start
        val duration: Long       =
          if (descriptor.id == env.backOfficeServiceId && actualDuration > 300L)
            300L
          else actualDuration

        analyticsQueue ! AnalyticsQueueEvent(
          descriptor,
          duration,
          overhead,
          counterIn.get(),
          counterOut.get(),
          resp.upstreamLatency,
          globalConfig
        )

        descriptor.targetsLoadBalancing match {
          case BestResponseTime            =>
            BestResponseTime.incrementAverage(descriptor, _target, duration)
          case WeightedBestResponseTime(_) =>
            BestResponseTime.incrementAverage(descriptor, _target, duration)
          case _                           =>
        }

        quotas.andThen {
          case Success(q) => {
            val fromLbl          =
              req.headers
                .get(env.Headers.OtoroshiVizFromLabel)
                .getOrElse("internet")
            val viz: OtoroshiViz = OtoroshiViz(
              to = descriptor.id,
              toLbl = descriptor.name,
              from = req.headers
                .get(env.Headers.OtoroshiVizFrom)
                .getOrElse("internet"),
              fromLbl = fromLbl,
              fromTo = s"$fromLbl###${descriptor.name}"
            )
            val evt              = GatewayEvent(
              `@id` = env.snowflakeGenerator.nextIdStr(),
              reqId = snowflake,
              parentReqId = fromOtoroshi,
              `@timestamp` = DateTime.now(),
              `@calledAt` = callDate,
              protocol = req.version,
              to = Location(
                scheme = req.theProtocol,
                host = req.theHost,
                uri = req.relativeUri
              ),
              target = Location(
                scheme = scheme,
                host = host,
                uri = req.relativeUri
              ),
              backendDuration = attrs.get(otoroshi.plugins.Keys.BackendDurationKey).getOrElse(-1L),
              duration = duration,
              overhead = overhead,
              cbDuration = cbDuration,
              overheadWoCb = Math.abs(overhead - cbDuration),
              callAttempts = callAttempts,
              url = url,
              method = req.method,
              from = req.theIpAddress,
              env = descriptor.env,
              data = DataInOut(
                dataIn = counterIn.get(),
                dataOut = counterOut.get()
              ),
              status = resp.status,
              headers = req.headers.toSimpleMap.toSeq.map(Header.apply),
              headersOut = resp.headersOut,
              otoroshiHeadersIn = resp.otoroshiHeadersIn,
              otoroshiHeadersOut = resp.otoroshiHeadersOut,
              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 = resp.isChunked,
              `@serviceId` = descriptor.id,
              `@service` = descriptor.name,
              descriptor = Some(descriptor),
              `@product` = descriptor.metadata.getOrElse("product", "--"),
              remainingQuotas = q,
              viz = Some(viz),
              clientCertChain = req.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()
            if (descriptor.logAnalyticsOnServer) {
              evt.log()(env, env.analyticsExecutionContext) // pressure EC
            }
          }
        }(env.analyticsExecutionContext) // pressure EC
      }
    }(env.analyticsExecutionContext) // pressure EC
    //.andThen {
    //  case _ => env.datastores.requestsDataStore.decrementProcessedRequests()
    //}
    val wsCookiesIn                     = req.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 rawRequest                      = otoroshi.script.HttpRequest(
      url = s"${req.theProtocol}://${req.theHost}${req.relativeUri}",
      method = req.method,
      headers = req.headers.toSimpleMap,
      cookies = wsCookiesIn,
      version = req.version,
      clientCertificateChain = req.clientCertificateChain,
      target = None,
      claims = claim,
      body = () => requestBody
    )
    val otoroshiRequest                 = otoroshi.script.HttpRequest(
      url = url,
      method = req.method,
      headers = headersIn.toMap,
      cookies = wsCookiesIn,
      version = req.version,
      clientCertificateChain = req.clientCertificateChain,
      target = Some(_target),
      claims = claim,
      body = () => requestBody
    )
    val transReqCtx                     = TransformerRequestContext(
      index = -1,
      snowflake = snowflake,
      rawRequest = rawRequest,
      otoroshiRequest = otoroshiRequest,
      descriptor = descriptor,
      apikey = apiKey,
      user = paUsr,
      request = req,
      config = descriptor.transformerConfig,
      attrs = attrs
    )
    val finalRequest                    = descriptor
      .transformRequest(transReqCtx)
    val finalBody                       = descriptor.transformRequestBody(
      TransformerRequestBodyContext(
        index = -1,
        snowflake = snowflake,
        rawRequest = rawRequest,
        otoroshiRequest = otoroshiRequest,
        descriptor = descriptor,
        apikey = apiKey,
        user = paUsr,
        request = req,
        config = descriptor.transformerConfig,
        body = lazySource,
        attrs = attrs
      )
    )
    finalRequest
      .flatMap {
        case Left(badResult)    => {
          quotas.fast.map { remainingQuotas =>
            val _headersOut: Seq[(String, String)] =
              HeadersHelper.composeHeadersOutBadResult(
                descriptor = descriptor,
                req = req,
                badResult = badResult,
                apiKey = apiKey,
                paUsr = paUsr,
                elCtx = elCtx,
                snowflake = snowflake,
                requestTimestamp = requestTimestamp,
                headersOutFiltered = headersOutFiltered,
                overhead = overhead,
                upstreamLatency = 0L,
                canaryId = canaryId,
                remainingQuotas = remainingQuotas,
                attrs = attrs
              )
            promise.trySuccess(
              ProxyDone(
                badResult.header.status,
                false,
                0,
                headersOut = badResult.header.headers.toSeq.map(Header.apply),
                otoroshiHeadersOut = _headersOut.map(Header.apply),
                otoroshiHeadersIn = headersIn.map(Header.apply)
              )
            )
            badResult.withHeaders(_headersOut: _*)
          }
        }
        case Right(httpRequest) => {
          val upstreamStart = System.currentTimeMillis()
          // Stream IN
          val body          =
            if (currentReqHasBody) SourceBody(finalBody)
            else EmptyBody

          val finalTarget = httpRequest.target.getOrElse(_target)
          attrs.put(otoroshi.plugins.Keys.RequestTargetKey -> finalTarget)

          val clientReq = descriptor.useAkkaHttpClient match {
            case _ if finalTarget.mtlsConfig.mtls =>
              env.gatewayClient.akkaUrlWithTarget(
                UrlSanitizer.sanitize(httpRequest.url),
                finalTarget,
                descriptor.clientConfig
              )
            case true                             =>
              env.gatewayClient.akkaUrlWithTarget(
                UrlSanitizer.sanitize(httpRequest.url),
                finalTarget,
                descriptor.clientConfig
              )
            case false                            =>
              env.gatewayClient.urlWithTarget(
                UrlSanitizer.sanitize(httpRequest.url),
                finalTarget,
                descriptor.clientConfig
              )
          }

          val extractedTimeout =
            descriptor.clientConfig.extractTimeout(req.relativeUri, _.callAndStreamTimeout, _.callAndStreamTimeout)
          if (ClientConfig.logger.isDebugEnabled)
            ClientConfig.logger.debug(s"[gateway] using callAndStreamTimeout: $extractedTimeout")
          val builder          = clientReq
            .withRequestTimeout(extractedTimeout)
            .withFailureIndicator(alreadyFailed)
            //.withRequestTimeout(env.requestTimeout) // we should monitor leaks
            .withMethod(httpRequest.method)
            // .withHttpHeaders(httpRequest.headers.toSeq.filterNot(_._1 == "Cookie"): _*)
            .withHttpHeaders(
              HeadersHelper
                .addClaims(httpRequest.headers, httpRequest.claims, descriptor)
                .filterNot(_._1 == "Cookie"): _*
            )
            .withCookies(wsCookiesIn: _*)
            .withFollowRedirects(false)
            .withMaybeProxyServer(
              descriptor.clientConfig.proxy.orElse(globalConfig.proxies.services)
            )

          // because writeableOf_WsBody always add a 'Content-Type: application/octet-stream' header
          val builderWithBody = if (currentReqHasBody) {
            if (shouldInjectContentLength) {
              builder.addHttpHeaders("Content-Length" -> "0").withBody(body)
            } else {
              builder.withBody(body)
            }
          } else {
            builder
          }

          builderWithBody
            .stream()
            .flatMap(resp => quotas.fast.map(q => (resp, q)))
            .flatMap { tuple =>
              val isUp                                  = true
              val (resp, remainingQuotas)               = tuple
              // val responseHeader          = ByteString(s"HTTP/1.1 ${resp.headers.status}")
              val headers                               = resp.headers.mapValues(_.head)
              val _headersForOut: Seq[(String, String)] =
                resp.headers.toSeq.flatMap(c =>
                  c._2.map(v => (c._1, v))
                ) //.map(tuple => (tuple._1, tuple._2.mkString(","))) //.toSimpleMap // .mapValues(_.head)
              val rawResponse         = otoroshi.script.HttpResponse(
                status = resp.status,
                headers = headers.toMap,
                cookies = resp.cookies,
                body = () => resp.bodyAsSource
              )
              val stateRespHeaderName = descriptor.secComHeaders.stateResponseName
                .getOrElse(env.Headers.OtoroshiStateResp)
              val stateResp           = headers
                .get(stateRespHeaderName)
                .orElse(headers.get(stateRespHeaderName.toLowerCase))
              ReverseProxyActionHelper.stateRespValidM(stateValue, stateResp, jti, descriptor, uri, req) match {
                case Left(stateRespInvalid) => {
                  resp.ignore()
                  if (
                    resp.status == 404 && headers
                      .get("X-CleverCloudUpgrade")
                      .contains("true")
                  ) {
                    Errors.craftResponseResult(
                      "No service found for the specified target host, the service descriptor should be verified !",
                      NotFound,
                      req,
                      Some(descriptor),
                      Some("errors.no.service.found"),
                      duration = System.currentTimeMillis - start,
                      overhead = (System
                        .currentTimeMillis() - secondStart) + firstOverhead,
                      cbDuration = cbDuration,
                      callAttempts = callAttempts,
                      attrs = attrs
                    )
                  } else if (isUp) {
                    logger.error(stateRespInvalid.errorMessage(resp.status, resp.headers.mapValues(_.last)))
                    val extraInfos    = attrs
                      .get(otoroshi.plugins.Keys.GatewayEventExtraInfosKey)
                      .map(_.as[JsObject])
                      .getOrElse(Json.obj())
                    val newExtraInfos =
                      extraInfos ++ Json.obj(
                        "stateRespInvalid" -> stateRespInvalid
                          .exchangePayload(resp.status, resp.headers.mapValues(_.last))
                      )
                    attrs.put(otoroshi.plugins.Keys.GatewayEventExtraInfosKey -> newExtraInfos)
                    Errors.craftResponseResult(
                      "Backend server does not seems to be secured. Cancelling request !",
                      BadGateway,
                      req,
                      Some(descriptor),
                      Some("errors.service.not.secured"),
                      duration = System.currentTimeMillis - start,
                      overhead = (System
                        .currentTimeMillis() - secondStart) + firstOverhead,
                      cbDuration = cbDuration,
                      callAttempts = callAttempts,
                      attrs = attrs
                    )
                  } else {
                    Errors.craftResponseResult(
                      "The service seems to be down :( come back later",
                      Forbidden,
                      req,
                      Some(descriptor),
                      Some("errors.service.down"),
                      duration = System.currentTimeMillis - start,
                      overhead = (System
                        .currentTimeMillis() - secondStart) + firstOverhead,
                      cbDuration = cbDuration,
                      callAttempts = callAttempts,
                      attrs = attrs
                    )
                  }
                }
                case Right(_)               => {
                  val upstreamLatency                    = System.currentTimeMillis() - upstreamStart
                  val _headersOut: Seq[(String, String)] =
                    HeadersHelper.composeHeadersOut(
                      descriptor = descriptor,
                      req = req,
                      resp = resp,
                      apiKey = apiKey,
                      paUsr = paUsr,
                      elCtx = elCtx,
                      snowflake = snowflake,
                      requestTimestamp = requestTimestamp,
                      headersOutFiltered = headersOutFiltered,
                      overhead = overhead,
                      upstreamLatency = upstreamLatency,
                      canaryId = canaryId,
                      remainingQuotas = remainingQuotas,
                      attrs = attrs
                    )

                  val otoroshiResponse = otoroshi.script.HttpResponse(
                    status = resp.status,
                    headers = _headersOut.toMap,
                    cookies = resp.cookies,
                    body = () => resp.bodyAsSource
                  )
                  descriptor
                    .transformResponse(
                      TransformerResponseContext(
                        index = -1,
                        snowflake = snowflake,
                        rawResponse = rawResponse,
                        otoroshiResponse = otoroshiResponse,
                        descriptor = descriptor,
                        apikey = apiKey,
                        user = paUsr,
                        request = req,
                        config = descriptor.transformerConfig,
                        attrs = attrs
                      )
                    )
                    .flatMap {
                      case Left(badResult)     => {
                        resp.ignore()
                        FastFuture.successful(badResult)
                      }
                      case Right(httpResponse) => {
                        val headersOut                  = httpResponse.headers.toSeq
                        val contentType: Option[String] = httpResponse.headers
                          .get("Content-Type")
                          .orElse(httpResponse.headers.get("content-type"))

                        val noContentLengthHeader: Boolean =
                          resp.contentLength.isEmpty
                        val hasChunkedHeader: Boolean      = resp
                          .header("Transfer-Encoding")
                          .orElse(httpResponse.headers.get("Transfer-Encoding"))
                          .exists(h => h.toLowerCase().contains("chunked"))
                        val isContentLengthZero: Boolean   =
                          resp.header("Content-Length").orElse(httpResponse.headers.get("Content-Length")).contains("0")
                        val isChunked: Boolean             = resp.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 theStream: Source[ByteString, _] = resp.bodyAsSource
                          .concat(snowMonkeyContext.trailingResponseBodyStream)
                          .alsoTo(Sink.onComplete {
                            case Success(_) =>
                              // debugLogger.trace(s"end of stream for ${protocol}://${req.host}${req.relativeUri}")
                              promise.trySuccess(
                                ProxyDone(
                                  httpResponse.status,
                                  isChunked,
                                  upstreamLatency,
                                  headersOut = resp.headers.mapValues(_.head).toSeq.map(Header.apply),
                                  otoroshiHeadersOut = headersOut.map(Header.apply),
                                  otoroshiHeadersIn = headersIn.map(Header.apply)
                                )
                              )
                            case Failure(e) =>
                              if (
                                !(req.relativeUri
                                  .startsWith("/api/live/global") && e.getMessage == "Connection reset by peer")
                              ) {
                                logger.error(
                                  s"error while transfering stream for ${req.theProtocol}://${req.theHost}${req.relativeUri}",
                                  e
                                )
                              }
                              resp.ignore()
                              promise.trySuccess(
                                ProxyDone(
                                  httpResponse.status,
                                  isChunked,
                                  upstreamLatency,
                                  headersOut = resp.headers.mapValues(_.head).toSeq.map(Header.apply),
                                  otoroshiHeadersOut = headersOut.map(Header.apply),
                                  otoroshiHeadersIn = headersIn.map(Header.apply)
                                )
                              )
                          })
                          .map { bs =>
                            counterOut.addAndGet(bs.length)
                            bs
                          }

                        val finalStream = descriptor.transformResponseBody(
                          TransformerResponseBodyContext(
                            index = -1,
                            snowflake = snowflake,
                            rawResponse = rawResponse,
                            otoroshiResponse = otoroshiResponse,
                            descriptor = descriptor,
                            apikey = apiKey,
                            user = paUsr,
                            request = req,
                            config = descriptor.transformerConfig,
                            body = theStream,
                            attrs = attrs
                          )
                        )

                        val cookies = httpResponse.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] = resp.headers.get("Set-Cookie").flatMap { values =>
                              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
                            )
                          }
                        }

                        if (req.version == "HTTP/1.0") {
                          if (descriptor.allowHttp10) {
                            logger.warn(
                              s"HTTP/1.0 request, storing temporary result in memory :( (${req.theProtocol}://${req.theHost}${req.relativeUri})"
                            )
                            finalStream
                              .via(
                                MaxLengthLimiter(
                                  globalConfig.maxHttp10ResponseSize.toInt,
                                  str => logger.warn(str)
                                )
                              )
                              .runWith(
                                Sink.reduce[ByteString]((bs, n) => bs.concat(n))
                              )
                              .fast
                              .flatMap { body =>
                                val response: Result = Status(
                                  attrs.get(otoroshi.plugins.Keys.StatusOverrideKey).getOrElse(httpResponse.status)
                                )(body)
                                  .withHeaders(
                                    headersOut.filterNot { h =>
                                      val lower = h._1.toLowerCase()
                                      lower == "content-type" || lower == "set-cookie" || lower == "transfer-encoding"
                                    }: _*
                                  )
                                  .withCookies(
                                    withTrackingCookies ++ jwtInjection.additionalCookies
                                      .map(t => Cookie(t._1, t._2)) ++ cookies: _*
                                  )
                                contentType match {
                                  case None      => descriptor.gzip.handleResult(req, response)
                                  case Some(ctp) =>
                                    descriptor.gzip.handleResult(req, response.as(ctp))
                                }
                              }
                          } else {
                            resp.ignore()
                            Errors.craftResponseResult(
                              "You cannot use HTTP/1.0 here",
                              HttpVersionNotSupported,
                              req,
                              Some(descriptor),
                              Some("errors.http.10.not.allowed"),
                              duration = System.currentTimeMillis - start,
                              overhead = (System
                                .currentTimeMillis() - secondStart) + firstOverhead,
                              cbDuration = cbDuration,
                              callAttempts = callAttempts,
                              attrs = attrs
                            )
                          }
                        } else {
                          val response: Result = isChunked match {
                            case true  => {
                              // stream out
                              val res = Status(
                                attrs.get(otoroshi.plugins.Keys.StatusOverrideKey).getOrElse(httpResponse.status)
                              )
                                .chunked(finalStream)
                                .withHeaders(
                                  headersOut.filterNot { h =>
                                    val lower = h._1.toLowerCase()
                                    lower == "content-type" || lower == "set-cookie" || lower == "transfer-encoding"
                                  }: _*
                                )
                                .withCookies(
                                  (withTrackingCookies ++ jwtInjection.additionalCookies
                                    .map(t => Cookie(t._1, t._2)) ++ cookies): _*
                                )
                              contentType match {
                                case None      => res
                                case Some(ctp) => res.as(ctp)
                              }
                            }
                            case false => {
                              val contentLength: Option[Long] = httpResponse.headers
                                .get("Content-Length")
                                .orElse(httpResponse.headers.get("content-length"))
                                .orElse(resp.contentLengthStr)
                                .map(
                                  _.toLong + snowMonkeyContext.trailingResponseBodySize
                                )
                              val actualContentLength: Long   =
                                contentLength.getOrElse(0L)
                              if (actualContentLength == 0L) {
                                // here, Play did not run the body because it's empty, so triggering things manually
                                logger
                                  .debug(
                                    "Triggering promise as content length is 0"
                                  )
                                promise.trySuccess(
                                  ProxyDone(
                                    httpResponse.status,
                                    isChunked,
                                    upstreamLatency,
                                    headersOut = resp.headers.mapValues(_.head).toSeq.map(Header.apply),
                                    otoroshiHeadersOut = headersOut.map(Header.apply),
                                    otoroshiHeadersIn = headersIn.map(Header.apply)
                                  )
                                )
                              }
                              // stream out
                              val res                         = Status(
                                attrs.get(otoroshi.plugins.Keys.StatusOverrideKey).getOrElse(httpResponse.status)
                              )
                                .sendEntity(
                                  HttpEntity.Streamed(
                                    finalStream,
                                    contentLength,
                                    contentType
                                  )
                                )
                                .withHeaders(
                                  headersOut.filterNot { h =>
                                    val lower = h._1.toLowerCase()
                                    lower == "content-type" || lower == "set-cookie" || lower == "transfer-encoding"
                                  }: _*
                                )
                                .withCookies(
                                  (withTrackingCookies ++ jwtInjection.additionalCookies
                                    .map(t => Cookie(t._1, t._2)) ++ cookies): _*
                                )
                              contentType match {
                                case None      => res
                                case Some(ctp) => res.as(ctp)
                              }
                            }
                          }
                          descriptor.gzip.handleResult(req, response)
                        }
                      }
                    }
                }
              }
            }
        }
      }
      .map(Right.apply)
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy