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.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)

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-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" }

View File

@ -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 }
}
}
}

View File

@ -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<AnnouncementService>()
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<APIAnnouncement>())
}
call.respond(announcementService.all(channel))
}
post("{id}/archive") {
val id: Int by call.parameters
val archivedAt = call.receiveNullable<APIAnnouncementArchivedAt>()?.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<APIAnnouncement>())
}
patch("{id}") {
val id: Int by call.parameters
val announcement = call.receive<APIAnnouncement>()
post("{id}/archive") {
val id: Int by call.parameters
val archivedAt = call.receiveNullable<APIAnnouncementArchivedAt>()?.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<APIAnnouncement>()
announcementService.update(id, announcement)
}
delete("{id}") {
val id: Int by call.parameters
announcementService.delete(id)
announcementService.delete(id)
}
}
}
}

View File

@ -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<ApiService>()
val authService = get<AuthService>()
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")
}
}

View File

@ -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<OldApiService>()
route(Regex("/(v2|tools|contributors).*")) {
handle {
oldApiService.proxy(call)
rateLimit(RateLimitName("weak")) {
route(Regex("/(v2|tools|contributors).*")) {
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 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<PatchesService>()
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() }
}
}
}
}