diff --git a/.env.example b/.env.example index e3a8ba04..78477d23 100644 --- a/.env.example +++ b/.env.example @@ -11,3 +11,5 @@ JWT_VALIDITY_IN_MIN= BASIC_USERNAME= BASIC_PASSWORD= + +OLD_API_URL= \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt index 585730a7..ba473448 100644 --- a/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt +++ b/src/main/kotlin/app/revanced/api/command/StartAPICommand.kt @@ -2,8 +2,11 @@ package app.revanced.api.command import app.revanced.api.configuration.* import app.revanced.api.configuration.routing.configureRouting +import io.ktor.server.application.* import io.ktor.server.engine.* import io.ktor.server.netty.* +import io.ktor.server.response.* +import io.ktor.server.routing.* import picocli.CommandLine @CommandLine.Command( diff --git a/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt b/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt index dc8f6204..8a603242 100644 --- a/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt +++ b/src/main/kotlin/app/revanced/api/configuration/Dependencies.kt @@ -2,6 +2,7 @@ package app.revanced.api.configuration import app.revanced.api.repository.AnnouncementRepository 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.github.GitHubBackendRepository 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.source.decodeFromStream 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 kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonNamingStrategy import org.jetbrains.exposed.sql.Database 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.ktor.plugin.Koin import java.io.File +@OptIn(ExperimentalSerializationApi::class) fun Application.configureDependencies() { val globalModule = module { single { @@ -26,6 +40,16 @@ fun Application.configureDependencies() { .systemProperties() .load() } + factory { params -> + val defaultRequestUri: String = params.get() + val configBlock = params.getOrNull<(HttpClientConfig.() -> Unit)>() ?: {} + + HttpClient(OkHttp) { + defaultRequest { url(defaultRequestUri) } + + configBlock() + } + } } val repositoryModule = module { @@ -40,6 +64,43 @@ fun Application.configureDependencies() { ) } + single { + GitHubBackendRepository( + get { + val defaultRequestUri = "https://api.github.com" + val configBlock: HttpClientConfig.() -> Unit = { + install(HttpCache) + install(Resources) + install(ContentNegotiation) { + json( + Json { + ignoreUnknownKeys = true + namingStrategy = JsonNamingStrategy.SnakeCase + }, + ) + } + + get()["GITHUB_TOKEN"]?.let { + install(Auth) { + bearer { + loadTokens { + BearerTokens( + accessToken = it, + refreshToken = "", // Required dummy value + ) + } + + sendWithoutRequest { true } + } + } + } + } + + parameterArrayOf(defaultRequestUri, configBlock) + }, + ) + } + single { val configFilePath = get()["CONFIG_FILE_PATH"] val configFile = File(configFilePath).inputStream() @@ -64,10 +125,13 @@ fun Application.configureDependencies() { AuthService(issuer, validityInMin, jwtSecret, basicUsername, basicPassword) } single { - val token = get()["GITHUB_TOKEN"] - - GitHubBackendRepository(token) - } bind BackendRepository::class + OldApiService( + get { + val defaultRequestUri = get()["OLD_API_URL"] + parameterArrayOf(defaultRequestUri) + }, + ) + } singleOf(::AnnouncementService) singleOf(::PatchesService) singleOf(::ApiService) diff --git a/src/main/kotlin/app/revanced/api/configuration/routing/Routing.kt b/src/main/kotlin/app/revanced/api/configuration/routing/Routing.kt index 18420b6c..8296ee3a 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routing/Routing.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routing/Routing.kt @@ -1,8 +1,9 @@ package app.revanced.api.configuration.routing -import app.revanced.api.configuration.routing.routes.configureAnnouncementsRoute -import app.revanced.api.configuration.routing.routes.configurePatchesRoute -import app.revanced.api.configuration.routing.routes.configureRootRoute +import app.revanced.api.configuration.routing.routes.announcementsRoute +import app.revanced.api.configuration.routing.routes.oldApiRoute +import app.revanced.api.configuration.routing.routes.patchesRoute +import app.revanced.api.configuration.routing.routes.rootRoute import app.revanced.api.repository.ConfigurationRepository import io.ktor.server.application.* import io.ktor.server.routing.* @@ -12,8 +13,11 @@ internal fun Application.configureRouting() = routing { val configuration = get() route("/v${configuration.apiVersion}") { - configureRootRoute() - configurePatchesRoute() - configureAnnouncementsRoute() + rootRoute() + patchesRoute() + announcementsRoute() } + + // TODO: Remove, once migration period from v2 API is over (In 1-2 years). + oldApiRoute() } 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 4f9f038b..bc4859d9 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 @@ -12,11 +12,11 @@ import io.ktor.server.routing.* import io.ktor.server.util.* import org.koin.ktor.ext.get as koinGet -internal fun Route.configureAnnouncementsRoute() = route("/announcements") { +internal fun Route.announcementsRoute() = route("announcements") { val announcementService = koinGet() - route("/{channel}/latest") { - get("/id") { + route("{channel}/latest") { + get("id") { val channel: String by call.parameters call.respond( @@ -33,14 +33,14 @@ internal fun Route.configureAnnouncementsRoute() = route("/announcements") { } } - get("/{channel}") { + get("{channel}") { val channel: String by call.parameters call.respond(announcementService.all(channel)) } - route("/latest") { - get("/id") { + route("latest") { + get("id") { call.respond(announcementService.latestId() ?: return@get call.respond(HttpStatusCode.NotFound)) } @@ -58,26 +58,26 @@ internal fun Route.configureAnnouncementsRoute() = route("/announcements") { announcementService.new(call.receive()) } - post("/{id}/archive") { + post("{id}/archive") { val id: Int by call.parameters val archivedAt = call.receiveNullable()?.archivedAt announcementService.archive(id, archivedAt) } - post("/{id}/unarchive") { + post("{id}/unarchive") { val id: Int by call.parameters announcementService.unarchive(id) } - patch("/{id}") { + patch("{id}") { val id: Int by call.parameters announcementService.update(id, call.receive()) } - delete("/{id}") { + delete("{id}") { val id: Int by call.parameters 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 b502ac82..56eb5531 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 @@ -10,31 +10,31 @@ import io.ktor.server.response.* import io.ktor.server.routing.* import org.koin.ktor.ext.get -internal fun Route.configureRootRoute() { +internal fun Route.rootRoute() { val apiService = get() val authService = get() - get("/contributors") { + get("contributors") { call.respond(apiService.contributors()) } - get("/team") { + get("team") { call.respond(apiService.team()) } - route("/ping") { + route("ping") { handle { call.respond(HttpStatusCode.NoContent) } } authenticate("basic") { - get("/token") { + get("token") { call.respond(authService.newToken()) } } - staticResources("/", "/static/api") { + 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 new file mode 100644 index 00000000..64f8006c --- /dev/null +++ b/src/main/kotlin/app/revanced/api/configuration/routing/routes/OldApi.kt @@ -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() + + route(Regex("(v2|tools|contributor).*")) { + 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 21e811ef..4ddfd655 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 @@ -7,7 +7,7 @@ import io.ktor.server.response.* import io.ktor.server.routing.* import org.koin.ktor.ext.get as koinGet -internal fun Route.configurePatchesRoute() = route("/patches") { +internal fun Route.patchesRoute() = route("patches") { val patchesService = koinGet() route("latest") { @@ -15,11 +15,11 @@ internal fun Route.configurePatchesRoute() = route("/patches") { call.respond(patchesService.latestRelease()) } - get("/version") { + get("version") { call.respond(patchesService.latestVersion()) } - get("/list") { + get("list") { call.respondBytes(ContentType.Application.Json) { patchesService.list() } } } diff --git a/src/main/kotlin/app/revanced/api/repository/OldApiService.kt b/src/main/kotlin/app/revanced/api/repository/OldApiService.kt new file mode 100644 index 00000000..4c5612da --- /dev/null +++ b/src/main/kotlin/app/revanced/api/repository/OldApiService.kt @@ -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) + } + }) + } +} diff --git a/src/main/kotlin/app/revanced/api/repository/backend/BackendRepository.kt b/src/main/kotlin/app/revanced/api/repository/backend/BackendRepository.kt index 47ec9d8a..f27012d9 100644 --- a/src/main/kotlin/app/revanced/api/repository/backend/BackendRepository.kt +++ b/src/main/kotlin/app/revanced/api/repository/backend/BackendRepository.kt @@ -1,20 +1,17 @@ package app.revanced.api.repository.backend import io.ktor.client.* -import io.ktor.client.engine.okhttp.* import kotlinx.datetime.LocalDateTime import kotlinx.serialization.Serializable /** * 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( - httpClientConfig: HttpClientConfig.() -> Unit = {}, + protected val client: HttpClient, ) { - protected val client: HttpClient = HttpClient(OkHttp, httpClientConfig) - /** * A user. * diff --git a/src/main/kotlin/app/revanced/api/repository/backend/github/GitHubBackendRepository.kt b/src/main/kotlin/app/revanced/api/repository/backend/github/GitHubBackendRepository.kt index 5687f72d..1664adf3 100644 --- a/src/main/kotlin/app/revanced/api/repository/backend/github/GitHubBackendRepository.kt +++ b/src/main/kotlin/app/revanced/api/repository/backend/github/GitHubBackendRepository.kt @@ -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.GitHubRepository.GitHubContributor 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.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.Resources -import io.ktor.serialization.kotlinx.json.* import kotlinx.coroutines.* import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonNamingStrategy -@OptIn(ExperimentalSerializationApi::class) -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 } - } - } - } -}) { +class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) { override suspend fun release( owner: String, repository: String, diff --git a/src/main/resources/static/api/about.json b/src/main/resources/app/revanced/api/static/about.json similarity index 100% rename from src/main/resources/static/api/about.json rename to src/main/resources/app/revanced/api/static/about.json diff --git a/src/main/resources/static/robots.txt b/src/main/resources/app/revanced/api/static/robots.txt similarity index 100% rename from src/main/resources/static/robots.txt rename to src/main/resources/app/revanced/api/static/robots.txt