mirror of
https://github.com/ReVanced/revanced-api.git
synced 2026-01-18 08:53:57 +00:00
Compare commits
14 Commits
v1.0.0-dev
...
v1.0.0-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
236e7e7907 | ||
|
|
541783d959 | ||
|
|
db41081155 | ||
|
|
c40d50c013 | ||
|
|
b3c82535eb | ||
|
|
f814fe5825 | ||
|
|
a34b7c8c31 | ||
|
|
ad7d4b226f | ||
|
|
97a5d119ec | ||
|
|
2ade550d58 | ||
|
|
f754ebb25c | ||
|
|
19ebc827bf | ||
|
|
e92fbdf1f4 | ||
|
|
7e99e49af2 |
@@ -1,7 +1,5 @@
|
|||||||
# Optional token for API calls to the backend
|
# Optional token for API calls to the backend
|
||||||
BACKEND_API_TOKEN=
|
BACKEND_API_TOKEN=
|
||||||
# A URL to the old API to proxy for migration purposes
|
|
||||||
OLD_API_URL=
|
|
||||||
|
|
||||||
# Database connection details
|
# Database connection details
|
||||||
DB_URL=jdbc:h2:./persistence/revanced-api
|
DB_URL=jdbc:h2:./persistence/revanced-api
|
||||||
|
|||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -45,7 +45,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||||
fingerprint: ${{ env.GPG_FINGERPRINT }}
|
fingerprint: ${{ vars.GPG_FINGERPRINT }}
|
||||||
|
|
||||||
- name: Setup QEMU
|
- name: Setup QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|||||||
42
CHANGELOG.md
42
CHANGELOG.md
@@ -1,3 +1,45 @@
|
|||||||
|
# [1.0.0-dev.7](https://github.com/ReVanced/revanced-api/compare/v1.0.0-dev.6...v1.0.0-dev.7) (2024-07-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Fix OpenAPI docs casing of a word ([541783d](https://github.com/ReVanced/revanced-api/commit/541783d9599c257f184d1b244e1b857b7c200227))
|
||||||
|
|
||||||
|
# [1.0.0-dev.6](https://github.com/ReVanced/revanced-api/compare/v1.0.0-dev.5...v1.0.0-dev.6) (2024-07-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Add bio field for team members ([c40d50c](https://github.com/ReVanced/revanced-api/commit/c40d50c01368cb6b9ab06857694ec51d27aba2cb))
|
||||||
|
|
||||||
|
# [1.0.0-dev.5](https://github.com/ReVanced/revanced-api/compare/v1.0.0-dev.4...v1.0.0-dev.5) (2024-07-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Move robots.txt to root ([2ade550](https://github.com/ReVanced/revanced-api/commit/2ade550d58c0e4b53fa7417bef0064f4f476aed8))
|
||||||
|
* Only list public members ([97a5d11](https://github.com/ReVanced/revanced-api/commit/97a5d119ec415f9c25fbc50cb240603047defc73))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Add configuration to specify public key id ([ad7d4b2](https://github.com/ReVanced/revanced-api/commit/ad7d4b226f71fd965421dc2f2a51825c8e8b3036))
|
||||||
|
* Add manager route ([f814fe5](https://github.com/ReVanced/revanced-api/commit/f814fe5825eb8b864f2b0cba53ff721eba577eb4))
|
||||||
|
|
||||||
|
# [1.0.0-dev.4](https://github.com/ReVanced/revanced-api/compare/v1.0.0-dev.3...v1.0.0-dev.4) (2024-07-11)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* List more repository contributors ([19ebc82](https://github.com/ReVanced/revanced-api/commit/19ebc827bfb54a597dd06f9d99bdc820ee9977ee))
|
||||||
|
|
||||||
|
# [1.0.0-dev.3](https://github.com/ReVanced/revanced-api/compare/v1.0.0-dev.2...v1.0.0-dev.3) (2024-07-11)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Move old API endpoint configuration from env to configuration file ([7e99e49](https://github.com/ReVanced/revanced-api/commit/7e99e49af202c4ec0a0d7e61dd0182dd2097e867))
|
||||||
|
|
||||||
# [1.0.0-dev.2](https://github.com/ReVanced/revanced-api/compare/v1.0.0-dev.1...v1.0.0-dev.2) (2024-07-11)
|
# [1.0.0-dev.2](https://github.com/ReVanced/revanced-api/compare/v1.0.0-dev.1...v1.0.0-dev.2) (2024-07-11)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
organization = "revanced"
|
organization = "revanced"
|
||||||
patches = { repository = "revanced-patches", asset-regex = "jar$", signature-asset-regex = "asc$", public-key-file = "patches-public-key.asc" }
|
patches = { repository = "revanced-patches", asset-regex = "jar$", signature-asset-regex = "asc$", public-key-file = "patches-public-key.asc" }
|
||||||
integrations = { repository = "revanced-integrations", asset-regex = "apk$", signature-asset-regex = "asc$", public-key-file = "integrations-public-key.asc" }
|
integrations = { repository = "revanced-integrations", asset-regex = "apk$", signature-asset-regex = "asc$", public-key-file = "integrations-public-key.asc" }
|
||||||
|
manager = { repository = "revanced-manager", asset-regex = "apk$" }
|
||||||
contributors-repositories = [
|
contributors-repositories = [
|
||||||
"revanced-patcher",
|
"revanced-patcher",
|
||||||
"revanced-patches",
|
"revanced-patches",
|
||||||
@@ -15,3 +16,4 @@ cors-allowed-hosts = [
|
|||||||
"*.revanced.app"
|
"*.revanced.app"
|
||||||
]
|
]
|
||||||
endpoint = "https://api.revanced.app"
|
endpoint = "https://api.revanced.app"
|
||||||
|
old-api-endpoint = "https://old-api.revanced.app"
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
organization = "revanced"
|
|
||||||
patches = { repository = "revanced-patches", asset-regex = "jar$", signature-asset-regex = "asc$", public-key-file = "key.asc" }
|
|
||||||
integrations = { repository = "revanced-integrations", asset-regex = "apk$", signature-asset-regex = "asc$", public-key-file = "key.asc" }
|
|
||||||
contributors-repositories = [
|
|
||||||
"revanced-patcher",
|
|
||||||
"revanced-patches",
|
|
||||||
"revanced-integrations",
|
|
||||||
"revanced-website",
|
|
||||||
"revanced-cli",
|
|
||||||
"revanced-manager",
|
|
||||||
]
|
|
||||||
api-version = 1
|
|
||||||
cors = { host = "*.127.0.0.1:8888", sub-domains = [] }
|
|
||||||
endpoint = "http://127.0.0.1:8888/"
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
org.gradle.parallel = true
|
org.gradle.parallel = true
|
||||||
org.gradle.caching = true
|
org.gradle.caching = true
|
||||||
kotlin.code.style = official
|
kotlin.code.style = official
|
||||||
version = 1.0.0-dev.2
|
version = 1.0.0-dev.7
|
||||||
|
|||||||
@@ -123,16 +123,18 @@ fun Application.configureDependencies(
|
|||||||
AuthService(issuer, validityInMin, jwtSecret, authSHA256DigestString)
|
AuthService(issuer, validityInMin, jwtSecret, authSHA256DigestString)
|
||||||
}
|
}
|
||||||
single {
|
single {
|
||||||
|
val configuration = get<ConfigurationRepository>()
|
||||||
|
|
||||||
OldApiService(
|
OldApiService(
|
||||||
get {
|
get {
|
||||||
val defaultRequestUri = get<Dotenv>()["OLD_API_URL"]
|
parameterArrayOf(configuration.oldApiEndpoint)
|
||||||
parameterArrayOf(defaultRequestUri)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
singleOf(::AnnouncementService)
|
singleOf(::AnnouncementService)
|
||||||
singleOf(::SignatureService)
|
singleOf(::SignatureService)
|
||||||
singleOf(::PatchesService)
|
singleOf(::PatchesService)
|
||||||
|
singleOf(::ManagerService)
|
||||||
singleOf(::ApiService)
|
singleOf(::ApiService)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
package app.revanced.api.configuration
|
package app.revanced.api.configuration
|
||||||
|
|
||||||
import app.revanced.api.configuration.repository.ConfigurationRepository
|
import app.revanced.api.configuration.repository.ConfigurationRepository
|
||||||
|
import app.revanced.api.configuration.routes.*
|
||||||
import app.revanced.api.configuration.routes.announcementsRoute
|
import app.revanced.api.configuration.routes.announcementsRoute
|
||||||
|
import app.revanced.api.configuration.routes.apiRoute
|
||||||
import app.revanced.api.configuration.routes.oldApiRoute
|
import app.revanced.api.configuration.routes.oldApiRoute
|
||||||
import app.revanced.api.configuration.routes.patchesRoute
|
import app.revanced.api.configuration.routes.patchesRoute
|
||||||
import app.revanced.api.configuration.routes.rootRoute
|
|
||||||
import io.bkbn.kompendium.core.routes.redoc
|
import io.bkbn.kompendium.core.routes.redoc
|
||||||
import io.bkbn.kompendium.core.routes.swagger
|
import io.bkbn.kompendium.core.routes.swagger
|
||||||
|
import io.ktor.http.*
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.http.content.*
|
||||||
import io.ktor.server.routing.*
|
import io.ktor.server.routing.*
|
||||||
import kotlin.time.Duration.Companion.minutes
|
import kotlin.time.Duration.Companion.minutes
|
||||||
import org.koin.ktor.ext.get as koinGet
|
import org.koin.ktor.ext.get as koinGet
|
||||||
@@ -18,9 +21,15 @@ internal fun Application.configureRouting() = routing {
|
|||||||
installCache(5.minutes)
|
installCache(5.minutes)
|
||||||
|
|
||||||
route("/v${configuration.apiVersion}") {
|
route("/v${configuration.apiVersion}") {
|
||||||
patchesRoute()
|
|
||||||
announcementsRoute()
|
announcementsRoute()
|
||||||
rootRoute()
|
patchesRoute()
|
||||||
|
managerRoute()
|
||||||
|
apiRoute()
|
||||||
|
}
|
||||||
|
|
||||||
|
staticResources("/", "/app/revanced/api/static/root") {
|
||||||
|
contentType { ContentType.Application.Json }
|
||||||
|
extensions("json")
|
||||||
}
|
}
|
||||||
|
|
||||||
swagger(pageTitle = "ReVanced API", path = "/")
|
swagger(pageTitle = "ReVanced API", path = "/")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.revanced.api.configuration.repository
|
package app.revanced.api.configuration.repository
|
||||||
|
|
||||||
|
import app.revanced.api.configuration.services.ManagerService
|
||||||
import app.revanced.api.configuration.services.PatchesService
|
import app.revanced.api.configuration.services.PatchesService
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
@@ -17,16 +18,19 @@ import java.io.File
|
|||||||
* @property organization The API backends organization name where the repositories for the patches and integrations are.
|
* @property organization The API backends organization name where the repositories for the patches and integrations are.
|
||||||
* @property patches The source of the patches.
|
* @property patches The source of the patches.
|
||||||
* @property integrations The source of the integrations.
|
* @property integrations The source of the integrations.
|
||||||
|
* @property manager The source of the manager.
|
||||||
* @property contributorsRepositoryNames The names of the repositories to get contributors from.
|
* @property contributorsRepositoryNames The names of the repositories to get contributors from.
|
||||||
* @property apiVersion The version to use for the API.
|
* @property apiVersion The version to use for the API.
|
||||||
* @property corsAllowedHosts The hosts allowed to make requests to the API.
|
* @property corsAllowedHosts The hosts allowed to make requests to the API.
|
||||||
* @property endpoint The endpoint of the API.
|
* @property endpoint The endpoint of the API.
|
||||||
|
* @property oldApiEndpoint The endpoint of the old API to proxy requests to.
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
internal class ConfigurationRepository(
|
internal class ConfigurationRepository(
|
||||||
val organization: String,
|
val organization: String,
|
||||||
val patches: AssetConfiguration,
|
val patches: SignedAssetConfiguration,
|
||||||
val integrations: AssetConfiguration,
|
val integrations: SignedAssetConfiguration,
|
||||||
|
val manager: AssetConfiguration,
|
||||||
@SerialName("contributors-repositories")
|
@SerialName("contributors-repositories")
|
||||||
val contributorsRepositoryNames: Set<String>,
|
val contributorsRepositoryNames: Set<String>,
|
||||||
@SerialName("api-version")
|
@SerialName("api-version")
|
||||||
@@ -34,11 +38,13 @@ internal class ConfigurationRepository(
|
|||||||
@SerialName("cors-allowed-hosts")
|
@SerialName("cors-allowed-hosts")
|
||||||
val corsAllowedHosts: Set<String>,
|
val corsAllowedHosts: Set<String>,
|
||||||
val endpoint: String,
|
val endpoint: String,
|
||||||
|
@SerialName("old-api-endpoint")
|
||||||
|
val oldApiEndpoint: String,
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* An asset configuration.
|
* Am asset configuration whose asset is signed.
|
||||||
*
|
*
|
||||||
* [PatchesService] uses [BackendRepository] to get assets from its releases.
|
* [PatchesService] for example uses [BackendRepository] to get assets from its releases.
|
||||||
* A release contains multiple assets.
|
* A release contains multiple assets.
|
||||||
*
|
*
|
||||||
* This configuration is used in [ConfigurationRepository]
|
* This configuration is used in [ConfigurationRepository]
|
||||||
@@ -48,9 +54,10 @@ internal class ConfigurationRepository(
|
|||||||
* @property assetRegex The regex matching the asset name.
|
* @property assetRegex The regex matching the asset name.
|
||||||
* @property signatureAssetRegex The regex matching the signature asset name to verify the asset.
|
* @property signatureAssetRegex The regex matching the signature asset name to verify the asset.
|
||||||
* @property publicKeyFile The public key file to verify the signature of the asset.
|
* @property publicKeyFile The public key file to verify the signature of the asset.
|
||||||
|
* @property publicKeyId The ID of the public key to verify the signature of the asset.
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
internal class AssetConfiguration(
|
internal class SignedAssetConfiguration(
|
||||||
val repository: String,
|
val repository: String,
|
||||||
@Serializable(with = RegexSerializer::class)
|
@Serializable(with = RegexSerializer::class)
|
||||||
@SerialName("asset-regex")
|
@SerialName("asset-regex")
|
||||||
@@ -61,6 +68,28 @@ internal class ConfigurationRepository(
|
|||||||
@Serializable(with = FileSerializer::class)
|
@Serializable(with = FileSerializer::class)
|
||||||
@SerialName("public-key-file")
|
@SerialName("public-key-file")
|
||||||
val publicKeyFile: File,
|
val publicKeyFile: File,
|
||||||
|
@SerialName("public-key-id")
|
||||||
|
val publicKeyId: Long,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Am asset configuration.
|
||||||
|
*
|
||||||
|
* [ManagerService] for example uses [BackendRepository] to get assets from its releases.
|
||||||
|
* A release contains multiple assets.
|
||||||
|
*
|
||||||
|
* This configuration is used in [ConfigurationRepository]
|
||||||
|
* to determine which release assets from repositories to get and to verify them.
|
||||||
|
*
|
||||||
|
* @property repository The repository in which releases are made to get an asset.
|
||||||
|
* @property assetRegex The regex matching the asset name.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
internal class AssetConfiguration(
|
||||||
|
val repository: String,
|
||||||
|
@Serializable(with = RegexSerializer::class)
|
||||||
|
@SerialName("asset-regex")
|
||||||
|
val assetRegex: Regex,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import kotlinx.coroutines.*
|
|||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
import kotlinx.datetime.toLocalDateTime
|
import kotlinx.datetime.toLocalDateTime
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) {
|
class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) {
|
||||||
@@ -66,10 +67,10 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) {
|
|||||||
|
|
||||||
override suspend fun members(organization: String): List<BackendMember> {
|
override suspend fun members(organization: String): List<BackendMember> {
|
||||||
// Get the list of members of the organization.
|
// Get the list of members of the organization.
|
||||||
val members: List<GitHubOrganization.GitHubMember> = client.get(Organization.Members(organization)).body()
|
val publicMembers: List<GitHubOrganization.GitHubMember> = client.get(Organization.PublicMembers(organization)).body()
|
||||||
|
|
||||||
return coroutineScope {
|
return coroutineScope {
|
||||||
members.map { member ->
|
publicMembers.map { member ->
|
||||||
async {
|
async {
|
||||||
awaitAll(
|
awaitAll(
|
||||||
async {
|
async {
|
||||||
@@ -186,15 +187,14 @@ class User(val login: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Organization {
|
class Organization {
|
||||||
@Resource("/orgs/{org}/members")
|
@Resource("/orgs/{org}/public_members")
|
||||||
class Members(val org: String)
|
class PublicMembers(val org: String)
|
||||||
|
|
||||||
class Repository {
|
class Repository {
|
||||||
@Resource("/repos/{owner}/{repo}/contributors")
|
@Resource("/repos/{owner}/{repo}/contributors")
|
||||||
class Contributors(val owner: String, val repo: String)
|
class Contributors(val owner: String, val repo: String, @SerialName("per_page") val perPage: Int = 100)
|
||||||
|
|
||||||
@Resource("/repos/{owner}/{repo}/releases")
|
class Releases {
|
||||||
class Releases(val owner: String, val repo: String) {
|
|
||||||
@Resource("/repos/{owner}/{repo}/releases/tags/{tag}")
|
@Resource("/repos/{owner}/{repo}/releases/tags/{tag}")
|
||||||
class Tag(val owner: String, val repo: String, val tag: String)
|
class Tag(val owner: String, val repo: String, val tag: String)
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import io.ktor.server.routing.*
|
|||||||
import kotlin.time.Duration.Companion.days
|
import kotlin.time.Duration.Companion.days
|
||||||
import org.koin.ktor.ext.get as koinGet
|
import org.koin.ktor.ext.get as koinGet
|
||||||
|
|
||||||
internal fun Route.rootRoute() {
|
internal fun Route.apiRoute() {
|
||||||
val apiService = koinGet<ApiService>()
|
val apiService = koinGet<ApiService>()
|
||||||
val authService = koinGet<AuthService>()
|
val authService = koinGet<AuthService>()
|
||||||
|
|
||||||
@@ -75,14 +75,14 @@ internal fun Route.rootRoute() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
staticResources("/", "/app/revanced/api/static") {
|
staticResources("/", "/app/revanced/api/static/versioned") {
|
||||||
contentType { ContentType.Application.Json }
|
contentType { ContentType.Application.Json }
|
||||||
extensions("json")
|
extensions("json")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Route.installRateLimitRouteDocumentation() = installNotarizedRoute {
|
private fun Route.installRateLimitRouteDocumentation() = installNotarizedRoute {
|
||||||
tags = setOf("API")
|
tags = setOf("API")
|
||||||
|
|
||||||
get = GetInfo.builder {
|
get = GetInfo.builder {
|
||||||
@@ -97,7 +97,7 @@ fun Route.installRateLimitRouteDocumentation() = installNotarizedRoute {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Route.installPingRouteDocumentation() = installNotarizedRoute {
|
private fun Route.installPingRouteDocumentation() = installNotarizedRoute {
|
||||||
tags = setOf("API")
|
tags = setOf("API")
|
||||||
|
|
||||||
head = HeadInfo.builder {
|
head = HeadInfo.builder {
|
||||||
@@ -111,7 +111,7 @@ fun Route.installPingRouteDocumentation() = installNotarizedRoute {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Route.installTeamRouteDocumentation() = installNotarizedRoute {
|
private fun Route.installTeamRouteDocumentation() = installNotarizedRoute {
|
||||||
tags = setOf("API")
|
tags = setOf("API")
|
||||||
|
|
||||||
get = GetInfo.builder {
|
get = GetInfo.builder {
|
||||||
@@ -126,7 +126,7 @@ fun Route.installTeamRouteDocumentation() = installNotarizedRoute {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Route.installContributorsRouteDocumentation() = installNotarizedRoute {
|
private fun Route.installContributorsRouteDocumentation() = installNotarizedRoute {
|
||||||
tags = setOf("API")
|
tags = setOf("API")
|
||||||
|
|
||||||
get = GetInfo.builder {
|
get = GetInfo.builder {
|
||||||
@@ -141,7 +141,7 @@ fun Route.installContributorsRouteDocumentation() = installNotarizedRoute {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Route.installTokenRouteDocumentation() = installNotarizedRoute {
|
private fun Route.installTokenRouteDocumentation() = installNotarizedRoute {
|
||||||
tags = setOf("API")
|
tags = setOf("API")
|
||||||
|
|
||||||
get = GetInfo.builder {
|
get = GetInfo.builder {
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package app.revanced.api.configuration.routes
|
||||||
|
|
||||||
|
import app.revanced.api.configuration.installNotarizedRoute
|
||||||
|
import app.revanced.api.configuration.schema.APIManagerAsset
|
||||||
|
import app.revanced.api.configuration.schema.APIRelease
|
||||||
|
import app.revanced.api.configuration.schema.APIReleaseVersion
|
||||||
|
import app.revanced.api.configuration.services.ManagerService
|
||||||
|
import io.bkbn.kompendium.core.metadata.GetInfo
|
||||||
|
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
|
||||||
|
|
||||||
|
internal fun Route.managerRoute() = route("manager") {
|
||||||
|
val managerService = koinGet<ManagerService>()
|
||||||
|
|
||||||
|
route("latest") {
|
||||||
|
installLatestManagerRouteDocumentation()
|
||||||
|
|
||||||
|
rateLimit(RateLimitName("weak")) {
|
||||||
|
get {
|
||||||
|
call.respond(managerService.latestRelease())
|
||||||
|
}
|
||||||
|
|
||||||
|
route("version") {
|
||||||
|
installLatestManagerVersionRouteDocumentation()
|
||||||
|
|
||||||
|
get {
|
||||||
|
call.respond(managerService.latestVersion())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Route.installLatestManagerRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("Manager")
|
||||||
|
|
||||||
|
get = GetInfo.builder {
|
||||||
|
description("Get the latest manager release")
|
||||||
|
summary("Get latest manager release")
|
||||||
|
response {
|
||||||
|
description("The latest manager release")
|
||||||
|
mediaTypes("application/json")
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
responseType<APIRelease<APIManagerAsset>>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Route.installLatestManagerVersionRouteDocumentation() = installNotarizedRoute {
|
||||||
|
tags = setOf("Manager")
|
||||||
|
|
||||||
|
get = GetInfo.builder {
|
||||||
|
description("Get the latest manager release version")
|
||||||
|
summary("Get latest manager release version")
|
||||||
|
response {
|
||||||
|
description("The latest manager release version")
|
||||||
|
mediaTypes("application/json")
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
responseType<APIReleaseVersion>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package app.revanced.api.configuration.routes
|
|||||||
import app.revanced.api.configuration.installCache
|
import app.revanced.api.configuration.installCache
|
||||||
import app.revanced.api.configuration.installNotarizedRoute
|
import app.revanced.api.configuration.installNotarizedRoute
|
||||||
import app.revanced.api.configuration.schema.APIAssetPublicKeys
|
import app.revanced.api.configuration.schema.APIAssetPublicKeys
|
||||||
|
import app.revanced.api.configuration.schema.APIPatchesAsset
|
||||||
import app.revanced.api.configuration.schema.APIRelease
|
import app.revanced.api.configuration.schema.APIRelease
|
||||||
import app.revanced.api.configuration.schema.APIReleaseVersion
|
import app.revanced.api.configuration.schema.APIReleaseVersion
|
||||||
import app.revanced.api.configuration.services.PatchesService
|
import app.revanced.api.configuration.services.PatchesService
|
||||||
@@ -59,7 +60,7 @@ internal fun Route.patchesRoute() = route("patches") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Route.installLatestPatchesRouteDocumentation() = installNotarizedRoute {
|
private fun Route.installLatestPatchesRouteDocumentation() = installNotarizedRoute {
|
||||||
tags = setOf("Patches")
|
tags = setOf("Patches")
|
||||||
|
|
||||||
get = GetInfo.builder {
|
get = GetInfo.builder {
|
||||||
@@ -69,12 +70,12 @@ fun Route.installLatestPatchesRouteDocumentation() = installNotarizedRoute {
|
|||||||
description("The latest patches release")
|
description("The latest patches release")
|
||||||
mediaTypes("application/json")
|
mediaTypes("application/json")
|
||||||
responseCode(HttpStatusCode.OK)
|
responseCode(HttpStatusCode.OK)
|
||||||
responseType<APIRelease>()
|
responseType<APIRelease<APIPatchesAsset>>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Route.installLatestPatchesVersionRouteDocumentation() = installNotarizedRoute {
|
private fun Route.installLatestPatchesVersionRouteDocumentation() = installNotarizedRoute {
|
||||||
tags = setOf("Patches")
|
tags = setOf("Patches")
|
||||||
|
|
||||||
get = GetInfo.builder {
|
get = GetInfo.builder {
|
||||||
@@ -89,7 +90,7 @@ fun Route.installLatestPatchesVersionRouteDocumentation() = installNotarizedRout
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Route.installLatestPatchesListRouteDocumentation() = installNotarizedRoute {
|
private fun Route.installLatestPatchesListRouteDocumentation() = installNotarizedRoute {
|
||||||
tags = setOf("Patches")
|
tags = setOf("Patches")
|
||||||
|
|
||||||
get = GetInfo.builder {
|
get = GetInfo.builder {
|
||||||
@@ -104,7 +105,7 @@ fun Route.installLatestPatchesListRouteDocumentation() = installNotarizedRoute {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Route.installPatchesPublicKeyRouteDocumentation() = installNotarizedRoute {
|
private fun Route.installPatchesPublicKeyRouteDocumentation() = installNotarizedRoute {
|
||||||
tags = setOf("Patches")
|
tags = setOf("Patches")
|
||||||
|
|
||||||
get = GetInfo.builder {
|
get = GetInfo.builder {
|
||||||
|
|||||||
@@ -3,15 +3,6 @@ package app.revanced.api.configuration.schema
|
|||||||
import kotlinx.datetime.LocalDateTime
|
import kotlinx.datetime.LocalDateTime
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class APIRelease(
|
|
||||||
val version: String,
|
|
||||||
val createdAt: LocalDateTime,
|
|
||||||
val description: String,
|
|
||||||
// Using a list instead of a set because set semantics are unnecessary here.
|
|
||||||
val assets: List<APIAsset>,
|
|
||||||
)
|
|
||||||
|
|
||||||
interface APIUser {
|
interface APIUser {
|
||||||
val name: String
|
val name: String
|
||||||
val avatarUrl: String
|
val avatarUrl: String
|
||||||
@@ -23,6 +14,7 @@ class APIMember(
|
|||||||
override val name: String,
|
override val name: String,
|
||||||
override val avatarUrl: String,
|
override val avatarUrl: String,
|
||||||
override val url: String,
|
override val url: String,
|
||||||
|
val bio: String?,
|
||||||
val gpgKey: APIGpgKey?,
|
val gpgKey: APIGpgKey?,
|
||||||
) : APIUser
|
) : APIUser
|
||||||
|
|
||||||
@@ -48,7 +40,21 @@ class APIContributable(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class APIAsset(
|
class APIRelease<T>(
|
||||||
|
val version: String,
|
||||||
|
val createdAt: LocalDateTime,
|
||||||
|
val description: String,
|
||||||
|
// Using a list instead of a set because set semantics are unnecessary here.
|
||||||
|
val assets: List<T>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class APIManagerAsset(
|
||||||
|
val downloadUrl: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class APIPatchesAsset(
|
||||||
val downloadUrl: String,
|
val downloadUrl: String,
|
||||||
val signatureDownloadUrl: String,
|
val signatureDownloadUrl: String,
|
||||||
// TODO: Remove this eventually when integrations are merged into patches.
|
// TODO: Remove this eventually when integrations are merged into patches.
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ internal class ApiService(
|
|||||||
member.name,
|
member.name,
|
||||||
member.avatarUrl,
|
member.avatarUrl,
|
||||||
member.url,
|
member.url,
|
||||||
|
member.bio,
|
||||||
if (member.gpgKeys.ids.isNotEmpty()) {
|
if (member.gpgKeys.ids.isNotEmpty()) {
|
||||||
APIGpgKey(
|
APIGpgKey(
|
||||||
// Must choose one of the GPG keys, because it does not make sense to have multiple GPG keys for the API.
|
// Must choose one of the GPG keys, because it does not make sense to have multiple GPG keys for the API.
|
||||||
@@ -39,7 +40,6 @@ internal class ApiService(
|
|||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
},
|
},
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package app.revanced.api.configuration.services
|
||||||
|
|
||||||
|
import app.revanced.api.configuration.repository.BackendRepository
|
||||||
|
import app.revanced.api.configuration.repository.BackendRepository.BackendOrganization.BackendRepository.BackendRelease.Companion.first
|
||||||
|
import app.revanced.api.configuration.repository.ConfigurationRepository
|
||||||
|
import app.revanced.api.configuration.schema.*
|
||||||
|
|
||||||
|
internal class ManagerService(
|
||||||
|
private val backendRepository: BackendRepository,
|
||||||
|
private val configurationRepository: ConfigurationRepository,
|
||||||
|
) {
|
||||||
|
suspend fun latestRelease(): APIRelease<APIManagerAsset> {
|
||||||
|
val managerRelease = backendRepository.release(
|
||||||
|
configurationRepository.organization,
|
||||||
|
configurationRepository.manager.repository,
|
||||||
|
)
|
||||||
|
|
||||||
|
val managerAsset = APIManagerAsset(
|
||||||
|
managerRelease.assets.first(configurationRepository.manager.assetRegex).downloadUrl,
|
||||||
|
)
|
||||||
|
|
||||||
|
return APIRelease(
|
||||||
|
managerRelease.tag,
|
||||||
|
managerRelease.createdAt,
|
||||||
|
managerRelease.releaseNote,
|
||||||
|
listOf(managerAsset),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun latestVersion(): APIReleaseVersion {
|
||||||
|
val managerRelease = backendRepository.release(
|
||||||
|
configurationRepository.organization,
|
||||||
|
configurationRepository.manager.repository,
|
||||||
|
)
|
||||||
|
|
||||||
|
return APIReleaseVersion(managerRelease.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ internal class PatchesService(
|
|||||||
private val backendRepository: BackendRepository,
|
private val backendRepository: BackendRepository,
|
||||||
private val configurationRepository: ConfigurationRepository,
|
private val configurationRepository: ConfigurationRepository,
|
||||||
) {
|
) {
|
||||||
suspend fun latestRelease(): APIRelease {
|
suspend fun latestRelease(): APIRelease<APIPatchesAsset> {
|
||||||
val patchesRelease = backendRepository.release(
|
val patchesRelease = backendRepository.release(
|
||||||
configurationRepository.organization,
|
configurationRepository.organization,
|
||||||
configurationRepository.patches.repository,
|
configurationRepository.patches.repository,
|
||||||
@@ -29,10 +29,10 @@ internal class PatchesService(
|
|||||||
configurationRepository.integrations.repository,
|
configurationRepository.integrations.repository,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun ConfigurationRepository.AssetConfiguration.asset(
|
fun ConfigurationRepository.SignedAssetConfiguration.asset(
|
||||||
release: BackendRepository.BackendOrganization.BackendRepository.BackendRelease,
|
release: BackendRepository.BackendOrganization.BackendRepository.BackendRelease,
|
||||||
assetName: APIAssetName,
|
assetName: APIAssetName,
|
||||||
) = APIAsset(
|
) = APIPatchesAsset(
|
||||||
release.assets.first(assetRegex).downloadUrl,
|
release.assets.first(assetRegex).downloadUrl,
|
||||||
release.assets.first(signatureAssetRegex).downloadUrl,
|
release.assets.first(signatureAssetRegex).downloadUrl,
|
||||||
assetName,
|
assetName,
|
||||||
@@ -92,6 +92,7 @@ internal class PatchesService(
|
|||||||
patchesFile,
|
patchesFile,
|
||||||
signatureDownloadUrl,
|
signatureDownloadUrl,
|
||||||
configurationRepository.patches.publicKeyFile,
|
configurationRepository.patches.publicKeyFile,
|
||||||
|
configurationRepository.patches.publicKeyId,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
PatchBundleLoader.Jar(patchesFile)
|
PatchBundleLoader.Jar(patchesFile)
|
||||||
@@ -112,8 +113,8 @@ internal class PatchesService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun publicKeys(): APIAssetPublicKeys {
|
fun publicKeys(): APIAssetPublicKeys {
|
||||||
fun publicKeyBase64(getAssetConfiguration: ConfigurationRepository.() -> ConfigurationRepository.AssetConfiguration) =
|
fun publicKeyBase64(getSignedAssetConfiguration: ConfigurationRepository.() -> ConfigurationRepository.SignedAssetConfiguration) =
|
||||||
configurationRepository.getAssetConfiguration().publicKeyFile.readBytes().encodeBase64()
|
configurationRepository.getSignedAssetConfiguration().publicKeyFile.readBytes().encodeBase64()
|
||||||
|
|
||||||
return APIAssetPublicKeys(
|
return APIAssetPublicKeys(
|
||||||
publicKeyBase64 { patches },
|
publicKeyBase64 { patches },
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package app.revanced.api.configuration.services
|
package app.revanced.api.configuration.services
|
||||||
|
|
||||||
import com.github.benmanes.caffeine.cache.Caffeine
|
import com.github.benmanes.caffeine.cache.Caffeine
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
|
||||||
import org.bouncycastle.openpgp.*
|
import org.bouncycastle.openpgp.*
|
||||||
import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator
|
import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator
|
||||||
import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider
|
import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider
|
||||||
@@ -9,7 +8,6 @@ import java.io.File
|
|||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.Security
|
|
||||||
|
|
||||||
internal class SignatureService {
|
internal class SignatureService {
|
||||||
private val signatureCache = Caffeine
|
private val signatureCache = Caffeine
|
||||||
@@ -21,6 +19,7 @@ internal class SignatureService {
|
|||||||
file: File,
|
file: File,
|
||||||
signatureDownloadUrl: String,
|
signatureDownloadUrl: String,
|
||||||
publicKeyFile: File,
|
publicKeyFile: File,
|
||||||
|
publicKeyId: Long,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val fileBytes = file.readBytes()
|
val fileBytes = file.readBytes()
|
||||||
|
|
||||||
@@ -28,7 +27,8 @@ internal class SignatureService {
|
|||||||
verify(
|
verify(
|
||||||
fileBytes = fileBytes,
|
fileBytes = fileBytes,
|
||||||
signatureInputStream = URL(signatureDownloadUrl).openStream(),
|
signatureInputStream = URL(signatureDownloadUrl).openStream(),
|
||||||
publicKeyInputStream = publicKeyFile.inputStream(),
|
publicKeyFileInputStream = publicKeyFile.inputStream(),
|
||||||
|
publicKeyId = publicKeyId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,37 +36,32 @@ internal class SignatureService {
|
|||||||
private fun verify(
|
private fun verify(
|
||||||
fileBytes: ByteArray,
|
fileBytes: ByteArray,
|
||||||
signatureInputStream: InputStream,
|
signatureInputStream: InputStream,
|
||||||
publicKeyInputStream: InputStream,
|
publicKeyFileInputStream: InputStream,
|
||||||
|
publicKeyId: Long,
|
||||||
) = getSignature(signatureInputStream).apply {
|
) = getSignature(signatureInputStream).apply {
|
||||||
init(BcPGPContentVerifierBuilderProvider(), getPublicKey(publicKeyInputStream))
|
init(BcPGPContentVerifierBuilderProvider(), getPublicKey(publicKeyFileInputStream, publicKeyId))
|
||||||
update(fileBytes)
|
update(fileBytes)
|
||||||
}.verify()
|
}.verify()
|
||||||
|
|
||||||
private fun getPublicKey(publicKeyInputStream: InputStream): PGPPublicKey {
|
private fun getPublicKey(
|
||||||
val decoderStream = PGPUtil.getDecoderStream(publicKeyInputStream)
|
publicKeyFileInputStream: InputStream,
|
||||||
|
publicKeyId: Long,
|
||||||
|
): PGPPublicKey {
|
||||||
|
val decoderStream = PGPUtil.getDecoderStream(publicKeyFileInputStream)
|
||||||
|
val pgpPublicKeyRingCollection = PGPPublicKeyRingCollection(decoderStream, BcKeyFingerprintCalculator())
|
||||||
|
val publicKeyRing = pgpPublicKeyRingCollection.getPublicKeyRing(publicKeyId)
|
||||||
|
?: throw IllegalArgumentException("Can't find public key ring with ID $publicKeyId.")
|
||||||
|
|
||||||
PGPPublicKeyRingCollection(decoderStream, BcKeyFingerprintCalculator()).forEach { keyRing ->
|
return publicKeyRing.getPublicKey(publicKeyId)
|
||||||
keyRing.publicKeys.forEach { publicKey ->
|
?: throw IllegalArgumentException("Can't find public key with ID $publicKeyId.")
|
||||||
if (publicKey.isEncryptionKey) {
|
|
||||||
return publicKey
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw IllegalArgumentException("Can't find encryption key in key ring.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSignature(inputStream: InputStream): PGPSignature {
|
private fun getSignature(inputStream: InputStream): PGPSignature {
|
||||||
val decoderStream = PGPUtil.getDecoderStream(inputStream)
|
val decoderStream = PGPUtil.getDecoderStream(inputStream)
|
||||||
val pgpObjectFactory = PGPObjectFactory(decoderStream, BcKeyFingerprintCalculator())
|
val pgpSignatureList = PGPObjectFactory(decoderStream, BcKeyFingerprintCalculator()).first {
|
||||||
val signatureList = pgpObjectFactory.nextObject() as PGPSignatureList
|
it is PGPSignatureList
|
||||||
|
} as PGPSignatureList
|
||||||
|
|
||||||
return signatureList.first()
|
return pgpSignatureList.first()
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
init {
|
|
||||||
Security.addProvider(BouncyCastleProvider())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user