Skip to content

Commit

Permalink
Merge pull request #2071 from MAIF/fix-throttling
Browse files Browse the repository at this point in the history
Fix throttling
  • Loading branch information
mathieuancelin authored Jan 22, 2025
2 parents 9599a96 + 350c25c commit 41c8973
Show file tree
Hide file tree
Showing 37 changed files with 272 additions and 186 deletions.
1 change: 1 addition & 0 deletions .github/workflows/build_dev_manual.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ jobs:
uses: actions/setup-java@v1
with:
java-version: 11
- uses: sbt/[email protected]
- name: Generate dev documentation website
run: sh ./scripts/doc.sh buildDev
- name: Commit files
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/publish_documentation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
uses: actions/setup-java@v1
with:
java-version: 11
- uses: sbt/[email protected]
- name: setup node
uses: actions/setup-node@v3
with:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jobs:
uses: actions/setup-java@v1
with:
java-version: 11
- uses: sbt/[email protected]
- name: setup node
uses: actions/setup-node@v3
with:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/server_build_and_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ jobs:
uses: actions/setup-java@v1
with:
java-version: 11
- name: install sbt
uses: sbt/[email protected]
# setup node to use yarn
- uses: actions/setup-node@v2
with:
Expand Down
6 changes: 3 additions & 3 deletions kubernetes/helm/otoroshi/crds-with-schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2316,7 +2316,7 @@ spec:
throttlingQuota:
type: "integer"
format: "int64"
description: "Authorized number of calls per second globally, measured\
description: "Authorized number of calls per window globally, measured\
\ on 10 seconds"
anonymousReporting:
type: "boolean"
Expand Down Expand Up @@ -3325,7 +3325,7 @@ spec:
perIpThrottlingQuota:
type: "integer"
format: "int64"
description: "Authorized number of calls per second globally per IP\
description: "Authorized number of calls per window globally per IP\
\ address, measured on 10 seconds"
useCircuitBreakers:
type: "boolean"
Expand Down Expand Up @@ -3501,7 +3501,7 @@ spec:
throttlingQuota:
type: "integer"
format: "int64"
description: "Authorized number of calls per second, measured on 10\
description: "Authorized number of calls per window, measured on 10\
\ seconds"
constrainedServicesOnly:
type: "boolean"
Expand Down
18 changes: 9 additions & 9 deletions otoroshi/app/controllers/SwaggerController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -896,7 +896,7 @@ class SwaggerController(cc: ControllerComponents, assetsBuilder: AssetsBuilder)(
"clientName" -> SimpleStringType ~~> "The name of the api key, for humans ;-)",
"authorizedEntities" -> SimpleArrayType ~~> "The group/service ids (prefixed by group_ or service_ on which the key is authorized",
"enabled" -> SimpleBooleanType ~~> "Whether or not the key is enabled. If disabled, resources won't be available to calls using this key",
"throttlingQuota" -> SimpleLongType ~~> "Authorized number of calls per second, measured on 10 seconds",
"throttlingQuota" -> SimpleLongType ~~> "Authorized number of calls per window, measured on 10 seconds",
"dailyQuota" -> SimpleLongType ~~> "Authorized number of calls per day",
"monthlyQuota" -> SimpleLongType ~~> "Authorized number of calls per month",
"metadata" -> SimpleObjectType ~~> "Bunch of metadata for the key"
Expand Down Expand Up @@ -990,8 +990,8 @@ class SwaggerController(cc: ControllerComponents, assetsBuilder: AssetsBuilder)(
"apiReadOnly" -> SimpleBooleanType ~~> "If enabled, Admin API won't be able to write/update/delete entities",
"u2fLoginOnly" -> SimpleBooleanType ~~> "If enabled, login to backoffice through Auth0 will be disabled",
"ipFiltering" -> Ref("IpFiltering"),
"throttlingQuota" -> SimpleLongType ~~> "Authorized number of calls per second globally, measured on 10 seconds",
"perIpThrottlingQuota" -> SimpleLongType ~~> "Authorized number of calls per second globally per IP address, measured on 10 seconds",
"throttlingQuota" -> SimpleLongType ~~> "Authorized number of calls per window globally",
"perIpThrottlingQuota" -> SimpleLongType ~~> "Authorized number of calls per window globally per IP address",
"elasticWritesConfigs" -> ArrayOf(Ref("ElasticConfig")) ~~> "Configs. for Elastic writes",
"elasticReadsConfig" -> Ref("ElasticConfig") ~~> "Config. for elastic reads",
"analyticsWebhooks" -> ArrayOf(Ref("Webhook")) ~~> "Webhook that will receive all internal Otoroshi events",
Expand Down Expand Up @@ -1176,9 +1176,9 @@ class SwaggerController(cc: ControllerComponents, assetsBuilder: AssetsBuilder)(
"description" -> "Quotas state for an api key on a service group",
"type" -> "object",
"required" -> Json.arr(
"authorizedCallsPerSec",
"currentCallsPerSec",
"remainingCallsPerSec",
"authorizedCallsPerWindow",
"throttlingCallsPerWindow",
"remainingCallsPerWindow",
"authorizedCallsPerDay",
"currentCallsPerDay",
"remainingCallsPerDay",
Expand All @@ -1187,9 +1187,9 @@ class SwaggerController(cc: ControllerComponents, assetsBuilder: AssetsBuilder)(
"remainingCallsPerMonth"
),
"properties" -> Json.obj(
"authorizedCallsPerSec" -> SimpleLongType ~~> "The number of authorized calls per second",
"currentCallsPerSec" -> SimpleLongType ~~> "The current number of calls per second",
"remainingCallsPerSec" -> SimpleLongType ~~> "The remaining number of calls per second",
"authorizedCallsPerWindow" -> SimpleLongType ~~> "The number of authorized calls per window",
"throttlingCallsPerWindow" -> SimpleLongType ~~> "The current number of calls per window",
"remainingCallsPerWindow" -> SimpleLongType ~~> "The remaining number of calls per window",
"authorizedCallsPerDay" -> SimpleLongType ~~> "The number of authorized calls per day",
"currentCallsPerDay" -> SimpleLongType ~~> "The current number of calls per day",
"remainingCallsPerDay" -> SimpleLongType ~~> "The remaining number of calls per day",
Expand Down
2 changes: 1 addition & 1 deletion otoroshi/app/gateway/generic.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1279,7 +1279,7 @@ object ReverseProxyHelper {
val quota = maybeQuota.getOrElse(globalConfig.perIpThrottlingQuota)
val (restrictionsNotPassing, restrictionsResponse) =
descriptor.restrictions.handleRestrictions(descriptor.id, descriptor.some, None, req, attrs)
if (secCalls > (quota * 10L)) {
if (secCalls > quota) {
errorResult(TooManyRequests, "[IP] You performed too much requests", "errors.too.much.requests")
} else {
if (!isSecured && descriptor.forceHttps) {
Expand Down
52 changes: 38 additions & 14 deletions otoroshi/app/models/apikey.scala
Original file line number Diff line number Diff line change
Expand Up @@ -45,28 +45,52 @@ import scala.jdk.CollectionConverters.asScalaBufferConverter
import scala.util.{Failure, Success, Try}

case class RemainingQuotas(
// secCalls: Long = RemainingQuotas.MaxValue,
// secCallsRemaining: Long = RemainingQuotas.MaxValue,
// dailyCalls: Long = RemainingQuotas.MaxValue,
// dailyCallsRemaining: Long = RemainingQuotas.MaxValue,
// monthlyCalls: Long = RemainingQuotas.MaxValue,
// monthlyCallsRemaining: Long = RemainingQuotas.MaxValue
authorizedCallsPerSec: Long = RemainingQuotas.MaxValue,
currentCallsPerSec: Long = RemainingQuotas.MaxValue,
remainingCallsPerSec: Long = RemainingQuotas.MaxValue,
authorizedCallsPerWindow: Long = RemainingQuotas.MaxValue,
throttlingCallsPerWindow: Long = RemainingQuotas.MaxValue,
remainingCallsPerWindow: Long = RemainingQuotas.MaxValue,
authorizedCallsPerDay: Long = RemainingQuotas.MaxValue,
currentCallsPerDay: Long = RemainingQuotas.MaxValue,
remainingCallsPerDay: Long = RemainingQuotas.MaxValue,
authorizedCallsPerMonth: Long = RemainingQuotas.MaxValue,
currentCallsPerMonth: Long = RemainingQuotas.MaxValue,
remainingCallsPerMonth: Long = RemainingQuotas.MaxValue
) {
def toJson: JsObject = RemainingQuotas.fmt.writes(this)
def toJson: JsObject = RemainingQuotas.fmt.writes(this).as[JsObject]
}

object RemainingQuotas {
val MaxValue: Long = 10000000L
implicit val fmt = Json.format[RemainingQuotas]
implicit val fmt = new Format[RemainingQuotas] {

override def reads(json: JsValue): JsResult[RemainingQuotas] = Try {
RemainingQuotas(
authorizedCallsPerWindow = json.select("authorizedCallsPerWindow").asOpt[Long].getOrElse(json.select("authorizedCallsPerSec").asLong),
throttlingCallsPerWindow = json.select("throttlingCallsPerWindow").asOpt[Long].getOrElse(json.select("currentCallsPerSec").asLong),
remainingCallsPerWindow = json.select("remainingCallsPerWindow").asOpt[Long].getOrElse(json.select("authorizedCallsPerSec").asLong),
authorizedCallsPerDay = json.select("authorizedCallsPerDay").asLong,
currentCallsPerDay = json.select("currentCallsPerDay").asLong,
remainingCallsPerDay = json.select("remainingCallsPerDay").asLong,
authorizedCallsPerMonth = json.select("authorizedCallsPerMonth").asLong,
currentCallsPerMonth = json.select("currentCallsPerMonth").asLong,
remainingCallsPerMonth = json.select("remainingCallsPerMonth").asLong,
)
} match {
case Failure(e) => JsError(e.getMessage)
case Success(e) => JsSuccess(e)
}

override def writes(o: RemainingQuotas): JsValue = Json.obj(
"authorizedCallsPerWindow" -> o.authorizedCallsPerWindow,
"throttlingCallsPerWindow" -> o.throttlingCallsPerWindow,
"remainingCallsPerWindow" -> o.remainingCallsPerWindow,
"authorizedCallsPerDay" -> o.authorizedCallsPerDay,
"currentCallsPerDay" -> o.currentCallsPerDay,
"remainingCallsPerDay" -> o.remainingCallsPerDay,
"authorizedCallsPerMonth" -> o.authorizedCallsPerMonth,
"currentCallsPerMonth" -> o.currentCallsPerMonth,
"remainingCallsPerMonth" -> o.remainingCallsPerMonth,
)
}
}

case class ApiKeyRotation(
Expand Down Expand Up @@ -244,7 +268,7 @@ case class ApiKey(
quotas <- env.datastores.apiKeyDataStore.remainingQuotas(this)
rotation <- env.datastores.apiKeyDataStore.keyRotation(this)
} yield {
val within = (quotas.currentCallsPerSec <= (throttlingQuota * env.throttlingWindow)) &&
val within = quotas.remainingCallsPerWindow > 0 &&
(quotas.currentCallsPerDay < dailyQuota) &&
(quotas.currentCallsPerMonth < monthlyQuota)
(within, rotation, quotas)
Expand Down Expand Up @@ -1906,9 +1930,9 @@ object ApiKeyHelper {
.build
Try(verifier.verify(jwt))
.filter { token =>
val aud = token.getAudience.asScala.headOption.filter(v =>
val aud = Option(token.getAudience).flatMap(_.asScala.headOption.filter(v =>
v.startsWith("http://") || v.startsWith("https://")
)
))
if (aud.isDefined) {
val currentUrl = req.theUrl
val audience = aud.get
Expand Down
2 changes: 1 addition & 1 deletion otoroshi/app/models/config.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1026,7 +1026,7 @@ object GlobalConfig {
}

trait GlobalConfigDataStore extends BasicStore[GlobalConfig] {
def incrementCallsForIpAddressWithTTL(ip: String, ttl: Int = 10)(implicit ec: ExecutionContext): Future[Long]
def incrementCallsForIpAddress(ip: String)(implicit ec: ExecutionContext): Future[Long]
def quotaForIpAddress(ip: String)(implicit ec: ExecutionContext): Future[Option[Long]]
def isOtoroshiEmpty()(implicit ec: ExecutionContext): Future[Boolean]
def withinThrottlingQuota()(implicit ec: ExecutionContext, env: Env): Future[Boolean]
Expand Down
6 changes: 3 additions & 3 deletions otoroshi/app/next/plugins/context.scala
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,9 @@ class ContextValidation extends NgAccessValidator {
| "otoroshi.core.RequestWebsocket" : false,
| "otoroshi.core.RequestCounterOut" : 0,
| "otoroshi.core.RemainingQuotas" : {
| "authorizedCallsPerSec" : 10000000,
| "currentCallsPerSec" : 0,
| "remainingCallsPerSec" : 10000000,
| "authorizedCallsPerWindow" : 10000000,
| "throttlingCallsPerWindow" : 0,
| "remainingCallsPerWindow" : 10000000,
| "authorizedCallsPerDay" : 10000000,
| "currentCallsPerDay" : 2,
| "remainingCallsPerDay" : 9999998,
Expand Down
8 changes: 4 additions & 4 deletions otoroshi/app/next/plugins/quotas.scala
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class GlobalPerIpAddressThrottling extends NgAccessValidator {
): Future[NgAccess] = {
val globalConfig = env.datastores.globalConfigDataStore.latest()
val quota = quotas.maybeQuota.getOrElse(globalConfig.perIpThrottlingQuota)
if (quotas.secCalls > (quota * 10L)) {
if (quotas.secCalls > quota) {
errorResult(ctx, Results.TooManyRequests, "[IP] You performed too much requests", "errors.too.much.requests")
} else {
NgAccess.NgAllowed.vfuture
Expand Down Expand Up @@ -266,7 +266,7 @@ class NgServiceQuotas extends NgAccessValidator {
)(implicit ec: ExecutionContext, env: Env): Future[Boolean] =
env.datastores.rawDataStore
.get(throttlingKey(route.id))
.map(_.map(_.utf8String.toLong).getOrElse(0L) <= (qconf.throttlingQuota * env.throttlingWindow))
.map(_.map(_.utf8String.toLong).getOrElse(0L) <= qconf.throttlingQuota)

private def withinDailyQuota(route: NgRoute, qconf: NgServiceQuotasConfig)(implicit
ec: ExecutionContext,
Expand Down Expand Up @@ -559,7 +559,7 @@ object NgCustomThrottling {
for {
secCalls <- env.datastores.rawDataStore.incrby(throttlingKey(expr, group), increment)
secTtl <- env.datastores.rawDataStore.pttl(throttlingKey(expr, group)).filter(_ > -1).recoverWith { case _ =>
env.datastores.rawDataStore.pexpire(throttlingKey(expr, group), ttl) // env.throttlingWindow * 1000)
env.datastores.rawDataStore.pexpire(throttlingKey(expr, group), ttl) // env.throttlingWindow * 1000
}
} yield ()
}
Expand Down Expand Up @@ -593,7 +593,7 @@ class NgCustomThrottling extends NgAccessValidator {
)(implicit ec: ExecutionContext, env: Env): Future[Boolean] = {
env.datastores.rawDataStore
.get(NgCustomThrottling.throttlingKey(qconf.computeExpression(ctx, env), qconf.computeGroup(ctx, env)))
.map(_.map(_.utf8String.toLong).getOrElse(0L) <= (qconf.throttlingQuota * env.throttlingWindow))
.map(_.map(_.utf8String.toLong).getOrElse(0L) <= qconf.throttlingQuota)
}

def forbidden(ctx: NgAccessContext)(implicit env: Env, ec: ExecutionContext): Future[NgAccess] = {
Expand Down
4 changes: 3 additions & 1 deletion otoroshi/app/next/proxy/engine.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2017,8 +2017,10 @@ class ProxyEngine() extends RequestHandler {
// quotasValidationFor increments calls for ip address
FEither(env.datastores.globalConfigDataStore.quotasValidationFor(remoteAddress).flatMap { r =>
val (within, secCalls, maybeQuota) = r
// println(s"maybe quota - $maybeQuota")
val quota = maybeQuota.getOrElse(globalConfig.perIpThrottlingQuota)
if (secCalls > (quota * 10L)) {
// if (secCalls > (quota * 10L)) {
if (secCalls > quota) {
errorResult(Results.TooManyRequests, "[IP] You performed too much requests", "errors.too.much.requests")
} else {
if (!within) {
Expand Down
12 changes: 6 additions & 6 deletions otoroshi/app/openapi/openapi-cfg.json
Original file line number Diff line number Diff line change
Expand Up @@ -1594,7 +1594,7 @@
"otoroshi.models.ApiKey.restrictions": "Apikey restrictions settings",
"otoroshi.models.ApiKey.rotation": "Apikey rotation settings",
"otoroshi.models.ApiKey.tags": "Apikey tags",
"otoroshi.models.ApiKey.throttlingQuota": "Authorized number of calls per second, measured on 10 seconds",
"otoroshi.models.ApiKey.throttlingQuota": "Authorized number of calls per window",
"otoroshi.models.ApiKey.validUntil": "Date until when the apikey is valid",
"otoroshi.models.ApiKeyConstraints.basicAuth": "Settings to extract basic auth style apikey",
"otoroshi.models.ApiKeyConstraints.clientIdAuth": "Settings to extract client_id only apikey",
Expand Down Expand Up @@ -1798,7 +1798,7 @@
"otoroshi.models.GlobalConfig.metadata": "Entity metadata",
"otoroshi.models.GlobalConfig.middleFingers": "Use middle finger emoji as a response character for endless HTTP responses",
"otoroshi.models.GlobalConfig.otoroshiId": "Unique id for this otoroshi instance",
"otoroshi.models.GlobalConfig.perIpThrottlingQuota": "Authorized number of calls per second globally per IP address, measured on 10 seconds",
"otoroshi.models.GlobalConfig.perIpThrottlingQuota": "Authorized number of calls per window globally per IP address",
"otoroshi.models.GlobalConfig.plugins": "global plugins settings",
"otoroshi.models.GlobalConfig.proxies": "Web proxies settings",
"otoroshi.models.GlobalConfig.quotasSettings": "Settings to generate alert when an apikey almost exceeded or exceeded its quotas",
Expand All @@ -1808,7 +1808,7 @@
"otoroshi.models.GlobalConfig.streamEntityOnly": "HTTP will be streamed only. Doesn't work with old browsers",
"otoroshi.models.GlobalConfig.tags": "Entity tags",
"otoroshi.models.GlobalConfig.templates": "The otoroshi default templates for entities",
"otoroshi.models.GlobalConfig.throttlingQuota": "Authorized number of calls per second globally, measured on 10 seconds",
"otoroshi.models.GlobalConfig.throttlingQuota": "Authorized number of calls per window globally",
"otoroshi.models.GlobalConfig.tlsSettings": "TLS settings",
"otoroshi.models.GlobalConfig.trustXForwarded": "Use X-Forwarded-* headers for routing",
"otoroshi.models.GlobalConfig.u2fLoginOnly": "If enabled, login to backoffice through Auth0 will be disabled",
Expand Down Expand Up @@ -1957,13 +1957,13 @@
"otoroshi.models.RegionMatch.region": "Region name",
"otoroshi.models.RemainingQuotas.authorizedCallsPerDay": "Number of authorized call per day",
"otoroshi.models.RemainingQuotas.authorizedCallsPerMonth": "Number of authorized call per month",
"otoroshi.models.RemainingQuotas.authorizedCallsPerSec": "Number of authorized call per second",
"otoroshi.models.RemainingQuotas.authorizedCallsPerWindow": "Number of authorized call per window",
"otoroshi.models.RemainingQuotas.currentCallsPerDay": "Current number of call per day",
"otoroshi.models.RemainingQuotas.currentCallsPerMonth": "Current number of call per month",
"otoroshi.models.RemainingQuotas.currentCallsPerSec": "Current number of call per second",
"otoroshi.models.RemainingQuotas.throttlingCallsPerWindow": "Current number of call per window",
"otoroshi.models.RemainingQuotas.remainingCallsPerDay": "Remaining number of call per day",
"otoroshi.models.RemainingQuotas.remainingCallsPerMonth": "Remaining number of call per month",
"otoroshi.models.RemainingQuotas.remainingCallsPerSec": "Remaining number of call per second",
"otoroshi.models.RemainingQuotas.remainingCallsPerWindow": "Remaining number of call per window",
"otoroshi.models.RestrictionPath.method": "Method of the http request",
"otoroshi.models.RestrictionPath.path": "Path of the http request",
"otoroshi.models.Restrictions.allowLast": "Evalute allowed paths after everything else",
Expand Down
Loading

0 comments on commit 41c8973

Please sign in to comment.