feat: Add rate limiting to routes

This commit is contained in:
oSumAtrIX 2024-06-06 23:59:06 +02:00
parent b9671703be
commit 80403f7130
No known key found for this signature in database
GPG Key ID: A9B3094ACDB604B4
7 changed files with 111 additions and 78 deletions

View File

@ -62,9 +62,9 @@ dependencies {
implementation(libs.ktor.server.auth.jwt) implementation(libs.ktor.server.auth.jwt)
implementation(libs.ktor.server.cors) implementation(libs.ktor.server.cors)
implementation(libs.ktor.server.caching.headers) implementation(libs.ktor.server.caching.headers)
implementation(libs.ktor.server.rate.limit)
implementation(libs.ktor.server.host.common) implementation(libs.ktor.server.host.common)
implementation(libs.ktor.server.jetty) implementation(libs.ktor.server.jetty)
implementation(libs.ktor.server.conditional.headers)
implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.koin.ktor) implementation(libs.koin.ktor)
implementation(libs.h2) implementation(libs.h2)

View File

@ -20,13 +20,13 @@ ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp" }
ktor-client-resources = { module = "io.ktor:ktor-client-resources" } ktor-client-resources = { module = "io.ktor:ktor-client-resources" }
ktor-client-auth = { module = "io.ktor:ktor-client-auth" } ktor-client-auth = { module = "io.ktor:ktor-client-auth" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation" } 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-core = { module = "io.ktor:ktor-server-core" }
ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negotiation" } ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negotiation" }
ktor-server-auth = { module = "io.ktor:ktor-server-auth" } ktor-server-auth = { module = "io.ktor:ktor-server-auth" }
ktor-server-auth-jwt = { module = "io.ktor:ktor-server-auth-jwt" } ktor-server-auth-jwt = { module = "io.ktor:ktor-server-auth-jwt" }
ktor-server-cors = { module = "io.ktor:ktor-server-cors" } ktor-server-cors = { module = "io.ktor:ktor-server-cors" }
ktor-server-caching-headers = { module = "io.ktor:ktor-server-caching-headers" } 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-host-common = { module = "io.ktor:ktor-server-host-common" }
ktor-server-jetty = { module = "io.ktor:ktor-server-jetty" } ktor-server-jetty = { module = "io.ktor:ktor-server-jetty" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json" }

View File

@ -3,15 +3,15 @@ package app.revanced.api.configuration
import io.ktor.http.* import io.ktor.http.*
import io.ktor.http.content.* import io.ktor.http.content.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.plugins.*
import io.ktor.server.plugins.cachingheaders.* import io.ktor.server.plugins.cachingheaders.*
import io.ktor.server.plugins.conditionalheaders.*
import io.ktor.server.plugins.cors.routing.* import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.plugins.ratelimit.*
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
fun Application.configureHTTP( fun Application.configureHTTP(
allowedHost: String, allowedHost: String,
) { ) {
install(ConditionalHeaders)
install(CORS) { install(CORS) {
allowMethod(HttpMethod.Options) allowMethod(HttpMethod.Options)
allowMethod(HttpMethod.Put) allowMethod(HttpMethod.Put)
@ -23,4 +23,14 @@ fun Application.configureHTTP(
install(CachingHeaders) { install(CachingHeaders) {
options { _, _ -> CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 5.minutes.inWholeSeconds.toInt())) } 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 }
}
}
} }

View File

@ -6,6 +6,7 @@ import app.revanced.api.configuration.schema.APIAnnouncementArchivedAt
import app.revanced.api.configuration.services.AnnouncementService import app.revanced.api.configuration.services.AnnouncementService
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.auth.* import io.ktor.server.auth.*
import io.ktor.server.plugins.ratelimit.*
import io.ktor.server.request.* import io.ktor.server.request.*
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
@ -15,69 +16,78 @@ import org.koin.ktor.ext.get as koinGet
internal fun Route.announcementsRoute() = route("announcements") { internal fun Route.announcementsRoute() = route("announcements") {
val announcementService = koinGet<AnnouncementService>() val announcementService = koinGet<AnnouncementService>()
route("{channel}/latest") { rateLimit(RateLimitName("weak")) {
get("id") { 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 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 { get {
val channel: String by call.parameters call.respond(announcementService.all())
call.respondOrNotFound(announcementService.latest(channel))
} }
} }
get("{channel}") { rateLimit(RateLimitName("strong")) {
val channel: String by call.parameters authenticate("jwt") {
post {
announcementService.new(call.receive<APIAnnouncement>())
}
call.respond(announcementService.all(channel)) post("{id}/archive") {
} val id: Int by call.parameters
val archivedAt = call.receiveNullable<APIAnnouncementArchivedAt>()?.archivedAt
route("latest") { announcementService.archive(id, archivedAt)
get("id") { }
call.respondOrNotFound(announcementService.latestId())
}
get { post("{id}/unarchive") {
call.respondOrNotFound(announcementService.latest()) val id: Int by call.parameters
}
}
get { announcementService.unarchive(id)
call.respond(announcementService.all()) }
}
authenticate("jwt") { patch("{id}") {
post { val id: Int by call.parameters
announcementService.new(call.receive<APIAnnouncement>()) val announcement = call.receive<APIAnnouncement>()
}
post("{id}/archive") { announcementService.update(id, announcement)
val id: Int by call.parameters }
val archivedAt = call.receiveNullable<APIAnnouncementArchivedAt>()?.archivedAt
announcementService.archive(id, archivedAt) delete("{id}") {
} val id: Int by call.parameters
post("{id}/unarchive") { announcementService.delete(id)
val id: Int by call.parameters }
announcementService.unarchive(id)
}
patch("{id}") {
val id: Int by call.parameters
val announcement = call.receive<APIAnnouncement>()
announcementService.update(id, announcement)
}
delete("{id}") {
val id: Int by call.parameters
announcementService.delete(id)
} }
} }
} }

View File

@ -7,6 +7,7 @@ import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.auth.* import io.ktor.server.auth.*
import io.ktor.server.http.content.* import io.ktor.server.http.content.*
import io.ktor.server.plugins.ratelimit.*
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
import org.koin.ktor.ext.get import org.koin.ktor.ext.get
@ -15,12 +16,20 @@ internal fun Route.rootRoute() {
val apiService = get<ApiService>() val apiService = get<ApiService>()
val authService = get<AuthService>() val authService = get<AuthService>()
get("contributors") { rateLimit(RateLimitName("strong")) {
call.respond(apiService.contributors()) authenticate("basic") {
} get("token") {
call.respond(authService.newToken())
}
}
get("team") { get("contributors") {
call.respond(apiService.team()) call.respond(apiService.contributors())
}
get("team") {
call.respond(apiService.team())
}
} }
route("ping") { route("ping") {
@ -29,18 +38,14 @@ internal fun Route.rootRoute() {
} }
} }
get("backend/rate_limit") { rateLimit(RateLimitName("weak")) {
call.respondOrNotFound(apiService.rateLimit()) get("backend/rate_limit") {
} call.respondOrNotFound(apiService.rateLimit())
}
authenticate("basic") { staticResources("/", "/app/revanced/api/static") {
get("token") { contentType { ContentType.Application.Json }
call.respond(authService.newToken()) extensions("json")
} }
} }
staticResources("/", "/app/revanced/api/static") {
contentType { ContentType.Application.Json }
extensions("json")
}
} }

View File

@ -2,15 +2,18 @@ package app.revanced.api.configuration.routing.routes
import app.revanced.api.configuration.services.OldApiService import app.revanced.api.configuration.services.OldApiService
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.plugins.ratelimit.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
import org.koin.ktor.ext.get import org.koin.ktor.ext.get
internal fun Route.oldApiRoute() { internal fun Route.oldApiRoute() {
val oldApiService = get<OldApiService>() val oldApiService = get<OldApiService>()
route(Regex("/(v2|tools|contributors).*")) { rateLimit(RateLimitName("weak")) {
handle { route(Regex("/(v2|tools|contributors).*")) {
oldApiService.proxy(call) handle {
oldApiService.proxy(call)
}
} }
} }
} }

View File

@ -3,6 +3,7 @@ package app.revanced.api.configuration.routing.routes
import app.revanced.api.configuration.services.PatchesService import app.revanced.api.configuration.services.PatchesService
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.plugins.ratelimit.*
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
import org.koin.ktor.ext.get as koinGet import org.koin.ktor.ext.get as koinGet
@ -11,16 +12,20 @@ internal fun Route.patchesRoute() = route("patches") {
val patchesService = koinGet<PatchesService>() val patchesService = koinGet<PatchesService>()
route("latest") { route("latest") {
get { rateLimit(RateLimitName("weak")) {
call.respond(patchesService.latestRelease()) get {
call.respond(patchesService.latestRelease())
}
get("version") {
call.respond(patchesService.latestVersion())
}
} }
get("version") { rateLimit(RateLimitName("strong")) {
call.respond(patchesService.latestVersion()) get("list") {
} call.respondBytes(ContentType.Application.Json) { patchesService.list() }
}
get("list") {
call.respondBytes(ContentType.Application.Json) { patchesService.list() }
} }
} }
} }