feat: Add proxy for old API

This commit is contained in:
oSumAtrIX 2024-06-05 04:28:36 +02:00
parent fa2f8b2f86
commit 39f54bbb32
No known key found for this signature in database
GPG Key ID: A9B3094ACDB604B4
13 changed files with 188 additions and 75 deletions

View File

@ -11,3 +11,5 @@ JWT_VALIDITY_IN_MIN=
BASIC_USERNAME= BASIC_USERNAME=
BASIC_PASSWORD= BASIC_PASSWORD=
OLD_API_URL=

View File

@ -2,8 +2,11 @@ package app.revanced.api.command
import app.revanced.api.configuration.* import app.revanced.api.configuration.*
import app.revanced.api.configuration.routing.configureRouting import app.revanced.api.configuration.routing.configureRouting
import io.ktor.server.application.*
import io.ktor.server.engine.* import io.ktor.server.engine.*
import io.ktor.server.netty.* import io.ktor.server.netty.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import picocli.CommandLine import picocli.CommandLine
@CommandLine.Command( @CommandLine.Command(

View File

@ -2,6 +2,7 @@ package app.revanced.api.configuration
import app.revanced.api.repository.AnnouncementRepository import app.revanced.api.repository.AnnouncementRepository
import app.revanced.api.repository.ConfigurationRepository import app.revanced.api.repository.ConfigurationRepository
import app.revanced.api.repository.OldApiService
import app.revanced.api.repository.backend.BackendRepository import app.revanced.api.repository.backend.BackendRepository
import app.revanced.api.repository.backend.github.GitHubBackendRepository import app.revanced.api.repository.backend.github.GitHubBackendRepository
import app.revanced.api.services.AnnouncementService import app.revanced.api.services.AnnouncementService
@ -11,14 +12,27 @@ import app.revanced.api.services.PatchesService
import com.akuleshov7.ktoml.Toml import com.akuleshov7.ktoml.Toml
import com.akuleshov7.ktoml.source.decodeFromStream import com.akuleshov7.ktoml.source.decodeFromStream
import io.github.cdimascio.dotenv.Dotenv import io.github.cdimascio.dotenv.Dotenv
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.auth.*
import io.ktor.client.plugins.auth.providers.*
import io.ktor.client.plugins.cache.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.resources.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.* import io.ktor.server.application.*
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNamingStrategy
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
import org.koin.dsl.bind import org.koin.core.parameter.parameterArrayOf
import org.koin.dsl.module import org.koin.dsl.module
import org.koin.ktor.plugin.Koin import org.koin.ktor.plugin.Koin
import java.io.File import java.io.File
@OptIn(ExperimentalSerializationApi::class)
fun Application.configureDependencies() { fun Application.configureDependencies() {
val globalModule = module { val globalModule = module {
single { single {
@ -26,6 +40,16 @@ fun Application.configureDependencies() {
.systemProperties() .systemProperties()
.load() .load()
} }
factory { params ->
val defaultRequestUri: String = params.get<String>()
val configBlock = params.getOrNull<(HttpClientConfig<OkHttpConfig>.() -> Unit)>() ?: {}
HttpClient(OkHttp) {
defaultRequest { url(defaultRequestUri) }
configBlock()
}
}
} }
val repositoryModule = module { val repositoryModule = module {
@ -40,6 +64,43 @@ fun Application.configureDependencies() {
) )
} }
single<BackendRepository> {
GitHubBackendRepository(
get {
val defaultRequestUri = "https://api.github.com"
val configBlock: HttpClientConfig<OkHttpConfig>.() -> Unit = {
install(HttpCache)
install(Resources)
install(ContentNegotiation) {
json(
Json {
ignoreUnknownKeys = true
namingStrategy = JsonNamingStrategy.SnakeCase
},
)
}
get<Dotenv>()["GITHUB_TOKEN"]?.let {
install(Auth) {
bearer {
loadTokens {
BearerTokens(
accessToken = it,
refreshToken = "", // Required dummy value
)
}
sendWithoutRequest { true }
}
}
}
}
parameterArrayOf(defaultRequestUri, configBlock)
},
)
}
single { single {
val configFilePath = get<Dotenv>()["CONFIG_FILE_PATH"] val configFilePath = get<Dotenv>()["CONFIG_FILE_PATH"]
val configFile = File(configFilePath).inputStream() val configFile = File(configFilePath).inputStream()
@ -64,10 +125,13 @@ fun Application.configureDependencies() {
AuthService(issuer, validityInMin, jwtSecret, basicUsername, basicPassword) AuthService(issuer, validityInMin, jwtSecret, basicUsername, basicPassword)
} }
single { single {
val token = get<Dotenv>()["GITHUB_TOKEN"] OldApiService(
get {
GitHubBackendRepository(token) val defaultRequestUri = get<Dotenv>()["OLD_API_URL"]
} bind BackendRepository::class parameterArrayOf(defaultRequestUri)
},
)
}
singleOf(::AnnouncementService) singleOf(::AnnouncementService)
singleOf(::PatchesService) singleOf(::PatchesService)
singleOf(::ApiService) singleOf(::ApiService)

View File

@ -1,8 +1,9 @@
package app.revanced.api.configuration.routing package app.revanced.api.configuration.routing
import app.revanced.api.configuration.routing.routes.configureAnnouncementsRoute import app.revanced.api.configuration.routing.routes.announcementsRoute
import app.revanced.api.configuration.routing.routes.configurePatchesRoute import app.revanced.api.configuration.routing.routes.oldApiRoute
import app.revanced.api.configuration.routing.routes.configureRootRoute import app.revanced.api.configuration.routing.routes.patchesRoute
import app.revanced.api.configuration.routing.routes.rootRoute
import app.revanced.api.repository.ConfigurationRepository import app.revanced.api.repository.ConfigurationRepository
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
@ -12,8 +13,11 @@ internal fun Application.configureRouting() = routing {
val configuration = get<ConfigurationRepository>() val configuration = get<ConfigurationRepository>()
route("/v${configuration.apiVersion}") { route("/v${configuration.apiVersion}") {
configureRootRoute() rootRoute()
configurePatchesRoute() patchesRoute()
configureAnnouncementsRoute() announcementsRoute()
} }
// TODO: Remove, once migration period from v2 API is over (In 1-2 years).
oldApiRoute()
} }

View File

@ -12,11 +12,11 @@ import io.ktor.server.routing.*
import io.ktor.server.util.* import io.ktor.server.util.*
import org.koin.ktor.ext.get as koinGet import org.koin.ktor.ext.get as koinGet
internal fun Route.configureAnnouncementsRoute() = route("/announcements") { internal fun Route.announcementsRoute() = route("announcements") {
val announcementService = koinGet<AnnouncementService>() val announcementService = koinGet<AnnouncementService>()
route("/{channel}/latest") { route("{channel}/latest") {
get("/id") { get("id") {
val channel: String by call.parameters val channel: String by call.parameters
call.respond( call.respond(
@ -33,14 +33,14 @@ internal fun Route.configureAnnouncementsRoute() = route("/announcements") {
} }
} }
get("/{channel}") { get("{channel}") {
val channel: String by call.parameters val channel: String by call.parameters
call.respond(announcementService.all(channel)) call.respond(announcementService.all(channel))
} }
route("/latest") { route("latest") {
get("/id") { get("id") {
call.respond(announcementService.latestId() ?: return@get call.respond(HttpStatusCode.NotFound)) call.respond(announcementService.latestId() ?: return@get call.respond(HttpStatusCode.NotFound))
} }
@ -58,26 +58,26 @@ internal fun Route.configureAnnouncementsRoute() = route("/announcements") {
announcementService.new(call.receive<APIAnnouncement>()) announcementService.new(call.receive<APIAnnouncement>())
} }
post("/{id}/archive") { post("{id}/archive") {
val id: Int by call.parameters val id: Int by call.parameters
val archivedAt = call.receiveNullable<APIAnnouncementArchivedAt>()?.archivedAt val archivedAt = call.receiveNullable<APIAnnouncementArchivedAt>()?.archivedAt
announcementService.archive(id, archivedAt) announcementService.archive(id, archivedAt)
} }
post("/{id}/unarchive") { post("{id}/unarchive") {
val id: Int by call.parameters val id: Int by call.parameters
announcementService.unarchive(id) announcementService.unarchive(id)
} }
patch("/{id}") { patch("{id}") {
val id: Int by call.parameters val id: Int by call.parameters
announcementService.update(id, call.receive<APIAnnouncement>()) announcementService.update(id, call.receive<APIAnnouncement>())
} }
delete("/{id}") { delete("{id}") {
val id: Int by call.parameters val id: Int by call.parameters
announcementService.delete(id) announcementService.delete(id)

View File

@ -10,31 +10,31 @@ 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
internal fun Route.configureRootRoute() { internal fun Route.rootRoute() {
val apiService = get<ApiService>() val apiService = get<ApiService>()
val authService = get<AuthService>() val authService = get<AuthService>()
get("/contributors") { get("contributors") {
call.respond(apiService.contributors()) call.respond(apiService.contributors())
} }
get("/team") { get("team") {
call.respond(apiService.team()) call.respond(apiService.team())
} }
route("/ping") { route("ping") {
handle { handle {
call.respond(HttpStatusCode.NoContent) call.respond(HttpStatusCode.NoContent)
} }
} }
authenticate("basic") { authenticate("basic") {
get("/token") { get("token") {
call.respond(authService.newToken()) call.respond(authService.newToken())
} }
} }
staticResources("/", "/static/api") { staticResources("/", "/app/revanced/api/static") {
contentType { ContentType.Application.Json } contentType { ContentType.Application.Json }
extensions("json") extensions("json")
} }

View File

@ -0,0 +1,16 @@
package app.revanced.api.configuration.routing.routes
import app.revanced.api.repository.OldApiService
import io.ktor.server.application.*
import io.ktor.server.routing.*
import org.koin.ktor.ext.get
internal fun Route.oldApiRoute() {
val oldApiService = get<OldApiService>()
route(Regex("(v2|tools|contributor).*")) {
handle {
oldApiService.proxy(call)
}
}
}

View File

@ -7,7 +7,7 @@ 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
internal fun Route.configurePatchesRoute() = route("/patches") { internal fun Route.patchesRoute() = route("patches") {
val patchesService = koinGet<PatchesService>() val patchesService = koinGet<PatchesService>()
route("latest") { route("latest") {
@ -15,11 +15,11 @@ internal fun Route.configurePatchesRoute() = route("/patches") {
call.respond(patchesService.latestRelease()) call.respond(patchesService.latestRelease())
} }
get("/version") { get("version") {
call.respond(patchesService.latestVersion()) call.respond(patchesService.latestVersion())
} }
get("/list") { get("list") {
call.respondBytes(ContentType.Application.Json) { patchesService.list() } call.respondBytes(ContentType.Application.Json) { patchesService.list() }
} }
} }

View File

@ -0,0 +1,65 @@
package app.revanced.api.repository
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.util.*
import io.ktor.utils.io.*
internal class OldApiService(private val client: HttpClient) {
@OptIn(InternalAPI::class)
suspend fun proxy(call: ApplicationCall) {
val channel = call.request.receiveChannel()
val size = channel.availableForRead
val byteArray = ByteArray(size)
channel.readFully(byteArray)
val response: HttpResponse = client.request(call.request.uri) {
method = call.request.httpMethod
headers {
appendAll(
call.request.headers.filter { key, _ ->
!key.equals(
HttpHeaders.ContentType,
ignoreCase = true,
) && !key.equals(
HttpHeaders.ContentLength,
ignoreCase = true,
) && !key.equals(HttpHeaders.Host, ignoreCase = true)
},
)
}
if (call.request.httpMethod == HttpMethod.Post) {
body = ByteArrayContent(byteArray, call.request.contentType())
}
}
val headers = response.headers
call.respond(object : OutgoingContent.WriteChannelContent() {
override val contentLength: Long? = headers[HttpHeaders.ContentLength]?.toLong()
override val contentType = headers[HttpHeaders.ContentType]?.let { ContentType.parse(it) }
override val headers: Headers = Headers.build {
appendAll(
headers.filter { key, _ ->
!key.equals(
HttpHeaders.ContentType,
ignoreCase = true,
) && !key.equals(HttpHeaders.ContentLength, ignoreCase = true)
},
)
}
override val status = response.status
override suspend fun writeTo(channel: ByteWriteChannel) {
response.content.copyAndClose(channel)
}
})
}
}

View File

@ -1,20 +1,17 @@
package app.revanced.api.repository.backend package app.revanced.api.repository.backend
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
/** /**
* The backend of the application used to get data for the API. * The backend of the application used to get data for the API.
* *
* @param httpClientConfig The configuration of the HTTP client. * @param client The HTTP client to use for requests.
*/ */
abstract class BackendRepository internal constructor( abstract class BackendRepository internal constructor(
httpClientConfig: HttpClientConfig<OkHttpConfig>.() -> Unit = {}, protected val client: HttpClient,
) { ) {
protected val client: HttpClient = HttpClient(OkHttp, httpClientConfig)
/** /**
* A user. * A user.
* *

View File

@ -13,52 +13,14 @@ import app.revanced.api.repository.backend.github.api.Response
import app.revanced.api.repository.backend.github.api.Response.GitHubOrganization.GitHubMember import app.revanced.api.repository.backend.github.api.Response.GitHubOrganization.GitHubMember
import app.revanced.api.repository.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubContributor import app.revanced.api.repository.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubContributor
import app.revanced.api.repository.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubRelease import app.revanced.api.repository.backend.github.api.Response.GitHubOrganization.GitHubRepository.GitHubRelease
import io.ktor.client.*
import io.ktor.client.call.* import io.ktor.client.call.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.auth.*
import io.ktor.client.plugins.auth.providers.*
import io.ktor.client.plugins.cache.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.resources.* import io.ktor.client.plugins.resources.*
import io.ktor.client.plugins.resources.Resources
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime import kotlinx.datetime.toLocalDateTime
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNamingStrategy
@OptIn(ExperimentalSerializationApi::class) class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) {
class GitHubBackendRepository(token: String? = null) : BackendRepository({
install(HttpCache)
install(Resources)
install(ContentNegotiation) {
json(
Json {
ignoreUnknownKeys = true
namingStrategy = JsonNamingStrategy.SnakeCase
},
)
}
defaultRequest { url("https://api.github.com") }
token?.let {
install(Auth) {
bearer {
loadTokens {
BearerTokens(
accessToken = it,
refreshToken = "", // Required dummy value
)
}
sendWithoutRequest { true }
}
}
}
}) {
override suspend fun release( override suspend fun release(
owner: String, owner: String,
repository: String, repository: String,