From 80403f7130cd48e68e802ee3111760256e49c77d Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 6 Jun 2024 23:59:06 +0200 Subject: [PATCH] feat: Add rate limiting to routes --- build.gradle.kts | 2 +- gradle/libs.versions.toml | 2 +- .../app/revanced/api/configuration/HTTP.kt | 14 ++- .../routing/routes/Announcements.kt | 104 ++++++++++-------- .../configuration/routing/routes/ApiRoute.kt | 37 ++++--- .../configuration/routing/routes/OldApi.kt | 9 +- .../routing/routes/PatchesRoute.kt | 21 ++-- 7 files changed, 111 insertions(+), 78 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 7e1f877..8c3226d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -62,9 +62,9 @@ dependencies { implementation(libs.ktor.server.auth.jwt) implementation(libs.ktor.server.cors) implementation(libs.ktor.server.caching.headers) + implementation(libs.ktor.server.rate.limit) implementation(libs.ktor.server.host.common) implementation(libs.ktor.server.jetty) - implementation(libs.ktor.server.conditional.headers) implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.koin.ktor) implementation(libs.h2) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d800cbf..3d38a95 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,13 +20,13 @@ ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp" } ktor-client-resources = { module = "io.ktor:ktor-client-resources" } ktor-client-auth = { module = "io.ktor:ktor-client-auth" } ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation" } -ktor-server-conditional-headers = { module = "io.ktor:ktor-server-conditional-headers" } ktor-server-core = { module = "io.ktor:ktor-server-core" } ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negotiation" } ktor-server-auth = { module = "io.ktor:ktor-server-auth" } ktor-server-auth-jwt = { module = "io.ktor:ktor-server-auth-jwt" } ktor-server-cors = { module = "io.ktor:ktor-server-cors" } ktor-server-caching-headers = { module = "io.ktor:ktor-server-caching-headers" } +ktor-server-rate-limit = { module = "io.ktor:ktor-server-rate-limit" } ktor-server-host-common = { module = "io.ktor:ktor-server-host-common" } ktor-server-jetty = { module = "io.ktor:ktor-server-jetty" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json" } diff --git a/src/main/kotlin/app/revanced/api/configuration/HTTP.kt b/src/main/kotlin/app/revanced/api/configuration/HTTP.kt index 336b100..d61350d 100644 --- a/src/main/kotlin/app/revanced/api/configuration/HTTP.kt +++ b/src/main/kotlin/app/revanced/api/configuration/HTTP.kt @@ -3,15 +3,15 @@ package app.revanced.api.configuration import io.ktor.http.* import io.ktor.http.content.* import io.ktor.server.application.* +import io.ktor.server.plugins.* import io.ktor.server.plugins.cachingheaders.* -import io.ktor.server.plugins.conditionalheaders.* import io.ktor.server.plugins.cors.routing.* +import io.ktor.server.plugins.ratelimit.* import kotlin.time.Duration.Companion.minutes fun Application.configureHTTP( allowedHost: String, ) { - install(ConditionalHeaders) install(CORS) { allowMethod(HttpMethod.Options) allowMethod(HttpMethod.Put) @@ -23,4 +23,14 @@ fun Application.configureHTTP( install(CachingHeaders) { options { _, _ -> CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 5.minutes.inWholeSeconds.toInt())) } } + install(RateLimit) { + register(RateLimitName("weak")) { + rateLimiter(limit = 30, refillPeriod = 2.minutes) + requestKey { it.request.origin.remoteAddress } + } + register(RateLimitName("strong")) { + rateLimiter(limit = 5, refillPeriod = 1.minutes) + requestKey { it.request.origin.remoteHost } + } + } } diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt b/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt index 0e96856..2901942 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routing/routes/Announcements.kt @@ -6,6 +6,7 @@ import app.revanced.api.configuration.schema.APIAnnouncementArchivedAt import app.revanced.api.configuration.services.AnnouncementService import io.ktor.server.application.* import io.ktor.server.auth.* +import io.ktor.server.plugins.ratelimit.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* @@ -15,69 +16,78 @@ import org.koin.ktor.ext.get as koinGet internal fun Route.announcementsRoute() = route("announcements") { val announcementService = koinGet() - route("{channel}/latest") { - get("id") { + rateLimit(RateLimitName("weak")) { + route("{channel}/latest") { + get("id") { + val channel: String by call.parameters + + call.respondOrNotFound(announcementService.latestId(channel)) + } + + get { + val channel: String by call.parameters + + call.respondOrNotFound(announcementService.latest(channel)) + } + } + } + + rateLimit(RateLimitName("strong")) { + get("{channel}") { val channel: String by call.parameters - call.respondOrNotFound(announcementService.latestId(channel)) + call.respond(announcementService.all(channel)) } + } + rateLimit(RateLimitName("strong")) { + route("latest") { + get("id") { + call.respondOrNotFound(announcementService.latestId()) + } + get { + call.respondOrNotFound(announcementService.latest()) + } + } + } + + rateLimit(RateLimitName("strong")) { get { - val channel: String by call.parameters - - call.respondOrNotFound(announcementService.latest(channel)) + call.respond(announcementService.all()) } } - get("{channel}") { - val channel: String by call.parameters + rateLimit(RateLimitName("strong")) { + authenticate("jwt") { + post { + announcementService.new(call.receive()) + } - call.respond(announcementService.all(channel)) - } + post("{id}/archive") { + val id: Int by call.parameters + val archivedAt = call.receiveNullable()?.archivedAt - route("latest") { - get("id") { - call.respondOrNotFound(announcementService.latestId()) - } + announcementService.archive(id, archivedAt) + } - get { - call.respondOrNotFound(announcementService.latest()) - } - } + post("{id}/unarchive") { + val id: Int by call.parameters - get { - call.respond(announcementService.all()) - } + announcementService.unarchive(id) + } - authenticate("jwt") { - post { - announcementService.new(call.receive()) - } + patch("{id}") { + val id: Int by call.parameters + val announcement = call.receive() - post("{id}/archive") { - val id: Int by call.parameters - val archivedAt = call.receiveNullable()?.archivedAt + announcementService.update(id, announcement) + } - announcementService.archive(id, archivedAt) - } + delete("{id}") { + val id: Int by call.parameters - post("{id}/unarchive") { - val id: Int by call.parameters - - announcementService.unarchive(id) - } - - patch("{id}") { - val id: Int by call.parameters - val announcement = call.receive() - - announcementService.update(id, announcement) - } - - delete("{id}") { - val id: Int by call.parameters - - announcementService.delete(id) + announcementService.delete(id) + } } } } diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/routes/ApiRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routing/routes/ApiRoute.kt index 143e14b..2e1ede3 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routing/routes/ApiRoute.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routing/routes/ApiRoute.kt @@ -7,6 +7,7 @@ import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.server.http.content.* +import io.ktor.server.plugins.ratelimit.* import io.ktor.server.response.* import io.ktor.server.routing.* import org.koin.ktor.ext.get @@ -15,12 +16,20 @@ internal fun Route.rootRoute() { val apiService = get() val authService = get() - get("contributors") { - call.respond(apiService.contributors()) - } + rateLimit(RateLimitName("strong")) { + authenticate("basic") { + get("token") { + call.respond(authService.newToken()) + } + } - get("team") { - call.respond(apiService.team()) + get("contributors") { + call.respond(apiService.contributors()) + } + + get("team") { + call.respond(apiService.team()) + } } route("ping") { @@ -29,18 +38,14 @@ internal fun Route.rootRoute() { } } - get("backend/rate_limit") { - call.respondOrNotFound(apiService.rateLimit()) - } + rateLimit(RateLimitName("weak")) { + get("backend/rate_limit") { + call.respondOrNotFound(apiService.rateLimit()) + } - authenticate("basic") { - get("token") { - call.respond(authService.newToken()) + staticResources("/", "/app/revanced/api/static") { + contentType { ContentType.Application.Json } + extensions("json") } } - - staticResources("/", "/app/revanced/api/static") { - contentType { ContentType.Application.Json } - extensions("json") - } } diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/routes/OldApi.kt b/src/main/kotlin/app/revanced/api/configuration/routing/routes/OldApi.kt index fe4f5be..f214f26 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routing/routes/OldApi.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routing/routes/OldApi.kt @@ -2,15 +2,18 @@ package app.revanced.api.configuration.routing.routes import app.revanced.api.configuration.services.OldApiService import io.ktor.server.application.* +import io.ktor.server.plugins.ratelimit.* import io.ktor.server.routing.* import org.koin.ktor.ext.get internal fun Route.oldApiRoute() { val oldApiService = get() - route(Regex("/(v2|tools|contributors).*")) { - handle { - oldApiService.proxy(call) + rateLimit(RateLimitName("weak")) { + route(Regex("/(v2|tools|contributors).*")) { + handle { + oldApiService.proxy(call) + } } } } diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/routes/PatchesRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routing/routes/PatchesRoute.kt index 6567c48..ac3445f 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routing/routes/PatchesRoute.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routing/routes/PatchesRoute.kt @@ -3,6 +3,7 @@ package app.revanced.api.configuration.routing.routes import app.revanced.api.configuration.services.PatchesService import io.ktor.http.* import io.ktor.server.application.* +import io.ktor.server.plugins.ratelimit.* import io.ktor.server.response.* import io.ktor.server.routing.* import org.koin.ktor.ext.get as koinGet @@ -11,16 +12,20 @@ internal fun Route.patchesRoute() = route("patches") { val patchesService = koinGet() route("latest") { - get { - call.respond(patchesService.latestRelease()) + rateLimit(RateLimitName("weak")) { + get { + call.respond(patchesService.latestRelease()) + } + + get("version") { + call.respond(patchesService.latestVersion()) + } } - get("version") { - call.respond(patchesService.latestVersion()) - } - - get("list") { - call.respondBytes(ContentType.Application.Json) { patchesService.list() } + rateLimit(RateLimitName("strong")) { + get("list") { + call.respondBytes(ContentType.Application.Json) { patchesService.list() } + } } } }