diff --git a/otoroshi/app/auth/oauth.scala b/otoroshi/app/auth/oauth.scala index 98e9c1a45b..76e8ad938f 100644 --- a/otoroshi/app/auth/oauth.scala +++ b/otoroshi/app/auth/oauth.scala @@ -87,27 +87,27 @@ case class GenericOauth2ModuleConfig( def `type`: String = "oauth2" override def authModule(config: GlobalConfig): AuthModule = GenericOauth2Module(this) override def asJson = Json.obj( - "type" -> "oauth2", - "id" -> this.id, - "name" -> this.name, - "desc" -> this.desc, - "sessionMaxAge" -> this.sessionMaxAge, - "clientId" -> this.clientId, - "clientSecret" -> this.clientSecret, - "authorizeUrl" -> this.authorizeUrl, - "tokenUrl" -> this.tokenUrl, - "userInfoUrl" -> this.userInfoUrl, - "loginUrl" -> this.loginUrl, - "logoutUrl" -> this.logoutUrl, - "scope" -> this.scope, - "useJson" -> this.useJson, + "type" -> "oauth2", + "id" -> this.id, + "name" -> this.name, + "desc" -> this.desc, + "sessionMaxAge" -> this.sessionMaxAge, + "clientId" -> this.clientId, + "clientSecret" -> this.clientSecret, + "authorizeUrl" -> this.authorizeUrl, + "tokenUrl" -> this.tokenUrl, + "userInfoUrl" -> this.userInfoUrl, + "loginUrl" -> this.loginUrl, + "logoutUrl" -> this.logoutUrl, + "scope" -> this.scope, + "useJson" -> this.useJson, "readProfileFromToken" -> this.readProfileFromToken, - "accessTokenField" -> this.accessTokenField, - "jwtVerifier" -> jwtVerifier.map(_.asJson).getOrElse(JsNull).as[JsValue], - "nameField" -> this.nameField, - "emailField" -> this.emailField, - "otoroshiDataField" -> this.otoroshiDataField, - "callbackUrl" -> this.callbackUrl + "accessTokenField" -> this.accessTokenField, + "jwtVerifier" -> jwtVerifier.map(_.asJson).getOrElse(JsNull).as[JsValue], + "nameField" -> this.nameField, + "emailField" -> this.emailField, + "otoroshiDataField" -> this.otoroshiDataField, + "callbackUrl" -> this.callbackUrl ) def save()(implicit ec: ExecutionContext, env: Env): Future[Boolean] = env.datastores.authConfigsDataStore.set(this) override def cookieSuffix(desc: ServiceDescriptor) = s"global-oauth-$id" @@ -222,14 +222,17 @@ case class GenericOauth2Module(authConfig: OAuth2ModuleConfig) extends AuthModul ) )(writeableOf_urlEncodedSimpleForm) } - future1.flatMap { resp => + future1 + .flatMap { resp => val accessToken = (resp.json \ authConfig.accessTokenField).as[String] if (authConfig.readProfileFromToken && authConfig.jwtVerifier.isDefined) { val algoSettings = authConfig.jwtVerifier.get - val tokenHeader = Try(Json.parse(ApacheBase64.decodeBase64(accessToken.split("\\.")(0)))).getOrElse(Json.obj()) - val tokenBody = Try(Json.parse(ApacheBase64.decodeBase64(accessToken.split("\\.")(1)))).getOrElse(Json.obj()) - val kid = (tokenHeader \ "kid").asOpt[String] - val alg = (tokenHeader \ "alg").asOpt[String].getOrElse("RS256") + val tokenHeader = + Try(Json.parse(ApacheBase64.decodeBase64(accessToken.split("\\.")(0)))).getOrElse(Json.obj()) + val tokenBody = + Try(Json.parse(ApacheBase64.decodeBase64(accessToken.split("\\.")(1)))).getOrElse(Json.obj()) + val kid = (tokenHeader \ "kid").asOpt[String] + val alg = (tokenHeader \ "alg").asOpt[String].getOrElse("RS256") algoSettings.asAlgorithmF(InputMode(alg, kid)).flatMap { case Some(algo) => { Try(JWT.require(algo).acceptLeeway(10000).build().verify(accessToken)).map { _ => @@ -323,14 +326,17 @@ case class GenericOauth2Module(authConfig: OAuth2ModuleConfig) extends AuthModul ) )(writeableOf_urlEncodedSimpleForm) } - future1.flatMap { resp => + future1 + .flatMap { resp => val accessToken = (resp.json \ authConfig.accessTokenField).as[String] if (authConfig.readProfileFromToken && authConfig.jwtVerifier.isDefined) { val algoSettings = authConfig.jwtVerifier.get - val tokenHeader = Try(Json.parse(ApacheBase64.decodeBase64(accessToken.split("\\.")(0)))).getOrElse(Json.obj()) - val tokenBody = Try(Json.parse(ApacheBase64.decodeBase64(accessToken.split("\\.")(1)))).getOrElse(Json.obj()) - val kid = (tokenHeader \ "kid").asOpt[String] - val alg = (tokenHeader \ "alg").asOpt[String].getOrElse("RS256") + val tokenHeader = + Try(Json.parse(ApacheBase64.decodeBase64(accessToken.split("\\.")(0)))).getOrElse(Json.obj()) + val tokenBody = + Try(Json.parse(ApacheBase64.decodeBase64(accessToken.split("\\.")(1)))).getOrElse(Json.obj()) + val kid = (tokenHeader \ "kid").asOpt[String] + val alg = (tokenHeader \ "alg").asOpt[String].getOrElse("RS256") algoSettings.asAlgorithmF(InputMode(alg, kid)).flatMap { case Some(algo) => { Try(JWT.require(algo).acceptLeeway(10000).build().verify(accessToken)).map { _ => diff --git a/otoroshi/app/controllers/BackOfficeController.scala b/otoroshi/app/controllers/BackOfficeController.scala index e6dfa1d441..eee8bfc0bd 100644 --- a/otoroshi/app/controllers/BackOfficeController.scala +++ b/otoroshi/app/controllers/BackOfficeController.scala @@ -754,18 +754,22 @@ class BackOfficeController(BackOfficeAction: BackOfficeAction, } def fetchOpenIdConfiguration() = BackOfficeActionAuth.async(parse.json) { ctx => - import scala.concurrent.duration._ - val id = (ctx.request.body \ "id").asOpt[String].getOrElse(IdGenerator.token(64)) + val id = (ctx.request.body \ "id").asOpt[String].getOrElse(IdGenerator.token(64)) val name = (ctx.request.body \ "name").asOpt[String].getOrElse("new oauth config") val desc = (ctx.request.body \ "desc").asOpt[String].getOrElse("new oauth config") (ctx.request.body \ "url").asOpt[String] match { - case None => FastFuture.successful(Ok(GenericOauth2ModuleConfig( - id = id, - name = name, - desc = desc - ).asJson)) + case None => + FastFuture.successful( + Ok( + GenericOauth2ModuleConfig( + id = id, + name = name, + desc = desc + ).asJson + ) + ) case Some(url) => { env.Ws.url(url).withRequestTimeout(10.seconds).get().map { resp => if (resp.status == 200) { @@ -775,44 +779,57 @@ class BackOfficeController(BackOfficeAction: BackOfficeAction, name = name, desc = desc ) - val body = Json.parse(resp.body) - val issuer = (body \ "issuer").asOpt[String].getOrElse("http://localhost:8082/") - val tokenUrl = (body \ "token_endpoint").asOpt[String].getOrElse(config.tokenUrl) + val body = Json.parse(resp.body) + val issuer = (body \ "issuer").asOpt[String].getOrElse("http://localhost:8082/") + val tokenUrl = (body \ "token_endpoint").asOpt[String].getOrElse(config.tokenUrl) val authorizeUrl = (body \ "authorization_endpoint").asOpt[String].getOrElse(config.authorizeUrl) - val userInfoUrl = (body \ "userinfo_endpoint").asOpt[String].getOrElse(config.userInfoUrl) - val loginUrl = (body \ "authorization_endpoint").asOpt[String].getOrElse(authorizeUrl) - val logoutUrl = (body \ "end_session_endpoint").asOpt[String].getOrElse((issuer + "/logout").replace("//logout", "/logout")) + val userInfoUrl = (body \ "userinfo_endpoint").asOpt[String].getOrElse(config.userInfoUrl) + val loginUrl = (body \ "authorization_endpoint").asOpt[String].getOrElse(authorizeUrl) + val logoutUrl = (body \ "end_session_endpoint") + .asOpt[String] + .getOrElse((issuer + "/logout").replace("//logout", "/logout")) val jwksUri = (body \ "jwks_uri").asOpt[String] - Ok(config.copy( - tokenUrl = tokenUrl, - authorizeUrl = authorizeUrl, - userInfoUrl = userInfoUrl, - loginUrl = loginUrl, - logoutUrl = logoutUrl, - accessTokenField = jwksUri.map(_ => "id_token").getOrElse("access_token"), - useJson = true, - readProfileFromToken = jwksUri.isDefined, - jwtVerifier = jwksUri.map(url => JWKSAlgoSettings( - url = url, - headers = Map.empty[String, String], - timeout = FiniteDuration(2000, TimeUnit.MILLISECONDS), - ttl = FiniteDuration(60 * 60 * 1000, TimeUnit.MILLISECONDS), - kty = KeyType.RSA - )) - ).asJson) + Ok( + config + .copy( + tokenUrl = tokenUrl, + authorizeUrl = authorizeUrl, + userInfoUrl = userInfoUrl, + loginUrl = loginUrl, + logoutUrl = logoutUrl, + accessTokenField = jwksUri.map(_ => "id_token").getOrElse("access_token"), + useJson = true, + readProfileFromToken = jwksUri.isDefined, + jwtVerifier = jwksUri.map( + url => + JWKSAlgoSettings( + url = url, + headers = Map.empty[String, String], + timeout = FiniteDuration(2000, TimeUnit.MILLISECONDS), + ttl = FiniteDuration(60 * 60 * 1000, TimeUnit.MILLISECONDS), + kty = KeyType.RSA + ) + ) + ) + .asJson + ) } getOrElse { - Ok(GenericOauth2ModuleConfig( + Ok( + GenericOauth2ModuleConfig( + id = id, + name = name, + desc = desc + ).asJson + ) + } + } else { + Ok( + GenericOauth2ModuleConfig( id = id, name = name, desc = desc - ).asJson) - } - } else { - Ok(GenericOauth2ModuleConfig( - id = id, - name = name, - desc = desc - ).asJson) + ).asJson + ) } } } diff --git a/otoroshi/app/env/Env.scala b/otoroshi/app/env/Env.scala index 483dd1085a..d28f384b82 100644 --- a/otoroshi/app/env/Env.scala +++ b/otoroshi/app/env/Env.scala @@ -138,9 +138,11 @@ class Env(val configuration: Configuration, lazy val clusterAgent: ClusterAgent = ClusterAgent(clusterConfig, this) lazy val clusterLeaderAgent: ClusterLeaderAgent = ClusterLeaderAgent(clusterConfig, this) - lazy val globalMaintenanceMode: Boolean = configuration.getOptional[Boolean]("otoroshi.maintenanceMode").getOrElse(false) + lazy val globalMaintenanceMode: Boolean = + configuration.getOptional[Boolean]("otoroshi.maintenanceMode").getOrElse(false) - lazy val requestTimeout: FiniteDuration = configuration.getOptional[Int]("app.proxy.requestTimeout").map(_.millis).getOrElse(1.hour) + lazy val requestTimeout: FiniteDuration = + configuration.getOptional[Int]("app.proxy.requestTimeout").map(_.millis).getOrElse(1.hour) lazy val healthAccessKey: Option[String] = configuration.getOptional[String]("app.health.accessKey") lazy val overheadThreshold: Double = configuration.getOptional[Double]("app.overheadThreshold").getOrElse(500.0) lazy val healthLimit: Double = configuration.getOptional[Double]("app.health.limit").getOrElse(1000.0) diff --git a/otoroshi/app/gateway/handlers.scala b/otoroshi/app/gateway/handlers.scala index 1979e972a9..7f74b49bfd 100644 --- a/otoroshi/app/gateway/handlers.scala +++ b/otoroshi/app/gateway/handlers.scala @@ -188,17 +188,18 @@ class GatewayRequestHandler(snowMonkey: SnowMonkey, val toHttps = env.exposedRootSchemeIsHttps val host = if (request.host.contains(":")) request.host.split(":")(0) else request.host host match { - case str if matchRedirection(str) => Some(redirectToMainDomain()) - case _ if request.relativeUri.contains("__otoroshi_assets") => super.routeRequest(request) - case _ if request.relativeUri.startsWith("/__otoroshi_private_apps_login") => Some(setPrivateAppsCookies()) - case _ if request.relativeUri.startsWith("/__otoroshi_private_apps_logout") => Some(removePrivateAppsCookies()) - case _ if request.relativeUri.startsWith("/.well-known/otoroshi/login") => Some(setPrivateAppsCookies()) - case _ if request.relativeUri.startsWith("/.well-known/otoroshi/logout") => Some(removePrivateAppsCookies()) - case env.backOfficeHost if !isSecured && toHttps && env.isProd => Some(redirectToHttps()) - case env.privateAppsHost if !isSecured && toHttps && env.isProd => Some(redirectToHttps()) - case env.adminApiHost if env.exposeAdminApi => super.routeRequest(request) - case env.backOfficeHost if env.exposeAdminDashboard => super.routeRequest(request) - case env.privateAppsHost => super.routeRequest(request) + case str if matchRedirection(str) => Some(redirectToMainDomain()) + case _ if request.relativeUri.contains("__otoroshi_assets") => super.routeRequest(request) + case _ if request.relativeUri.startsWith("/__otoroshi_private_apps_login") => Some(setPrivateAppsCookies()) + case _ if request.relativeUri.startsWith("/__otoroshi_private_apps_logout") => + Some(removePrivateAppsCookies()) + case _ if request.relativeUri.startsWith("/.well-known/otoroshi/login") => Some(setPrivateAppsCookies()) + case _ if request.relativeUri.startsWith("/.well-known/otoroshi/logout") => Some(removePrivateAppsCookies()) + case env.backOfficeHost if !isSecured && toHttps && env.isProd => Some(redirectToHttps()) + case env.privateAppsHost if !isSecured && toHttps && env.isProd => Some(redirectToHttps()) + case env.adminApiHost if env.exposeAdminApi => super.routeRequest(request) + case env.backOfficeHost if env.exposeAdminDashboard => super.routeRequest(request) + case env.privateAppsHost => super.routeRequest(request) case _ => request.headers.get("Sec-WebSocket-Version") match { case None => Some(forwardCall()) @@ -209,13 +210,17 @@ class GatewayRequestHandler(snowMonkey: SnowMonkey, } } - def xForwardedHeader(desc: ServiceDescriptor, request: RequestHeader): Seq[(String, String)] = { if (desc.xForwardedHeaders) { - val xForwardedFor = request.headers.get("X-Forwarded-For").map(v => v + ", " + request.remoteAddress).getOrElse(request.remoteAddress) + val xForwardedFor = request.headers + .get("X-Forwarded-For") + .map(v => v + ", " + request.remoteAddress) + .getOrElse(request.remoteAddress) val xForwardedProto = getProtocolFor(request) - val xForwardedHost = request.headers.get("X-Forwarded-Host").getOrElse(request.host) - Seq("X-Forwarded-For" -> xForwardedFor, "X-Forwarded-Host" -> xForwardedHost, "X-Forwarded-Proto" -> xForwardedProto) + val xForwardedHost = request.headers.get("X-Forwarded-Host").getOrElse(request.host) + Seq("X-Forwarded-For" -> xForwardedFor, + "X-Forwarded-Host" -> xForwardedHost, + "X-Forwarded-Proto" -> xForwardedProto) } else { Seq.empty[(String, String)] } @@ -639,250 +644,251 @@ class GatewayRequestHandler(snowMonkey: SnowMonkey, Some("errors.service.in.maintenance") ) } else { - passWithReadOnly(rawDesc.readOnly, req) { - applyJwtVerifier(rawDesc, req) { jwtInjection => - applySidecar(rawDesc, remoteAddress, req) { desc => - val firstOverhead = System.currentTimeMillis() - start - snowMonkey.introduceChaos(reqNumber, globalConfig, desc, hasBody(req)) { snowMonkeyContext => - val secondStart = System.currentTimeMillis() - val maybeTrackingId = req.cookies - .get("otoroshi-canary") - .map(_.value) - .orElse(req.headers.get(env.Headers.OtoroshiTrackerId)) - .filter { value => - if (value.contains("::")) { - value.split("::").toList match { - case signed :: id :: Nil if env.sign(id) == signed => true - case _ => false + passWithReadOnly(rawDesc.readOnly, req) { + applyJwtVerifier(rawDesc, req) { jwtInjection => + applySidecar(rawDesc, remoteAddress, req) { desc => + val firstOverhead = System.currentTimeMillis() - start + snowMonkey.introduceChaos(reqNumber, globalConfig, desc, hasBody(req)) { snowMonkeyContext => + val secondStart = System.currentTimeMillis() + val maybeTrackingId = req.cookies + .get("otoroshi-canary") + .map(_.value) + .orElse(req.headers.get(env.Headers.OtoroshiTrackerId)) + .filter { value => + if (value.contains("::")) { + value.split("::").toList match { + case signed :: id :: Nil if env.sign(id) == signed => true + case _ => false + } + } else { + false } - } else { - false - } - } map (value => value.split("::")(1)) - val trackingId: String = maybeTrackingId.getOrElse(IdGenerator.uuid + "-" + reqNumber) + } map (value => value.split("::")(1)) + val trackingId: String = maybeTrackingId.getOrElse(IdGenerator.uuid + "-" + reqNumber) - if (maybeTrackingId.isDefined) { - logger.debug(s"request already has tracking id : $trackingId") - } else { - logger.debug(s"request has a new tracking id : $trackingId") - } + if (maybeTrackingId.isDefined) { + logger.debug(s"request already has tracking id : $trackingId") + } else { + logger.debug(s"request has a new tracking id : $trackingId") + } - val withTrackingCookies: Seq[Cookie] = - if (!desc.canary.enabled) - jwtInjection.additionalCookies - .map(t => Cookie(t._1, t._2)) - .toSeq //Seq.empty[play.api.mvc.Cookie] - else if (maybeTrackingId.isDefined) - jwtInjection.additionalCookies - .map(t => Cookie(t._1, t._2)) - .toSeq //Seq.empty[play.api.mvc.Cookie] - else - Seq( - play.api.mvc.Cookie( - name = "otoroshi-canary", - value = s"${env.sign(trackingId)}::$trackingId", - maxAge = Some(2592000), - path = "/", - domain = Some(req.host), - httpOnly = false - ) - ) ++ jwtInjection.additionalCookies.map(t => Cookie(t._1, t._2)) - - //desc.isUp.flatMap(iu => splitToCanary(desc, trackingId).fast.map(d => (iu, d))).fast.flatMap { - splitToCanary(desc, trackingId, reqNumber, globalConfig).fast.flatMap { _desc => - val isUp = true - - val descriptor = if (env.redirectToDev) _desc.copy(env = "dev") else _desc - - def callDownstream(config: GlobalConfig, - apiKey: Option[ApiKey] = None, - paUsr: Option[PrivateAppsUser] = None): Future[Result] = { - desc.validateClientCertificates(req, apiKey, paUsr) { - passWithReadOnly(apiKey.map(_.readOnly).getOrElse(false), req) { - if (config.useCircuitBreakers && descriptor.clientConfig.useCircuitBreaker) { - val cbStart = System.currentTimeMillis() - val counter = new AtomicInteger(0) - env.circuitBeakersHolder - .get(desc.id, () => new ServiceDescriptorCircuitBreaker()) - .call( - descriptor, - bodyAlreadyConsumed, - s"${req.method} ${req.relativeUri}", - counter, - (t, attempts) => - actuallyCallDownstream(t, - apiKey, - paUsr, - System.currentTimeMillis - cbStart, - counter.get()) - ) recoverWith { - case BodyAlreadyConsumedException => - Errors.craftResponseResult( - s"Something went wrong, the downstream service does not respond quickly enough but consumed all the request body, you should try later. Thanks for your understanding", - BadGateway, - req, - Some(descriptor), - Some("errors.request.timeout"), - duration = System.currentTimeMillis - start, - overhead = (System.currentTimeMillis() - secondStart) + firstOverhead, - cbDuration = System.currentTimeMillis - cbStart, - callAttempts = counter.get() - ) - case RequestTimeoutException => - Errors.craftResponseResult( - s"Something went wrong, the downstream service does not respond quickly enough, you should try later. Thanks for your understanding", - BadGateway, - req, - Some(descriptor), - Some("errors.request.timeout"), - duration = System.currentTimeMillis - start, - overhead = (System.currentTimeMillis() - secondStart) + firstOverhead, - cbDuration = System.currentTimeMillis - cbStart, - callAttempts = counter.get() - ) - case _: scala.concurrent.TimeoutException => - Errors.craftResponseResult( - s"Something went wrong, the downstream service does not respond quickly enough, you should try later. Thanks for your understanding", - BadGateway, - req, - Some(descriptor), - Some("errors.request.timeout"), - duration = System.currentTimeMillis - start, - overhead = (System.currentTimeMillis() - secondStart) + firstOverhead, - cbDuration = System.currentTimeMillis - cbStart, - callAttempts = counter.get() - ) - case AllCircuitBreakersOpenException => - Errors.craftResponseResult( - s"Something went wrong, the downstream service seems a little bit overwhelmed, you should try later. Thanks for your understanding", - BadGateway, - req, - Some(descriptor), - Some("errors.circuit.breaker.open"), - duration = System.currentTimeMillis - start, - overhead = (System.currentTimeMillis() - secondStart) + firstOverhead, - cbDuration = System.currentTimeMillis - cbStart, - callAttempts = counter.get() - ) - case error - if error != null && error.getMessage != null && error.getMessage - .toLowerCase() - .contains("connection refused") => - Errors.craftResponseResult( - s"Something went wrong, the connection to downstream service was refused, you should try later. Thanks for your understanding", - BadGateway, - req, - Some(descriptor), - Some("errors.connection.refused"), - duration = System.currentTimeMillis - start, - overhead = (System.currentTimeMillis() - secondStart) + firstOverhead, - cbDuration = System.currentTimeMillis - cbStart, - callAttempts = counter.get() - ) - 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.", - BadGateway, - req, - Some(descriptor), - Some("errors.proxy.error"), - duration = System.currentTimeMillis - start, - overhead = (System.currentTimeMillis() - secondStart) + firstOverhead, - cbDuration = System.currentTimeMillis - cbStart, - callAttempts = counter.get() - ) - 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", - BadGateway, - req, - Some(descriptor), - Some("errors.proxy.error"), - duration = System.currentTimeMillis - start, - overhead = (System.currentTimeMillis() - secondStart) + firstOverhead, - cbDuration = System.currentTimeMillis - cbStart, - callAttempts = counter.get() - ) + val withTrackingCookies: Seq[Cookie] = + if (!desc.canary.enabled) + jwtInjection.additionalCookies + .map(t => Cookie(t._1, t._2)) + .toSeq //Seq.empty[play.api.mvc.Cookie] + else if (maybeTrackingId.isDefined) + jwtInjection.additionalCookies + .map(t => Cookie(t._1, t._2)) + .toSeq //Seq.empty[play.api.mvc.Cookie] + else + Seq( + play.api.mvc.Cookie( + name = "otoroshi-canary", + value = s"${env.sign(trackingId)}::$trackingId", + maxAge = Some(2592000), + path = "/", + domain = Some(req.host), + httpOnly = false + ) + ) ++ jwtInjection.additionalCookies.map(t => Cookie(t._1, t._2)) + + //desc.isUp.flatMap(iu => splitToCanary(desc, trackingId).fast.map(d => (iu, d))).fast.flatMap { + splitToCanary(desc, trackingId, reqNumber, globalConfig).fast.flatMap { _desc => + val isUp = true + + val descriptor = if (env.redirectToDev) _desc.copy(env = "dev") else _desc + + def callDownstream(config: GlobalConfig, + apiKey: Option[ApiKey] = None, + paUsr: Option[PrivateAppsUser] = None): Future[Result] = { + desc.validateClientCertificates(req, apiKey, paUsr) { + passWithReadOnly(apiKey.map(_.readOnly).getOrElse(false), req) { + if (config.useCircuitBreakers && descriptor.clientConfig.useCircuitBreaker) { + val cbStart = System.currentTimeMillis() + val counter = new AtomicInteger(0) + env.circuitBeakersHolder + .get(desc.id, () => new ServiceDescriptorCircuitBreaker()) + .call( + descriptor, + bodyAlreadyConsumed, + s"${req.method} ${req.relativeUri}", + counter, + (t, attempts) => + actuallyCallDownstream(t, + apiKey, + paUsr, + System.currentTimeMillis - cbStart, + counter.get()) + ) recoverWith { + case BodyAlreadyConsumedException => + Errors.craftResponseResult( + s"Something went wrong, the downstream service does not respond quickly enough but consumed all the request body, you should try later. Thanks for your understanding", + BadGateway, + req, + Some(descriptor), + Some("errors.request.timeout"), + duration = System.currentTimeMillis - start, + overhead = (System.currentTimeMillis() - secondStart) + firstOverhead, + cbDuration = System.currentTimeMillis - cbStart, + callAttempts = counter.get() + ) + case RequestTimeoutException => + Errors.craftResponseResult( + s"Something went wrong, the downstream service does not respond quickly enough, you should try later. Thanks for your understanding", + BadGateway, + req, + Some(descriptor), + Some("errors.request.timeout"), + duration = System.currentTimeMillis - start, + overhead = (System.currentTimeMillis() - secondStart) + firstOverhead, + cbDuration = System.currentTimeMillis - cbStart, + callAttempts = counter.get() + ) + case _: scala.concurrent.TimeoutException => + Errors.craftResponseResult( + s"Something went wrong, the downstream service does not respond quickly enough, you should try later. Thanks for your understanding", + BadGateway, + req, + Some(descriptor), + Some("errors.request.timeout"), + duration = System.currentTimeMillis - start, + overhead = (System.currentTimeMillis() - secondStart) + firstOverhead, + cbDuration = System.currentTimeMillis - cbStart, + callAttempts = counter.get() + ) + case AllCircuitBreakersOpenException => + Errors.craftResponseResult( + s"Something went wrong, the downstream service seems a little bit overwhelmed, you should try later. Thanks for your understanding", + BadGateway, + req, + Some(descriptor), + Some("errors.circuit.breaker.open"), + duration = System.currentTimeMillis - start, + overhead = (System.currentTimeMillis() - secondStart) + firstOverhead, + cbDuration = System.currentTimeMillis - cbStart, + callAttempts = counter.get() + ) + case error + if error != null && error.getMessage != null && error.getMessage + .toLowerCase() + .contains("connection refused") => + Errors.craftResponseResult( + s"Something went wrong, the connection to downstream service was refused, you should try later. Thanks for your understanding", + BadGateway, + req, + Some(descriptor), + Some("errors.connection.refused"), + duration = System.currentTimeMillis - start, + overhead = (System.currentTimeMillis() - secondStart) + firstOverhead, + cbDuration = System.currentTimeMillis - cbStart, + callAttempts = counter.get() + ) + 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.", + BadGateway, + req, + Some(descriptor), + Some("errors.proxy.error"), + duration = System.currentTimeMillis - start, + overhead = (System.currentTimeMillis() - secondStart) + firstOverhead, + cbDuration = System.currentTimeMillis - cbStart, + callAttempts = counter.get() + ) + 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", + BadGateway, + req, + Some(descriptor), + Some("errors.proxy.error"), + duration = System.currentTimeMillis - start, + overhead = (System.currentTimeMillis() - secondStart) + firstOverhead, + cbDuration = System.currentTimeMillis - cbStart, + callAttempts = counter.get() + ) + } + } else { + val index = reqCounter.get() % (if (descriptor.targets.nonEmpty) + descriptor.targets.size + else 1) + // Round robin loadbalancing is happening here !!!!! + val target = descriptor.targets.apply(index.toInt) + actuallyCallDownstream(target, apiKey, paUsr, 0L, 1) } - } else { - val index = reqCounter.get() % (if (descriptor.targets.nonEmpty) - descriptor.targets.size - else 1) - // Round robin loadbalancing is happening here !!!!! - val target = descriptor.targets.apply(index.toInt) - actuallyCallDownstream(target, apiKey, paUsr, 0L, 1) } } } - } - def actuallyCallDownstream(target: Target, - apiKey: Option[ApiKey] = None, - paUsr: Option[PrivateAppsUser] = None, - cbDuration: Long, - callAttempts: Int): Future[Result] = { - val snowflake = env.snowflakeGenerator.nextIdStr() - val requestTimestamp = DateTime.now().toString("yyyy-MM-dd'T'HH:mm:ss.SSSZZ") - val state = IdGenerator.extendedToken(128) - val rawUri = req.relativeUri.substring(1) - val uriParts = rawUri.split("/").toSeq - val uri: String = - descriptor.matchingRoot.map(m => req.relativeUri.replace(m, "")).getOrElse(rawUri) - val scheme = if (descriptor.redirectToLocal) descriptor.localScheme else target.scheme - val host = if (descriptor.redirectToLocal) descriptor.localHost else target.host - val root = descriptor.root - val url = s"$scheme://$host$root$uri" - lazy val currentReqHasBody = hasBody(req) - // 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 = OtoroshiClaim( - iss = env.Headers.OtoroshiIssuer, - sub = paUsr - .filter(_ => descriptor.privateApp) - .map(k => s"pa:${k.email}") - .orElse(apiKey.map(k => s"apikey:${k.clientId}")) - .getOrElse("--"), - aud = descriptor.name, - exp = DateTime.now().plusSeconds(30).toDate.getTime, - iat = DateTime.now().toDate.getTime, - jti = IdGenerator.uuid - ).withClaim("email", paUsr.map(_.email)) - .withClaim("name", paUsr.map(_.name).orElse(apiKey.map(_.clientName))) - .withClaim("picture", paUsr.flatMap(_.picture)) - .withClaim("user_id", paUsr.flatMap(_.userId).orElse(apiKey.map(_.clientId))) - .withClaim("given_name", paUsr.flatMap(_.field("given_name"))) - .withClaim("family_name", paUsr.flatMap(_.field("family_name"))) - .withClaim("gender", paUsr.flatMap(_.field("gender"))) - .withClaim("locale", paUsr.flatMap(_.field("locale"))) - .withClaim("nickname", paUsr.flatMap(_.field("nickname"))) - .withClaims(paUsr.flatMap(_.otoroshiData).orElse(apiKey.map(_.metadata))) - .withClaim("metadata", - paUsr - .flatMap(_.otoroshiData) - .orElse(apiKey.map(_.metadata)) - .map(m => Json.stringify(Json.toJson(m)))) - .withClaim("user", paUsr.map(u => Json.stringify(u.toJson))) - .withClaim("apikey", - apiKey.map( - ak => - Json.stringify( - Json.obj( - "clientId" -> ak.clientId, - "clientName" -> ak.clientName, - "metadata" -> ak.metadata - ) - ) - )) - .serialize(desc.secComSettings)(env) - logger.trace(s"Claim is : $claim") - val headersIn: Seq[(String, String)] = - (req.headers.toMap.toSeq.flatMap(c => c._2.map(v => (c._1, v))) //.map(tuple => (tuple._1, tuple._2.mkString(","))) //.toSimpleMap + def actuallyCallDownstream(target: Target, + apiKey: Option[ApiKey] = None, + paUsr: Option[PrivateAppsUser] = None, + cbDuration: Long, + callAttempts: Int): Future[Result] = { + val snowflake = env.snowflakeGenerator.nextIdStr() + val requestTimestamp = DateTime.now().toString("yyyy-MM-dd'T'HH:mm:ss.SSSZZ") + val state = IdGenerator.extendedToken(128) + val rawUri = req.relativeUri.substring(1) + val uriParts = rawUri.split("/").toSeq + val uri: String = + descriptor.matchingRoot.map(m => req.relativeUri.replace(m, "")).getOrElse(rawUri) + val scheme = if (descriptor.redirectToLocal) descriptor.localScheme else target.scheme + val host = if (descriptor.redirectToLocal) descriptor.localHost else target.host + val root = descriptor.root + val url = s"$scheme://$host$root$uri" + lazy val currentReqHasBody = hasBody(req) + // 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 = OtoroshiClaim( + iss = env.Headers.OtoroshiIssuer, + sub = paUsr + .filter(_ => descriptor.privateApp) + .map(k => s"pa:${k.email}") + .orElse(apiKey.map(k => s"apikey:${k.clientId}")) + .getOrElse("--"), + aud = descriptor.name, + exp = DateTime.now().plusSeconds(30).toDate.getTime, + iat = DateTime.now().toDate.getTime, + jti = IdGenerator.uuid + ).withClaim("email", paUsr.map(_.email)) + .withClaim("name", paUsr.map(_.name).orElse(apiKey.map(_.clientName))) + .withClaim("picture", paUsr.flatMap(_.picture)) + .withClaim("user_id", paUsr.flatMap(_.userId).orElse(apiKey.map(_.clientId))) + .withClaim("given_name", paUsr.flatMap(_.field("given_name"))) + .withClaim("family_name", paUsr.flatMap(_.field("family_name"))) + .withClaim("gender", paUsr.flatMap(_.field("gender"))) + .withClaim("locale", paUsr.flatMap(_.field("locale"))) + .withClaim("nickname", paUsr.flatMap(_.field("nickname"))) + .withClaims(paUsr.flatMap(_.otoroshiData).orElse(apiKey.map(_.metadata))) + .withClaim("metadata", + paUsr + .flatMap(_.otoroshiData) + .orElse(apiKey.map(_.metadata)) + .map(m => Json.stringify(Json.toJson(m)))) + .withClaim("user", paUsr.map(u => Json.stringify(u.toJson))) + .withClaim("apikey", + apiKey.map( + ak => + Json.stringify( + Json.obj( + "clientId" -> ak.clientId, + "clientName" -> ak.clientName, + "metadata" -> ak.metadata + ) + ) + )) + .serialize(desc.secComSettings)(env) + logger.trace(s"Claim is : $claim") + val headersIn: Seq[(String, String)] = + (req.headers.toMap.toSeq + .flatMap(c => c._2.map(v => (c._1, v))) //.map(tuple => (tuple._1, tuple._2.mkString(","))) //.toSimpleMap .filterNot( t => if (t._1.toLowerCase == "content-type" && !currentReqHasBody) true @@ -890,7 +896,7 @@ class GatewayRequestHandler(snowMonkey: SnowMonkey, else false ) .filterNot(t => headersInFiltered.contains(t._1.toLowerCase)) ++ Map( - env.Headers.OtoroshiProxiedHost -> req.headers.get("Host").getOrElse("--"), + env.Headers.OtoroshiProxiedHost -> req.headers.get("Host").getOrElse("--"), //"Host" -> host, "Host" -> (if (desc.overrideHost) host else req.headers.get("Host").getOrElse("--")), env.Headers.OtoroshiRequestId -> snowflake, @@ -918,864 +924,870 @@ class GatewayRequestHandler(snowMonkey: SnowMonkey, descriptor.additionalHeaders.filter(t => t._1.trim.nonEmpty) ++ fromOtoroshi .map(v => Map(env.Headers.OtoroshiGatewayParentRequest -> fromOtoroshi.get)) .getOrElse(Map.empty[String, String]) ++ jwtInjection.additionalHeaders).toSeq - .filterNot(t => jwtInjection.removeHeaders.contains(t._1)) ++ xForwardedHeader(desc, req) - - val lazySource = Source.single(ByteString.empty).flatMapConcat { _ => - bodyAlreadyConsumed.compareAndSet(false, true) - req.body - .concat(snowMonkeyContext.trailingRequestBodyStream) - .map(bs => { - // meterIn.mark(bs.length) - counterIn.addAndGet(bs.length) - bs - }) - } - // val requestHeader = ByteString( - // req.method + " " + req.relativeUri + " HTTP/1.1\n" + headersIn - // .map(h => s"${h._1}: ${h._2}") - // .mkString("\n") + "\n" - // ) - // meterIn.mark(requestHeader.length) - // counterIn.addAndGet(requestHeader.length) - // logger.trace(s"curl -X ${req.method.toUpperCase()} ${headersIn.map(h => s"-H '${h._1}: ${h._2}'").mkString(" ")} '$url?${queryString.map(h => s"${h._1}=${h._2}").mkString("&")}' --include") - debugLogger.trace( - s"curl -X ${req.method - .toUpperCase()} ${headersIn.map(h => s"-H '${h._1}: ${h._2}'").mkString(" ")} '$url' --include" - ) - val overhead = (System.currentTimeMillis() - secondStart) + firstOverhead - if (overhead > env.overheadThreshold) { - HighOverheadAlert( - `@id` = env.snowflakeGenerator.nextIdStr(), - limitOverhead = env.overheadThreshold, - currentOverhead = overhead, - serviceDescriptor = descriptor, - target = Location( - scheme = getProtocolFor(req), - host = req.host, - 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) - - 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}" - ) - GatewayEvent( - `@id` = env.snowflakeGenerator.nextIdStr(), - reqId = snowflake, - parentReqId = fromOtoroshi, - `@timestamp` = DateTime.now(), - protocol = req.version, - to = Location( - scheme = getProtocolFor(req), - host = req.host, - uri = req.relativeUri - ), - target = Location( - scheme = scheme, - host = host, - uri = req.relativeUri - ), - duration = duration, - overhead = overhead, - cbDuration = cbDuration, - overheadWoCb = overhead - cbDuration, - callAttempts = callAttempts, - url = url, - method = req.method, - from = from, - 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, - identity = apiKey - .map( - k => - Identity( - identityType = "APIKEY", - identity = k.clientId, - label = k.clientName - ) - ) - .orElse( - paUsr.map( - k => - Identity( - identityType = "PRIVATEAPP", - identity = k.email, - label = k.name - ) - ) - ), - `@serviceId` = descriptor.id, - `@service` = descriptor.name, - descriptor = descriptor, - `@product` = descriptor.metadata.getOrElse("product", "--"), - remainingQuotas = q, - viz = Some(viz) - ).toAnalytics() - } - }(env.otoroshiExecutionContext) // pressure EC + .filterNot(t => jwtInjection.removeHeaders.contains(t._1)) ++ xForwardedHeader(desc, + req) + + val lazySource = Source.single(ByteString.empty).flatMapConcat { _ => + bodyAlreadyConsumed.compareAndSet(false, true) + req.body + .concat(snowMonkeyContext.trailingRequestBodyStream) + .map(bs => { + // meterIn.mark(bs.length) + counterIn.addAndGet(bs.length) + bs + }) } - }(env.otoroshiExecutionContext) // pressure EC - //.andThen { - // case _ => env.datastores.requestsDataStore.decrementProcessedRequests() - //} - val wsCookiesIn = req.cookies.toSeq.map(c => DefaultWSCookie( - name = c.name, - value = c.value, - domain = c.domain, - path = Option(c.path), - maxAge = c.maxAge.map(_.toLong), - secure = c.secure, - httpOnly = c.httpOnly - )) - val rawRequest = otoroshi.script.HttpRequest( - url = s"${req.theProtocol}://${req.host}${req.relativeUri}", - method = req.method, - headers = req.headers.toSimpleMap, - cookies = wsCookiesIn - ) - val otoroshiRequest = otoroshi.script.HttpRequest( - url = url, - method = req.method, - headers = headersIn.toMap, - cookies = wsCookiesIn - ) - val upstreamStart = System.currentTimeMillis() - descriptor - .transformRequest( - snowflake = snowflake, - rawRequest = rawRequest, - otoroshiRequest = otoroshiRequest, - desc = descriptor, - apiKey = apiKey, - user = paUsr + // val requestHeader = ByteString( + // req.method + " " + req.relativeUri + " HTTP/1.1\n" + headersIn + // .map(h => s"${h._1}: ${h._2}") + // .mkString("\n") + "\n" + // ) + // meterIn.mark(requestHeader.length) + // counterIn.addAndGet(requestHeader.length) + // logger.trace(s"curl -X ${req.method.toUpperCase()} ${headersIn.map(h => s"-H '${h._1}: ${h._2}'").mkString(" ")} '$url?${queryString.map(h => s"${h._1}=${h._2}").mkString("&")}' --include") + debugLogger.trace( + s"curl -X ${req.method + .toUpperCase()} ${headersIn.map(h => s"-H '${h._1}: ${h._2}'").mkString(" ")} '$url' --include" ) - .flatMap { - case Left(badResult) => FastFuture.successful(badResult) - case Right(httpRequest) => { - val body = - if (currentReqHasBody) - SourceBody( - descriptor.transformRequestBody( - body = lazySource, - snowflake = snowflake, - rawRequest = rawRequest, - otoroshiRequest = otoroshiRequest, - desc = descriptor, - apiKey = apiKey, - user = paUsr - ) + val overhead = (System.currentTimeMillis() - secondStart) + firstOverhead + if (overhead > env.overheadThreshold) { + HighOverheadAlert( + `@id` = env.snowflakeGenerator.nextIdStr(), + limitOverhead = env.overheadThreshold, + currentOverhead = overhead, + serviceDescriptor = descriptor, + target = Location( + scheme = getProtocolFor(req), + host = req.host, + 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) + + 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}" ) - else EmptyBody // Stream IN - env.gatewayClient - .urlWithProtocol(target.scheme, UrlSanitizer.sanitize(httpRequest.url)) - .withRequestTimeout(env.requestTimeout) // we should monitor leaks - .withMethod(httpRequest.method) - .withHttpHeaders(httpRequest.headers.toSeq.filterNot(_._1 == "Cookie"): _*) - .withCookies(wsCookiesIn: _*) - .withBody(body) - .withFollowRedirects(false) - .stream() - // env.gatewayClient - // .urlWithProtocol(target.scheme, url) - // //.withRequestTimeout(descriptor.clientConfig.callTimeout.millis) - // .withRequestTimeout(6.hour) // we should monitor leaks - // .withMethod(req.method) - // // .withQueryString(queryString: _*) - // .withHttpHeaders(headersIn: _*) - // .withBody(body) - // .withFollowRedirects(false) - // .stream() - .flatMap(resp => quotas.fast.map(q => (resp, q))) - .flatMap { tuple => - 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( + GatewayEvent( + `@id` = env.snowflakeGenerator.nextIdStr(), + reqId = snowflake, + parentReqId = fromOtoroshi, + `@timestamp` = DateTime.now(), + protocol = req.version, + to = Location( + scheme = getProtocolFor(req), + host = req.host, + uri = req.relativeUri + ), + target = Location( + scheme = scheme, + host = host, + uri = req.relativeUri + ), + duration = duration, + overhead = overhead, + cbDuration = cbDuration, + overheadWoCb = overhead - cbDuration, + callAttempts = callAttempts, + url = url, + method = req.method, + from = from, + env = descriptor.env, + data = DataInOut( + dataIn = counterIn.get(), + dataOut = counterOut.get() + ), status = resp.status, - headers = headers.toMap, - cookies = resp.cookies - ) - // logger.trace(s"Connection: ${resp.headers.headers.get("Connection").map(_.last)}") - // if (env.notDev && !headers.get(env.Headers.OtoroshiStateResp).contains(state)) { - // val validState = headers.get(env.Headers.OtoroshiStateResp).filter(c => env.crypto.verifyString(state, c)).orElse(headers.get(env.Headers.OtoroshiStateResp).contains(state)).getOrElse(false) - if (env.notDev && (descriptor.enforceSecureCommunication && descriptor.sendStateChallenge) - && !descriptor.isUriExcludedFromSecuredCommunication("/" + uri) - && !headers.get(env.Headers.OtoroshiStateResp).contains(state)) { - 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 - ) - } else if (isUp) { - // val body = Await.result(resp.body.runFold(ByteString.empty)((a, b) => a.concat(b)).map(_.utf8String), Duration("10s")) - val exchange = Json.prettyPrint( - Json.obj( - "uri" -> req.relativeUri, - "url" -> url, - "state" -> state, - "reveivedState" -> JsString( - headers.getOrElse(env.Headers.OtoroshiStateResp, "--") - ), - "claim" -> claim, - "method" -> req.method, - "query" -> req.rawQueryString, - "status" -> resp.status, - "headersIn" -> JsArray( - req.headers.toSimpleMap - .map(t => Json.obj("name" -> t._1, "value" -> t._2)) - .toSeq - ), - "headersOut" -> JsArray( - headers.map(t => Json.obj("name" -> t._1, "values" -> t._2)).toSeq - ) - ) - ) - logger - .error( - s"\n\nError while talking with downstream service :(\n\n$exchange\n\n" + headers = req.headers.toSimpleMap.toSeq.map(Header.apply), + headersOut = resp.headersOut, + identity = apiKey + .map( + k => + Identity( + identityType = "APIKEY", + identity = k.clientId, + label = k.clientName ) - Errors.craftResponseResult( - "Downstream microservice 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 - ) - } 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 ) - } - } else { - val upstreamLatency = System.currentTimeMillis() - upstreamStart - val _headersOut: Seq[(String, String)] = _headersForOut - .filterNot(t => headersOutFiltered.contains(t._1.toLowerCase)) ++ ( - if (descriptor.sendOtoroshiHeadersBack) { - Seq( - env.Headers.OtoroshiRequestId -> snowflake, - env.Headers.OtoroshiRequestTimestamp -> requestTimestamp, - env.Headers.OtoroshiProxyLatency -> s"$overhead", - env.Headers.OtoroshiUpstreamLatency -> s"$upstreamLatency" //, - //env.Headers.OtoroshiTrackerId -> s"${env.sign(trackingId)}::$trackingId" + .orElse( + paUsr.map( + k => + Identity( + identityType = "PRIVATEAPP", + identity = k.email, + label = k.name + ) ) - } else { - Seq.empty[(String, String)] - } - ) ++ Some(trackingId) - .filter(_ => desc.canary.enabled) - .map( - _ => - env.Headers.OtoroshiTrackerId -> s"${env.sign(trackingId)}::$trackingId" - ) ++ (if (descriptor.sendOtoroshiHeadersBack && apiKey.isDefined) { - Seq( - env.Headers.OtoroshiDailyCallsRemaining -> remainingQuotas.remainingCallsPerDay.toString, - env.Headers.OtoroshiMonthlyCallsRemaining -> remainingQuotas.remainingCallsPerMonth.toString - ) - } else { - Seq.empty[(String, String)] - }) ++ descriptor.cors - .asHeaders(req) ++ desc.additionalHeadersOut.toSeq - - val otoroshiResponse = otoroshi.script.HttpResponse( - status = resp.status, - headers = _headersOut.toMap, - cookies = resp.cookies - ) - descriptor - .transformResponse( + ), + `@serviceId` = descriptor.id, + `@service` = descriptor.name, + descriptor = descriptor, + `@product` = descriptor.metadata.getOrElse("product", "--"), + remainingQuotas = q, + viz = Some(viz) + ).toAnalytics() + } + }(env.otoroshiExecutionContext) // pressure EC + } + }(env.otoroshiExecutionContext) // pressure EC + //.andThen { + // case _ => env.datastores.requestsDataStore.decrementProcessedRequests() + //} + val wsCookiesIn = req.cookies.toSeq.map( + c => + DefaultWSCookie( + name = c.name, + value = c.value, + domain = c.domain, + path = Option(c.path), + maxAge = c.maxAge.map(_.toLong), + secure = c.secure, + httpOnly = c.httpOnly + ) + ) + val rawRequest = otoroshi.script.HttpRequest( + url = s"${req.theProtocol}://${req.host}${req.relativeUri}", + method = req.method, + headers = req.headers.toSimpleMap, + cookies = wsCookiesIn + ) + val otoroshiRequest = otoroshi.script.HttpRequest( + url = url, + method = req.method, + headers = headersIn.toMap, + cookies = wsCookiesIn + ) + val upstreamStart = System.currentTimeMillis() + descriptor + .transformRequest( + snowflake = snowflake, + rawRequest = rawRequest, + otoroshiRequest = otoroshiRequest, + desc = descriptor, + apiKey = apiKey, + user = paUsr + ) + .flatMap { + case Left(badResult) => FastFuture.successful(badResult) + case Right(httpRequest) => { + val body = + if (currentReqHasBody) + SourceBody( + descriptor.transformRequestBody( + body = lazySource, snowflake = snowflake, - rawResponse = rawResponse, - otoroshiResponse = otoroshiResponse, + rawRequest = rawRequest, + otoroshiRequest = otoroshiRequest, desc = descriptor, apiKey = apiKey, user = paUsr ) - .flatMap { - case Left(badResult) => FastFuture.successful(badResult) - case Right(httpResponse) => { - val headersOut = httpResponse.headers.toSeq - val contentType = - httpResponse.headers.getOrElse("Content-Type", MimeTypes.TEXT) - // val _contentTypeOpt = resp.headers.get("Content-Type").flatMap(_.lastOption) - // meterOut.mark(responseHeader.length) - // counterOut.addAndGet(responseHeader.length) - - 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, - upstreamLatency, - headersOut.map(Header.apply)) - ) - case Failure(e) => - logger.error( - s"error while transfering stream for ${protocol}://${req.host}${req.relativeUri}", - e - ) - promise.trySuccess( - ProxyDone(httpResponse.status, - upstreamLatency, - headersOut.map(Header.apply)) + ) + else EmptyBody // Stream IN + env.gatewayClient + .urlWithProtocol(target.scheme, UrlSanitizer.sanitize(httpRequest.url)) + .withRequestTimeout(env.requestTimeout) // we should monitor leaks + .withMethod(httpRequest.method) + .withHttpHeaders(httpRequest.headers.toSeq.filterNot(_._1 == "Cookie"): _*) + .withCookies(wsCookiesIn: _*) + .withBody(body) + .withFollowRedirects(false) + .stream() + // env.gatewayClient + // .urlWithProtocol(target.scheme, url) + // //.withRequestTimeout(descriptor.clientConfig.callTimeout.millis) + // .withRequestTimeout(6.hour) // we should monitor leaks + // .withMethod(req.method) + // // .withQueryString(queryString: _*) + // .withHttpHeaders(headersIn: _*) + // .withBody(body) + // .withFollowRedirects(false) + // .stream() + .flatMap(resp => quotas.fast.map(q => (resp, q))) + .flatMap { tuple => + 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 + ) + // logger.trace(s"Connection: ${resp.headers.headers.get("Connection").map(_.last)}") + // if (env.notDev && !headers.get(env.Headers.OtoroshiStateResp).contains(state)) { + // val validState = headers.get(env.Headers.OtoroshiStateResp).filter(c => env.crypto.verifyString(state, c)).orElse(headers.get(env.Headers.OtoroshiStateResp).contains(state)).getOrElse(false) + if (env.notDev && (descriptor.enforceSecureCommunication && descriptor.sendStateChallenge) + && !descriptor.isUriExcludedFromSecuredCommunication("/" + uri) + && !headers.get(env.Headers.OtoroshiStateResp).contains(state)) { + 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 + ) + } else if (isUp) { + // val body = Await.result(resp.body.runFold(ByteString.empty)((a, b) => a.concat(b)).map(_.utf8String), Duration("10s")) + val exchange = Json.prettyPrint( + Json.obj( + "uri" -> req.relativeUri, + "url" -> url, + "state" -> state, + "reveivedState" -> JsString( + headers.getOrElse(env.Headers.OtoroshiStateResp, "--") + ), + "claim" -> claim, + "method" -> req.method, + "query" -> req.rawQueryString, + "status" -> resp.status, + "headersIn" -> JsArray( + req.headers.toSimpleMap + .map(t => Json.obj("name" -> t._1, "value" -> t._2)) + .toSeq + ), + "headersOut" -> JsArray( + headers.map(t => Json.obj("name" -> t._1, "values" -> t._2)).toSeq + ) + ) + ) + logger + .error( + s"\n\nError while talking with downstream service :(\n\n$exchange\n\n" + ) + Errors.craftResponseResult( + "Downstream microservice 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 + ) + } 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 + ) + } + } else { + val upstreamLatency = System.currentTimeMillis() - upstreamStart + val _headersOut: Seq[(String, String)] = _headersForOut + .filterNot(t => headersOutFiltered.contains(t._1.toLowerCase)) ++ ( + if (descriptor.sendOtoroshiHeadersBack) { + Seq( + env.Headers.OtoroshiRequestId -> snowflake, + env.Headers.OtoroshiRequestTimestamp -> requestTimestamp, + env.Headers.OtoroshiProxyLatency -> s"$overhead", + env.Headers.OtoroshiUpstreamLatency -> s"$upstreamLatency" //, + //env.Headers.OtoroshiTrackerId -> s"${env.sign(trackingId)}::$trackingId" + ) + } else { + Seq.empty[(String, String)] + } + ) ++ Some(trackingId) + .filter(_ => desc.canary.enabled) + .map( + _ => env.Headers.OtoroshiTrackerId -> s"${env.sign(trackingId)}::$trackingId" + ) ++ (if (descriptor.sendOtoroshiHeadersBack && apiKey.isDefined) { + Seq( + env.Headers.OtoroshiDailyCallsRemaining -> remainingQuotas.remainingCallsPerDay.toString, + env.Headers.OtoroshiMonthlyCallsRemaining -> remainingQuotas.remainingCallsPerMonth.toString ) - }) - .map { bs => - // debugLogger.trace(s"chunk on ${req.relativeUri} => ${bs.utf8String}") - // meterOut.mark(bs.length) - counterOut.addAndGet(bs.length) - bs - } + } else { + Seq.empty[(String, String)] + }) ++ descriptor.cors + .asHeaders(req) ++ desc.additionalHeadersOut.toSeq + + val otoroshiResponse = otoroshi.script.HttpResponse( + status = resp.status, + headers = _headersOut.toMap, + cookies = resp.cookies + ) + descriptor + .transformResponse( + snowflake = snowflake, + rawResponse = rawResponse, + otoroshiResponse = otoroshiResponse, + desc = descriptor, + apiKey = apiKey, + user = paUsr + ) + .flatMap { + case Left(badResult) => FastFuture.successful(badResult) + case Right(httpResponse) => { + val headersOut = httpResponse.headers.toSeq + val contentType = + httpResponse.headers.getOrElse("Content-Type", MimeTypes.TEXT) + // val _contentTypeOpt = resp.headers.get("Content-Type").flatMap(_.lastOption) + // meterOut.mark(responseHeader.length) + // counterOut.addAndGet(responseHeader.length) + + 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, + upstreamLatency, + headersOut.map(Header.apply)) + ) + case Failure(e) => + logger.error( + s"error while transfering stream for ${protocol}://${req.host}${req.relativeUri}", + e + ) + promise.trySuccess( + ProxyDone(httpResponse.status, + upstreamLatency, + headersOut.map(Header.apply)) + ) + }) + .map { bs => + // debugLogger.trace(s"chunk on ${req.relativeUri} => ${bs.utf8String}") + // meterOut.mark(bs.length) + counterOut.addAndGet(bs.length) + bs + } - val finalStream = descriptor.transformResponseBody( - snowflake = snowflake, - rawResponse = rawResponse, - otoroshiResponse = otoroshiResponse, - desc = descriptor, - apiKey = apiKey, - user = paUsr, - body = theStream - ) + val finalStream = descriptor.transformResponseBody( + snowflake = snowflake, + rawResponse = rawResponse, + otoroshiResponse = otoroshiResponse, + desc = descriptor, + apiKey = apiKey, + user = paUsr, + body = theStream + ) - val cookies = httpResponse.cookies.map(c => Cookie( - name = c.name, - value = c.value, - maxAge = c.maxAge.map(_.toInt), - path = c.path.getOrElse("/"), - domain = c.domain, - secure = c.secure, - httpOnly = c.httpOnly, - sameSite = None - )) - - if (req.version == "HTTP/1.0") { - logger.warn( - s"HTTP/1.0 request, storing temporary result in memory :( (${protocol}://${req.host}${req.relativeUri})" + val cookies = httpResponse.cookies.map( + c => + Cookie( + name = c.name, + value = c.value, + maxAge = c.maxAge.map(_.toInt), + path = c.path.getOrElse("/"), + domain = c.domain, + secure = c.secure, + httpOnly = c.httpOnly, + sameSite = None + ) ) - finalStream - .via( - MaxLengthLimiter(globalConfig.maxHttp10ResponseSize.toInt, - str => logger.warn(str)) + + if (req.version == "HTTP/1.0") { + logger.warn( + s"HTTP/1.0 request, storing temporary result in memory :( (${protocol}://${req.host}${req.relativeUri})" ) - .runWith(Sink.reduce[ByteString]((bs, n) => bs.concat(n))) - .fast - .map { body => - Status(httpResponse.status)(body) - .withHeaders(headersOut.filterNot(h => h._1 == "Content-Type" || h._1 == "Set-Cookie"): _*) - .as(contentType) - .withCookies((withTrackingCookies ++ cookies): _*) - } - } else if (globalConfig.streamEntityOnly) { // only temporary - // stream out - val entity = - if (httpResponse.headers - .get("Transfer-Encoding") - //.flatMap(_.lastOption) - .contains("chunked")) { - HttpEntity.Chunked( - finalStream - .map(i => play.api.http.HttpChunk.Chunk(i)) - .concat( - Source.single( - play.api.http.HttpChunk.LastChunk(play.api.mvc.Headers()) - ) - ), - Some(contentType) // contentTypeOpt - ) - } else { - HttpEntity.Streamed( - finalStream, - httpResponse.headers - .get("Content-Length") - //.flatMap(_.lastOption) - .map(_.toLong + snowMonkeyContext.trailingResponseBodySize), - Some(contentType) // contentTypeOpt + finalStream + .via( + MaxLengthLimiter(globalConfig.maxHttp10ResponseSize.toInt, + str => logger.warn(str)) ) - } - FastFuture.successful( - Status(httpResponse.status) - .sendEntity(entity) - .withHeaders(headersOut.filterNot(h => h._1 == "Content-Type" || h._1 == "Set-Cookie"): _*) - .as(contentType) - .withCookies((withTrackingCookies ++ cookies): _*) - ) - } else { - val response = httpResponse.headers - .get("Transfer-Encoding") - //.flatMap(_.lastOption) - .filter(_ == "chunked") - .map { _ => - // stream out - Status(httpResponse.status) - .chunked(finalStream) - .withHeaders(headersOut.filterNot(h => h._1 == "Set-Cookie"): _*) - .withCookies((withTrackingCookies ++ cookies): _*) - // .as(contentType) - } getOrElse { + .runWith(Sink.reduce[ByteString]((bs, n) => bs.concat(n))) + .fast + .map { body => + Status(httpResponse.status)(body) + .withHeaders( + headersOut.filterNot( + h => h._1 == "Content-Type" || h._1 == "Set-Cookie" + ): _* + ) + .as(contentType) + .withCookies((withTrackingCookies ++ cookies): _*) + } + } else if (globalConfig.streamEntityOnly) { // only temporary // stream out - Status(httpResponse.status) - .sendEntity( + val entity = + if (httpResponse.headers + .get("Transfer-Encoding") + //.flatMap(_.lastOption) + .contains("chunked")) { + HttpEntity.Chunked( + finalStream + .map(i => play.api.http.HttpChunk.Chunk(i)) + .concat( + Source.single( + play.api.http.HttpChunk + .LastChunk(play.api.mvc.Headers()) + ) + ), + Some(contentType) // contentTypeOpt + ) + } else { HttpEntity.Streamed( finalStream, httpResponse.headers .get("Content-Length") //.flatMap(_.lastOption) .map(_.toLong + snowMonkeyContext.trailingResponseBodySize), - httpResponse.headers.get("Content-Type") + Some(contentType) // contentTypeOpt ) - ) - .withHeaders(headersOut.filterNot(h => h._1 == "Content-Type" || h._1 == "Set-Cookie"): _*) - .withCookies((withTrackingCookies ++ cookies): _*) - .as(contentType) + } + FastFuture.successful( + Status(httpResponse.status) + .sendEntity(entity) + .withHeaders( + headersOut.filterNot( + h => h._1 == "Content-Type" || h._1 == "Set-Cookie" + ): _* + ) + .as(contentType) + .withCookies((withTrackingCookies ++ cookies): _*) + ) + } else { + val response = httpResponse.headers + .get("Transfer-Encoding") + //.flatMap(_.lastOption) + .filter(_ == "chunked") + .map { _ => + // stream out + Status(httpResponse.status) + .chunked(finalStream) + .withHeaders( + headersOut.filterNot(h => h._1 == "Set-Cookie"): _* + ) + .withCookies((withTrackingCookies ++ cookies): _*) + // .as(contentType) + } getOrElse { + // stream out + Status(httpResponse.status) + .sendEntity( + HttpEntity.Streamed( + finalStream, + httpResponse.headers + .get("Content-Length") + //.flatMap(_.lastOption) + .map( + _.toLong + snowMonkeyContext.trailingResponseBodySize + ), + httpResponse.headers.get("Content-Type") + ) + ) + .withHeaders( + headersOut.filterNot( + h => h._1 == "Content-Type" || h._1 == "Set-Cookie" + ): _* + ) + .withCookies((withTrackingCookies ++ cookies): _*) + .as(contentType) + } + FastFuture.successful(response) } - FastFuture.successful(response) } } - } + } + } + } + } + } + def passWithApiKey(config: GlobalConfig): Future[Result] = { + val authByJwtToken = req.headers + .get(env.Headers.OtoroshiBearer) + .orElse( + req.headers.get("Authorization").filter(_.startsWith("Bearer ")) + ) + .map(_.replace("Bearer ", "")) + .orElse( + req.queryString.get(env.Headers.OtoroshiBearerAuthorization).flatMap(_.lastOption) + ) + .orElse(req.cookies.get(env.Headers.OtoroshiJWTAuthorization).map(_.value)) + .filter(_.split("\\.").length == 3) + val authBasic = req.headers + .get(env.Headers.OtoroshiAuthorization) + .orElse( + req.headers.get("Authorization").filter(_.startsWith("Basic ")) + ) + .map(_.replace("Basic ", "")) + .flatMap(e => Try(decodeBase64(e)).toOption) + .orElse( + req.queryString + .get(env.Headers.OtoroshiBasicAuthorization) + .flatMap(_.lastOption) + .flatMap(e => Try(decodeBase64(e)).toOption) + ) + val authByCustomHeaders = req.headers + .get(env.Headers.OtoroshiClientId) + .flatMap(id => req.headers.get(env.Headers.OtoroshiClientSecret).map(s => (id, s))) + val authBySimpleApiKeyClientId = req.headers + .get(env.Headers.OtoroshiSimpleApiKeyClientId) + if (authBySimpleApiKeyClientId.isDefined) { + val clientId = authBySimpleApiKeyClientId.get + env.datastores.apiKeyDataStore.findAuthorizeKeyFor(clientId, descriptor.id).flatMap { + case None => + Errors.craftResponseResult( + "Invalid API key", + BadRequest, + req, + Some(descriptor), + Some("errors.invalid.api.key"), + duration = System.currentTimeMillis - start, + overhead = (System.currentTimeMillis() - secondStart) + firstOverhead + ) + case Some(key) if !key.allowClientIdOnly => { + Errors.craftResponseResult( + "Bad API key", + BadRequest, + req, + Some(descriptor), + Some("errors.bad.api.key"), + duration = System.currentTimeMillis - start, + overhead = (System.currentTimeMillis() - secondStart) + firstOverhead + ) + } + case Some(key) if key.allowClientIdOnly => + key.withingQuotas().flatMap { + case true => callDownstream(config, Some(key)) + case false => + Errors.craftResponseResult( + "You performed too much requests", + TooManyRequests, + req, + Some(descriptor), + Some("errors.too.much.requests"), + duration = System.currentTimeMillis - start, + overhead = (System.currentTimeMillis() - secondStart) + firstOverhead + ) } } - } - } - - def passWithApiKey(config: GlobalConfig): Future[Result] = { - val authByJwtToken = req.headers - .get(env.Headers.OtoroshiBearer) - .orElse( - req.headers.get("Authorization").filter(_.startsWith("Bearer ")) - ) - .map(_.replace("Bearer ", "")) - .orElse( - req.queryString.get(env.Headers.OtoroshiBearerAuthorization).flatMap(_.lastOption) - ) - .orElse(req.cookies.get(env.Headers.OtoroshiJWTAuthorization).map(_.value)) - .filter(_.split("\\.").length == 3) - val authBasic = req.headers - .get(env.Headers.OtoroshiAuthorization) - .orElse( - req.headers.get("Authorization").filter(_.startsWith("Basic ")) - ) - .map(_.replace("Basic ", "")) - .flatMap(e => Try(decodeBase64(e)).toOption) - .orElse( - req.queryString - .get(env.Headers.OtoroshiBasicAuthorization) - .flatMap(_.lastOption) - .flatMap(e => Try(decodeBase64(e)).toOption) - ) - val authByCustomHeaders = req.headers - .get(env.Headers.OtoroshiClientId) - .flatMap(id => req.headers.get(env.Headers.OtoroshiClientSecret).map(s => (id, s))) - val authBySimpleApiKeyClientId = req.headers - .get(env.Headers.OtoroshiSimpleApiKeyClientId) - if (authBySimpleApiKeyClientId.isDefined) { - val clientId = authBySimpleApiKeyClientId.get - env.datastores.apiKeyDataStore.findAuthorizeKeyFor(clientId, descriptor.id).flatMap { - case None => - Errors.craftResponseResult( - "Invalid API key", - BadRequest, - req, - Some(descriptor), - Some("errors.invalid.api.key"), - duration = System.currentTimeMillis - start, - overhead = (System.currentTimeMillis() - secondStart) + firstOverhead - ) - case Some(key) if !key.allowClientIdOnly => { - Errors.craftResponseResult( - "Bad API key", - BadRequest, - req, - Some(descriptor), - Some("errors.bad.api.key"), - duration = System.currentTimeMillis - start, - overhead = (System.currentTimeMillis() - secondStart) + firstOverhead - ) - } - case Some(key) if key.allowClientIdOnly => - key.withingQuotas().flatMap { - case true => callDownstream(config, Some(key)) - case false => - Errors.craftResponseResult( - "You performed too much requests", - TooManyRequests, - req, - Some(descriptor), - Some("errors.too.much.requests"), - duration = System.currentTimeMillis - start, - overhead = (System.currentTimeMillis() - secondStart) + firstOverhead - ) + } else if (authByCustomHeaders.isDefined) { + val (clientId, clientSecret) = authByCustomHeaders.get + env.datastores.apiKeyDataStore.findAuthorizeKeyFor(clientId, descriptor.id).flatMap { + case None => + Errors.craftResponseResult( + "Invalid API key", + BadRequest, + req, + Some(descriptor), + Some("errors.invalid.api.key"), + duration = System.currentTimeMillis - start, + overhead = (System.currentTimeMillis() - secondStart) + firstOverhead + ) + case Some(key) if key.isInvalid(clientSecret) => { + Alerts.send( + RevokedApiKeyUsageAlert(env.snowflakeGenerator.nextIdStr(), + DateTime.now(), + env.env, + req, + key, + descriptor) + ) + Errors.craftResponseResult( + "Bad API key", + BadRequest, + req, + Some(descriptor), + Some("errors.bad.api.key"), + duration = System.currentTimeMillis - start, + overhead = (System.currentTimeMillis() - secondStart) + firstOverhead + ) } - } - } else if (authByCustomHeaders.isDefined) { - val (clientId, clientSecret) = authByCustomHeaders.get - env.datastores.apiKeyDataStore.findAuthorizeKeyFor(clientId, descriptor.id).flatMap { - case None => - Errors.craftResponseResult( - "Invalid API key", - BadRequest, - req, - Some(descriptor), - Some("errors.invalid.api.key"), - duration = System.currentTimeMillis - start, - overhead = (System.currentTimeMillis() - secondStart) + firstOverhead - ) - case Some(key) if key.isInvalid(clientSecret) => { - Alerts.send( - RevokedApiKeyUsageAlert(env.snowflakeGenerator.nextIdStr(), - DateTime.now(), - env.env, - req, - key, - descriptor) - ) - Errors.craftResponseResult( - "Bad API key", - BadRequest, - req, - Some(descriptor), - Some("errors.bad.api.key"), - duration = System.currentTimeMillis - start, - overhead = (System.currentTimeMillis() - secondStart) + firstOverhead - ) + case Some(key) if key.isValid(clientSecret) => + key.withingQuotas().flatMap { + case true => callDownstream(config, Some(key)) + case false => + Errors.craftResponseResult( + "You performed too much requests", + TooManyRequests, + req, + Some(descriptor), + Some("errors.too.much.requests"), + duration = System.currentTimeMillis - start, + overhead = (System.currentTimeMillis() - secondStart) + firstOverhead + ) + } } - case Some(key) if key.isValid(clientSecret) => - key.withingQuotas().flatMap { - case true => callDownstream(config, Some(key)) - case false => + } else if (authByJwtToken.isDefined) { + val jwtTokenValue = authByJwtToken.get + Try { + JWT.decode(jwtTokenValue) + } map { jwt => + Option(jwt.getClaim("iss")).map(_.asString()) match { + case Some(clientId) => + env.datastores.apiKeyDataStore + .findAuthorizeKeyFor(clientId, descriptor.id) + .flatMap { + case Some(apiKey) => { + val algorithm = Option(jwt.getAlgorithm).map { + case "HS256" => Algorithm.HMAC256(apiKey.clientSecret) + case "HS512" => Algorithm.HMAC512(apiKey.clientSecret) + } getOrElse Algorithm.HMAC512(apiKey.clientSecret) + val verifier = JWT.require(algorithm).withIssuer(apiKey.clientName).build + Try(verifier.verify(jwtTokenValue)).filter { token => + val xsrfToken = token.getClaim("xsrfToken") + val xsrfTokenHeader = req.headers.get("X-XSRF-TOKEN") + if (!xsrfToken.isNull && xsrfTokenHeader.isDefined) { + xsrfToken.asString() == xsrfTokenHeader.get + } else if (!xsrfToken.isNull && xsrfTokenHeader.isEmpty) { + false + } else { + true + } + } match { + case Success(_) => + apiKey.withingQuotas().flatMap { + case true => callDownstream(config, Some(apiKey)) + case false => + Errors.craftResponseResult( + "You performed too much requests", + TooManyRequests, + req, + Some(descriptor), + Some("errors.too.much.requests"), + duration = System.currentTimeMillis - start, + overhead = (System + .currentTimeMillis() - secondStart) + firstOverhead + ) + } + case Failure(e) => { + Alerts.send( + RevokedApiKeyUsageAlert(env.snowflakeGenerator.nextIdStr(), + DateTime.now(), + env.env, + req, + apiKey, + descriptor) + ) + Errors.craftResponseResult( + "Bad API key", + BadRequest, + req, + Some(descriptor), + Some("errors.bad.api.key"), + duration = System.currentTimeMillis - start, + overhead = (System + .currentTimeMillis() - secondStart) + firstOverhead + ) + } + } + } + case None => + Errors.craftResponseResult( + "Invalid ApiKey provided 1", + BadRequest, + req, + Some(descriptor), + Some("errors.invalid.api.key"), + duration = System.currentTimeMillis - start, + overhead = (System.currentTimeMillis() - secondStart) + firstOverhead + ) + } + case None => Errors.craftResponseResult( - "You performed too much requests", - TooManyRequests, + "Invalid ApiKey provided 2", + BadRequest, req, Some(descriptor), - Some("errors.too.much.requests"), + Some("errors.invalid.api.key"), duration = System.currentTimeMillis - start, overhead = (System.currentTimeMillis() - secondStart) + firstOverhead ) } - } - } else if (authByJwtToken.isDefined) { - val jwtTokenValue = authByJwtToken.get - Try { - JWT.decode(jwtTokenValue) - } map { jwt => - Option(jwt.getClaim("iss")).map(_.asString()) match { - case Some(clientId) => + } getOrElse Errors.craftResponseResult( + s"Invalid ApiKey provided 3, $authByJwtToken, $authBasic", + BadRequest, + req, + Some(descriptor), + Some("errors.invalid.api.key"), + duration = System.currentTimeMillis - start, + overhead = (System.currentTimeMillis() - secondStart) + firstOverhead + ) + } else if (authBasic.isDefined) { + val auth = authBasic.get + val id = auth.split(":").headOption.map(_.trim) + val secret = auth.split(":").lastOption.map(_.trim) + (id, secret) match { + case (Some(apiKeyClientId), Some(apiKeySecret)) => { env.datastores.apiKeyDataStore - .findAuthorizeKeyFor(clientId, descriptor.id) + .findAuthorizeKeyFor(apiKeyClientId, descriptor.id) .flatMap { - case Some(apiKey) => { - val algorithm = Option(jwt.getAlgorithm).map { - case "HS256" => Algorithm.HMAC256(apiKey.clientSecret) - case "HS512" => Algorithm.HMAC512(apiKey.clientSecret) - } getOrElse Algorithm.HMAC512(apiKey.clientSecret) - val verifier = JWT.require(algorithm).withIssuer(apiKey.clientName).build - Try(verifier.verify(jwtTokenValue)).filter { token => - val xsrfToken = token.getClaim("xsrfToken") - val xsrfTokenHeader = req.headers.get("X-XSRF-TOKEN") - if (!xsrfToken.isNull && xsrfTokenHeader.isDefined) { - xsrfToken.asString() == xsrfTokenHeader.get - } else if (!xsrfToken.isNull && xsrfTokenHeader.isEmpty) { - false - } else { - true - } - } match { - case Success(_) => - apiKey.withingQuotas().flatMap { - case true => callDownstream(config, Some(apiKey)) - case false => - Errors.craftResponseResult( - "You performed too much requests", - TooManyRequests, - req, - Some(descriptor), - Some("errors.too.much.requests"), - duration = System.currentTimeMillis - start, - overhead = (System - .currentTimeMillis() - secondStart) + firstOverhead - ) - } - case Failure(e) => { - Alerts.send( - RevokedApiKeyUsageAlert(env.snowflakeGenerator.nextIdStr(), - DateTime.now(), - env.env, - req, - apiKey, - descriptor) - ) - Errors.craftResponseResult( - "Bad API key", - BadRequest, - req, - Some(descriptor), - Some("errors.bad.api.key"), - duration = System.currentTimeMillis - start, - overhead = (System.currentTimeMillis() - secondStart) + firstOverhead - ) - } - } - } case None => Errors.craftResponseResult( - "Invalid ApiKey provided 1", - BadRequest, + "Invalid API key", + BadGateway, req, Some(descriptor), Some("errors.invalid.api.key"), duration = System.currentTimeMillis - start, overhead = (System.currentTimeMillis() - secondStart) + firstOverhead ) + case Some(key) if key.isInvalid(apiKeySecret) => { + Alerts.send( + RevokedApiKeyUsageAlert(env.snowflakeGenerator.nextIdStr(), + DateTime.now(), + env.env, + req, + key, + descriptor) + ) + Errors.craftResponseResult( + "Bad API key", + BadGateway, + req, + Some(descriptor), + Some("errors.bad.api.key"), + duration = System.currentTimeMillis - start, + overhead = (System.currentTimeMillis() - secondStart) + firstOverhead + ) + } + case Some(key) if key.isValid(apiKeySecret) => + key.withingQuotas().flatMap { + case true => callDownstream(config, Some(key)) + case false => + Errors.craftResponseResult( + "You performed too much requests", + TooManyRequests, + req, + Some(descriptor), + Some("errors.too.much.requests"), + duration = System.currentTimeMillis - start, + overhead = (System.currentTimeMillis() - secondStart) + firstOverhead + ) + } } - case None => + } + case _ => Errors.craftResponseResult( - "Invalid ApiKey provided 2", + "No ApiKey provided", BadRequest, req, Some(descriptor), - Some("errors.invalid.api.key"), + Some("errors.no.api.key"), duration = System.currentTimeMillis - start, overhead = (System.currentTimeMillis() - secondStart) + firstOverhead ) } - } getOrElse Errors.craftResponseResult( - s"Invalid ApiKey provided 3, $authByJwtToken, $authBasic", - BadRequest, - req, - Some(descriptor), - Some("errors.invalid.api.key"), - duration = System.currentTimeMillis - start, - overhead = (System.currentTimeMillis() - secondStart) + firstOverhead - ) - } else if (authBasic.isDefined) { - val auth = authBasic.get - val id = auth.split(":").headOption.map(_.trim) - val secret = auth.split(":").lastOption.map(_.trim) - (id, secret) match { - case (Some(apiKeyClientId), Some(apiKeySecret)) => { - env.datastores.apiKeyDataStore - .findAuthorizeKeyFor(apiKeyClientId, descriptor.id) - .flatMap { - case None => - Errors.craftResponseResult( - "Invalid API key", - BadGateway, - req, - Some(descriptor), - Some("errors.invalid.api.key"), - duration = System.currentTimeMillis - start, - overhead = (System.currentTimeMillis() - secondStart) + firstOverhead - ) - case Some(key) if key.isInvalid(apiKeySecret) => { - Alerts.send( - RevokedApiKeyUsageAlert(env.snowflakeGenerator.nextIdStr(), - DateTime.now(), - env.env, - req, - key, - descriptor) - ) - Errors.craftResponseResult( - "Bad API key", - BadGateway, - req, - Some(descriptor), - Some("errors.bad.api.key"), - duration = System.currentTimeMillis - start, - overhead = (System.currentTimeMillis() - secondStart) + firstOverhead - ) - } - case Some(key) if key.isValid(apiKeySecret) => - key.withingQuotas().flatMap { - case true => callDownstream(config, Some(key)) - case false => - Errors.craftResponseResult( - "You performed too much requests", - TooManyRequests, - req, - Some(descriptor), - Some("errors.too.much.requests"), - duration = System.currentTimeMillis - start, - overhead = (System.currentTimeMillis() - secondStart) + firstOverhead - ) - } - } - } - case _ => - Errors.craftResponseResult( - "No ApiKey provided", - BadRequest, - req, - Some(descriptor), - Some("errors.no.api.key"), - duration = System.currentTimeMillis - start, - overhead = (System.currentTimeMillis() - secondStart) + firstOverhead - ) + } else { + Errors.craftResponseResult( + "No ApiKey provided", + BadRequest, + req, + Some(descriptor), + Some("errors.no.api.key"), + duration = System.currentTimeMillis - start, + overhead = (System.currentTimeMillis() - secondStart) + firstOverhead + ) } - } else { - Errors.craftResponseResult( - "No ApiKey provided", - BadRequest, - req, - Some(descriptor), - Some("errors.no.api.key"), - duration = System.currentTimeMillis - start, - overhead = (System.currentTimeMillis() - secondStart) + firstOverhead - ) } - } - def passWithAuth0(config: GlobalConfig): Future[Result] = { - isPrivateAppsSessionValid(req, descriptor).flatMap { - case Some(paUsr) => callDownstream(config, paUsr = Some(paUsr)) - case None => { - val redirectTo = env.rootScheme + env.privateAppsHost + env.privateAppsPort - .map(a => s":$a") - .getOrElse("") + controllers.routes.AuthController - .confidentialAppLoginPage() - .url + s"?desc=${descriptor.id}&redirect=${protocol}://${req.host}${req.relativeUri}" - logger.trace("should redirect to " + redirectTo) - descriptor.authConfigRef match { - case None => - Errors.craftResponseResult( - "Auth. config. ref not found on the descriptor", - InternalServerError, - req, - Some(descriptor), - Some("errors.auth.config.ref.not.found"), - duration = System.currentTimeMillis - start, - overhead = (System.currentTimeMillis() - secondStart) + firstOverhead - ) - case Some(ref) => { - env.datastores.authConfigsDataStore.findById(ref).flatMap { - case None => - Errors.craftResponseResult( - "Auth. config. not found on the descriptor", - InternalServerError, - req, - Some(descriptor), - Some("errors.auth.config.not.found"), - duration = System.currentTimeMillis - start, - overhead = (System.currentTimeMillis() - secondStart) + firstOverhead - ) - case Some(auth) => { - FastFuture.successful( - Results - .Redirect(redirectTo) - .discardingCookies( - env.removePrivateSessionCookies( - ServiceDescriptorQuery(subdomain, serviceEnv, domain, "/").toHost, - descriptor, - auth - ): _* - ) - ) + def passWithAuth0(config: GlobalConfig): Future[Result] = { + isPrivateAppsSessionValid(req, descriptor).flatMap { + case Some(paUsr) => callDownstream(config, paUsr = Some(paUsr)) + case None => { + val redirectTo = env.rootScheme + env.privateAppsHost + env.privateAppsPort + .map(a => s":$a") + .getOrElse("") + controllers.routes.AuthController + .confidentialAppLoginPage() + .url + s"?desc=${descriptor.id}&redirect=${protocol}://${req.host}${req.relativeUri}" + logger.trace("should redirect to " + redirectTo) + descriptor.authConfigRef match { + case None => + Errors.craftResponseResult( + "Auth. config. ref not found on the descriptor", + InternalServerError, + req, + Some(descriptor), + Some("errors.auth.config.ref.not.found"), + duration = System.currentTimeMillis - start, + overhead = (System.currentTimeMillis() - secondStart) + firstOverhead + ) + case Some(ref) => { + env.datastores.authConfigsDataStore.findById(ref).flatMap { + case None => + Errors.craftResponseResult( + "Auth. config. not found on the descriptor", + InternalServerError, + req, + Some(descriptor), + Some("errors.auth.config.not.found"), + duration = System.currentTimeMillis - start, + overhead = (System.currentTimeMillis() - secondStart) + firstOverhead + ) + case Some(auth) => { + FastFuture.successful( + Results + .Redirect(redirectTo) + .discardingCookies( + env.removePrivateSessionCookies( + ServiceDescriptorQuery(subdomain, serviceEnv, domain, "/").toHost, + descriptor, + auth + ): _* + ) + ) + } } } } } } } - } - // Algo is : - // if (app.private) { - // if (uri.isPublic) { - // AUTH0 - // } else { - // APIKEY - // } - // } else { - // if (uri.isPublic) { - // PASSTHROUGH without gateway auth - // } else { - // APIKEY - // } - // } - - env.datastores.globalConfigDataStore.quotasValidationFor(from).flatMap { r => - val (within, secCalls, maybeQuota) = r - val quota = maybeQuota.getOrElse(globalConfig.perIpThrottlingQuota) - if (secCalls > (quota * 10L)) { - Errors.craftResponseResult( - "[IP] You performed too much requests", - TooManyRequests, - req, - Some(descriptor), - Some("errors.too.much.requests"), - duration = System.currentTimeMillis - start, - overhead = (System.currentTimeMillis() - secondStart) + firstOverhead - ) - } else { - if (env.isProd && !isSecured && desc.forceHttps) { - val theDomain = req.domain - val protocol = getProtocolFor(req) - logger.trace( - s"redirects prod service from ${protocol}://$theDomain${req.relativeUri} to https://$theDomain${req.relativeUri}" - ) - //FastFuture.successful(Redirect(s"${env.rootScheme}$theDomain${req.relativeUri}")) - FastFuture.successful(Redirect(s"https://$theDomain${req.relativeUri}")) - } else if (!within) { - // TODO : count as served req here !!! + // Algo is : + // if (app.private) { + // if (uri.isPublic) { + // AUTH0 + // } else { + // APIKEY + // } + // } else { + // if (uri.isPublic) { + // PASSTHROUGH without gateway auth + // } else { + // APIKEY + // } + // } + + env.datastores.globalConfigDataStore.quotasValidationFor(from).flatMap { r => + val (within, secCalls, maybeQuota) = r + val quota = maybeQuota.getOrElse(globalConfig.perIpThrottlingQuota) + if (secCalls > (quota * 10L)) { Errors.craftResponseResult( - "[GLOBAL] You performed too much requests", + "[IP] You performed too much requests", TooManyRequests, req, Some(descriptor), @@ -1783,141 +1795,164 @@ class GatewayRequestHandler(snowMonkey: SnowMonkey, duration = System.currentTimeMillis - start, overhead = (System.currentTimeMillis() - secondStart) + firstOverhead ) - } else if (globalConfig.ipFiltering.whitelist.nonEmpty && !globalConfig.ipFiltering.whitelist - .exists(ip => utils.RegexPool(ip).matches(remoteAddress))) { - Errors.craftResponseResult( - "Your IP address is not allowed", - Forbidden, - req, - Some(descriptor), - Some("errors.ip.address.not.allowed"), - duration = System.currentTimeMillis - start, - overhead = (System.currentTimeMillis() - secondStart) + firstOverhead - ) // global whitelist - } else if (globalConfig.ipFiltering.blacklist.nonEmpty && globalConfig.ipFiltering.blacklist - .exists(ip => utils.RegexPool(ip).matches(remoteAddress))) { - Errors.craftResponseResult( - "Your IP address is not allowed", - Forbidden, - req, - Some(descriptor), - Some("errors.ip.address.not.allowed"), - duration = System.currentTimeMillis - start, - overhead = (System.currentTimeMillis() - secondStart) + firstOverhead - ) // global blacklist - } else if (descriptor.ipFiltering.whitelist.nonEmpty && !descriptor.ipFiltering.whitelist - .exists(ip => utils.RegexPool(ip).matches(remoteAddress))) { - Errors.craftResponseResult( - "Your IP address is not allowed", - Forbidden, - req, - Some(descriptor), - Some("errors.ip.address.not.allowed"), - duration = System.currentTimeMillis - start, - overhead = (System.currentTimeMillis() - secondStart) + firstOverhead - ) // service whitelist - } else if (descriptor.ipFiltering.blacklist.nonEmpty && descriptor.ipFiltering.blacklist - .exists(ip => utils.RegexPool(ip).matches(remoteAddress))) { - Errors.craftResponseResult( - "Your IP address is not allowed", - Forbidden, - req, - Some(descriptor), - Some("errors.ip.address.not.allowed"), - duration = System.currentTimeMillis - start, - overhead = (System.currentTimeMillis() - secondStart) + firstOverhead - ) // service blacklist - } else if (globalConfig.endlessIpAddresses.nonEmpty && globalConfig.endlessIpAddresses - .exists(ip => RegexPool(ip).matches(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 - FastFuture.successful( - Status(200) - .sendEntity( - HttpEntity.Streamed( - Source - .repeat(characters) - .take(expected), // 128 Go of zeros or middle fingers - None, - Some("application/octet-stream") - ) - ) - ) - } else if (descriptor.maintenanceMode) { - Errors.craftResponseResult( - "Service in maintenance mode", - ServiceUnavailable, - req, - Some(descriptor), - Some("errors.service.in.maintenance"), - duration = System.currentTimeMillis - start, - overhead = (System.currentTimeMillis() - secondStart) + firstOverhead - ) - } else if (descriptor.buildMode) { - Errors.craftResponseResult( - "Service under construction", - ServiceUnavailable, - req, - Some(descriptor), - Some("errors.service.under.construction"), - duration = System.currentTimeMillis - start, - overhead = (System.currentTimeMillis() - secondStart) + firstOverhead - ) - } else if (descriptor.cors.enabled && req.method == "OPTIONS" && req.headers - .get("Access-Control-Request-Method") - .isDefined && descriptor.cors.shouldApplyCors(req.path)) { - // handle cors preflight request - if (descriptor.cors.enabled && descriptor.cors.shouldNotPass(req)) { + } else { + if (env.isProd && !isSecured && desc.forceHttps) { + val theDomain = req.domain + val protocol = getProtocolFor(req) + logger.trace( + s"redirects prod service from ${protocol}://$theDomain${req.relativeUri} to https://$theDomain${req.relativeUri}" + ) + //FastFuture.successful(Redirect(s"${env.rootScheme}$theDomain${req.relativeUri}")) + FastFuture.successful(Redirect(s"https://$theDomain${req.relativeUri}")) + } else if (!within) { + // TODO : count as served req here !!! Errors.craftResponseResult( - "Cors error", - BadRequest, + "[GLOBAL] You performed too much requests", + TooManyRequests, req, Some(descriptor), - Some("errors.cors.error"), + Some("errors.too.much.requests"), duration = System.currentTimeMillis - start, overhead = (System.currentTimeMillis() - secondStart) + firstOverhead ) - } else { + } else if (globalConfig.ipFiltering.whitelist.nonEmpty && !globalConfig.ipFiltering.whitelist + .exists(ip => utils.RegexPool(ip).matches(remoteAddress))) { + Errors.craftResponseResult( + "Your IP address is not allowed", + Forbidden, + req, + Some(descriptor), + Some("errors.ip.address.not.allowed"), + duration = System.currentTimeMillis - start, + overhead = (System.currentTimeMillis() - secondStart) + firstOverhead + ) // global whitelist + } else if (globalConfig.ipFiltering.blacklist.nonEmpty && globalConfig.ipFiltering.blacklist + .exists(ip => utils.RegexPool(ip).matches(remoteAddress))) { + Errors.craftResponseResult( + "Your IP address is not allowed", + Forbidden, + req, + Some(descriptor), + Some("errors.ip.address.not.allowed"), + duration = System.currentTimeMillis - start, + overhead = (System.currentTimeMillis() - secondStart) + firstOverhead + ) // global blacklist + } else if (descriptor.ipFiltering.whitelist.nonEmpty && !descriptor.ipFiltering.whitelist + .exists(ip => utils.RegexPool(ip).matches(remoteAddress))) { + Errors.craftResponseResult( + "Your IP address is not allowed", + Forbidden, + req, + Some(descriptor), + Some("errors.ip.address.not.allowed"), + duration = System.currentTimeMillis - start, + overhead = (System.currentTimeMillis() - secondStart) + firstOverhead + ) // service whitelist + } else if (descriptor.ipFiltering.blacklist.nonEmpty && descriptor.ipFiltering.blacklist + .exists(ip => utils.RegexPool(ip).matches(remoteAddress))) { + Errors.craftResponseResult( + "Your IP address is not allowed", + Forbidden, + req, + Some(descriptor), + Some("errors.ip.address.not.allowed"), + duration = System.currentTimeMillis - start, + overhead = (System.currentTimeMillis() - secondStart) + firstOverhead + ) // service blacklist + } else if (globalConfig.endlessIpAddresses.nonEmpty && globalConfig.endlessIpAddresses + .exists(ip => RegexPool(ip).matches(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 FastFuture.successful( - Results.Ok(ByteString.empty).withHeaders(descriptor.cors.asHeaders(req): _*) + Status(200) + .sendEntity( + HttpEntity.Streamed( + Source + .repeat(characters) + .take(expected), // 128 Go of zeros or middle fingers + None, + Some("application/octet-stream") + ) + ) ) - } - } else if (isUp) { - if (descriptor.isPrivate && descriptor.authConfigRef.isDefined && !descriptor - .isExcludedFromSecurity(req.path)) { - if (descriptor.isUriPublic(req.path)) { - passWithAuth0(globalConfig) + } else if (descriptor.maintenanceMode) { + Errors.craftResponseResult( + "Service in maintenance mode", + ServiceUnavailable, + req, + Some(descriptor), + Some("errors.service.in.maintenance"), + duration = System.currentTimeMillis - start, + overhead = (System.currentTimeMillis() - secondStart) + firstOverhead + ) + } else if (descriptor.buildMode) { + Errors.craftResponseResult( + "Service under construction", + ServiceUnavailable, + req, + Some(descriptor), + Some("errors.service.under.construction"), + duration = System.currentTimeMillis - start, + overhead = (System.currentTimeMillis() - secondStart) + firstOverhead + ) + } else if (descriptor.cors.enabled && req.method == "OPTIONS" && req.headers + .get("Access-Control-Request-Method") + .isDefined && descriptor.cors.shouldApplyCors(req.path)) { + // handle cors preflight request + if (descriptor.cors.enabled && descriptor.cors.shouldNotPass(req)) { + Errors.craftResponseResult( + "Cors error", + BadRequest, + req, + Some(descriptor), + Some("errors.cors.error"), + duration = System.currentTimeMillis - start, + overhead = (System.currentTimeMillis() - secondStart) + firstOverhead + ) } else { - isPrivateAppsSessionValid(req, descriptor).fast.flatMap { - case Some(_) if descriptor.strictlyPrivate => passWithApiKey(globalConfig) - case Some(user) => passWithAuth0(globalConfig) - case None => passWithApiKey(globalConfig) - } + FastFuture.successful( + Results.Ok(ByteString.empty).withHeaders(descriptor.cors.asHeaders(req): _*) + ) } - } else { - if (descriptor.isUriPublic(req.path)) { - callDownstream(globalConfig) + } else if (isUp) { + if (descriptor.isPrivate && descriptor.authConfigRef.isDefined && !descriptor + .isExcludedFromSecurity(req.path)) { + if (descriptor.isUriPublic(req.path)) { + passWithAuth0(globalConfig) + } else { + isPrivateAppsSessionValid(req, descriptor).fast.flatMap { + case Some(_) if descriptor.strictlyPrivate => passWithApiKey(globalConfig) + case Some(user) => passWithAuth0(globalConfig) + case None => passWithApiKey(globalConfig) + } + } } else { - passWithApiKey(globalConfig) + if (descriptor.isUriPublic(req.path)) { + callDownstream(globalConfig) + } else { + passWithApiKey(globalConfig) + } } - } - } else { - // fail fast - 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 - ) + } else { + // fail fast + 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 + ) + } } } } @@ -1927,7 +1962,6 @@ class GatewayRequestHandler(snowMonkey: SnowMonkey, } } } - } } } } diff --git a/otoroshi/app/gateway/websockets.scala b/otoroshi/app/gateway/websockets.scala index 269cd7ff15..ae136209c3 100644 --- a/otoroshi/app/gateway/websockets.scala +++ b/otoroshi/app/gateway/websockets.scala @@ -18,7 +18,12 @@ import models._ import org.joda.time.DateTime import play.api.Logger import play.api.http.HttpEntity -import play.api.http.websocket.{CloseMessage, BinaryMessage => PlayWSBinaryMessage, Message => PlayWSMessage, TextMessage => PlayWSTextMessage} +import play.api.http.websocket.{ + CloseMessage, + BinaryMessage => PlayWSBinaryMessage, + Message => PlayWSMessage, + TextMessage => PlayWSTextMessage +} import play.api.libs.streams.ActorFlow import play.api.mvc.Results.{BadGateway, MethodNotAllowed, ServiceUnavailable, Status, TooManyRequests} import play.api.mvc._ @@ -93,10 +98,15 @@ class WebSocketHandler()(implicit env: Env) { def xForwardedHeader(desc: ServiceDescriptor, request: RequestHeader): Seq[(String, String)] = { if (desc.xForwardedHeaders) { - val xForwardedFor = request.headers.get("X-Forwarded-For").map(v => v + ", " + request.remoteAddress).getOrElse(request.remoteAddress) + val xForwardedFor = request.headers + .get("X-Forwarded-For") + .map(v => v + ", " + request.remoteAddress) + .getOrElse(request.remoteAddress) val xForwardedProto = getProtocolFor(request) - val xForwardedHost = request.headers.get("X-Forwarded-Host").getOrElse(request.host) - Seq("X-Forwarded-For" -> xForwardedFor, "X-Forwarded-Host" -> xForwardedHost, "X-Forwarded-Proto" -> xForwardedProto) + val xForwardedHost = request.headers.get("X-Forwarded-Host").getOrElse(request.host) + Seq("X-Forwarded-For" -> xForwardedFor, + "X-Forwarded-Host" -> xForwardedHost, + "X-Forwarded-Proto" -> xForwardedProto) } else { Seq.empty[(String, String)] } @@ -533,29 +543,30 @@ class WebSocketHandler()(implicit env: Env) { .serialize(desc.secComSettings)(env) logger.trace(s"Claim is : $claim") val headersIn: Seq[(String, String)] = - (req.headers.toMap.toSeq.flatMap(c => c._2.map(v => (c._1, v))) //.map(tuple => (tuple._1, tuple._2.mkString(","))) //.toSimpleMap - .filterNot(t => headersInFiltered.contains(t._1.toLowerCase)) ++ Map( - env.Headers.OtoroshiProxiedHost -> req.headers.get("Host").getOrElse("--"), - // "Host" -> host, - "Host" -> (if (desc.overrideHost) host else req.headers.get("Host").getOrElse("--")), - env.Headers.OtoroshiRequestId -> snowflake, - env.Headers.OtoroshiRequestTimestamp -> requestTimestamp - ) ++ (if (descriptor.enforceSecureCommunication && descriptor.sendStateChallenge) { - Map( - env.Headers.OtoroshiState -> state, - env.Headers.OtoroshiClaim -> claim - ) - } else if (descriptor.enforceSecureCommunication && !descriptor.sendStateChallenge) { - Map( - env.Headers.OtoroshiClaim -> claim - ) - } else { - Map.empty[String, String] - }) ++ - descriptor.additionalHeaders.filter(t => t._1.trim.nonEmpty) ++ fromOtoroshi - .map(v => Map(env.Headers.OtoroshiGatewayParentRequest -> fromOtoroshi.get)) - .getOrElse(Map.empty[String, String]) ++ jwtInjection.additionalHeaders).toSeq - .filterNot(t => jwtInjection.removeHeaders.contains(t._1)) ++ xForwardedHeader(desc, req) + (req.headers.toMap.toSeq + .flatMap(c => c._2.map(v => (c._1, v))) //.map(tuple => (tuple._1, tuple._2.mkString(","))) //.toSimpleMap + .filterNot(t => headersInFiltered.contains(t._1.toLowerCase)) ++ Map( + env.Headers.OtoroshiProxiedHost -> req.headers.get("Host").getOrElse("--"), + // "Host" -> host, + "Host" -> (if (desc.overrideHost) host else req.headers.get("Host").getOrElse("--")), + env.Headers.OtoroshiRequestId -> snowflake, + env.Headers.OtoroshiRequestTimestamp -> requestTimestamp + ) ++ (if (descriptor.enforceSecureCommunication && descriptor.sendStateChallenge) { + Map( + env.Headers.OtoroshiState -> state, + env.Headers.OtoroshiClaim -> claim + ) + } else if (descriptor.enforceSecureCommunication && !descriptor.sendStateChallenge) { + Map( + env.Headers.OtoroshiClaim -> claim + ) + } else { + Map.empty[String, String] + }) ++ + descriptor.additionalHeaders.filter(t => t._1.trim.nonEmpty) ++ fromOtoroshi + .map(v => Map(env.Headers.OtoroshiGatewayParentRequest -> fromOtoroshi.get)) + .getOrElse(Map.empty[String, String]) ++ jwtInjection.additionalHeaders).toSeq + .filterNot(t => jwtInjection.removeHeaders.contains(t._1)) ++ xForwardedHeader(desc, req) // val requestHeader = ByteString( // req.method + " " + req.relativeUri + " HTTP/1.1\n" + headersIn @@ -655,15 +666,18 @@ class WebSocketHandler()(implicit env: Env) { } } - val wsCookiesIn = req.cookies.toSeq.map(c => DefaultWSCookie( - name = c.name, - value = c.value, - domain = c.domain, - path = Option(c.path), - maxAge = c.maxAge.map(_.toLong), - secure = c.secure, - httpOnly = c.httpOnly - )) + val wsCookiesIn = req.cookies.toSeq.map( + c => + DefaultWSCookie( + name = c.name, + value = c.value, + domain = c.domain, + path = Option(c.path), + maxAge = c.maxAge.map(_.toLong), + secure = c.secure, + httpOnly = c.httpOnly + ) + ) val rawRequest = otoroshi.script.HttpRequest( url = s"${req.theProtocol}://${req.host}${req.relativeUri}", method = req.method, @@ -698,7 +712,8 @@ class WebSocketHandler()(implicit env: Env) { out, env, http, - httpRequest.headers.toSeq.filterNot(_._1 == "Cookie")) + httpRequest.headers.toSeq + .filterNot(_._1 == "Cookie")) ) ) ) diff --git a/otoroshi/app/script/script.scala b/otoroshi/app/script/script.scala index 0214cb19cb..2c7d9d1ecc 100644 --- a/otoroshi/app/script/script.scala +++ b/otoroshi/app/script/script.scala @@ -31,7 +31,10 @@ import scala.concurrent.duration._ import scala.concurrent.{ExecutionContext, Future} import scala.util.{Failure, Success, Try} -case class HttpRequest(url: String, method: String, headers: Map[String, String], cookies: Seq[WSCookie] = Seq.empty[WSCookie]) { +case class HttpRequest(url: String, + method: String, + headers: Map[String, String], + cookies: Seq[WSCookie] = Seq.empty[WSCookie]) { lazy val host: String = headers.getOrElse("Host", "") lazy val uri: Uri = Uri(url) lazy val scheme: String = uri.scheme diff --git a/otoroshi/javascript/src/components/AuthModuleConfig.js b/otoroshi/javascript/src/components/AuthModuleConfig.js index fdcbf6e7df..b878aa0506 100644 --- a/otoroshi/javascript/src/components/AuthModuleConfig.js +++ b/otoroshi/javascript/src/components/AuthModuleConfig.js @@ -56,8 +56,8 @@ export class Oauth2ModuleConfig extends Component { }; fetchConfig = () => { - const url = window.prompt("URL of the OIDC config"); - if( url) { + const url = window.prompt('URL of the OIDC config'); + if (url) { return fetch(`/bo/api/oidc/_fetchConfig`, { method: 'POST', credentials: 'include', @@ -65,17 +65,19 @@ export class Oauth2ModuleConfig extends Component { Accept: 'application/json', 'Content-Type': 'application/json', }, - body: JSON.stringify({ - url, + body: JSON.stringify({ + url, id: this.props.value.id, name: this.props.value.name, desc: this.props.value.desc, }), - }).then(r => r.json()).then(config => { - this.props.onChange(config); - }); + }) + .then(r => r.json()) + .then(config => { + this.props.onChange(config); + }); } - } + }; render() { const settings = this.props.value || this.props.settings; @@ -90,11 +92,16 @@ export class Oauth2ModuleConfig extends Component { return {this.state.error.message ? this.state.error.message : this.state.error}; } return ( -
-
- +
+
+
changeTheValue(path + '.otoroshiDataField', v)} /> - {settings.readProfileFromToken && } + {settings.readProfileFromToken && ( + + )}
); } diff --git a/otoroshi/javascript/src/pages/ServicePage.js b/otoroshi/javascript/src/pages/ServicePage.js index 589d17ae29..e484ffc8d6 100644 --- a/otoroshi/javascript/src/pages/ServicePage.js +++ b/otoroshi/javascript/src/pages/ServicePage.js @@ -522,60 +522,60 @@ export class ServicePage extends Component { onChange={e => this.changeTheValue('name', e)} /> -
-
- this.changeTheValue('enabled', v)} - /> - this.changeTheValue('readOnly', v)} - /> - this.changeTheValue('maintenanceMode', v)} - /> - this.changeTheValue('buildMode', v)} - /> +
+
+ this.changeTheValue('enabled', v)} + /> + this.changeTheValue('readOnly', v)} + /> + this.changeTheValue('maintenanceMode', v)} + /> + this.changeTheValue('buildMode', v)} + /> +
+
+ this.changeTheValue('sendOtoroshiHeadersBack', v)} + /> + this.changeTheValue('overrideHost', v)} + /> + this.changeTheValue('xForwardedHeaders', v)} + /> + this.changeTheValue('forceHttps', v)} + /> +
-
- this.changeTheValue('sendOtoroshiHeadersBack', v)} - /> - this.changeTheValue('overrideHost', v)} - /> - this.changeTheValue('xForwardedHeaders', v)} - /> - this.changeTheValue('forceHttps', v)} - /> -
-