Compare commits

...

12 Commits

Author SHA1 Message Date
semantic-release-bot
db41081155 chore(release): 1.0.0-dev.6 [skip ci]
# [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](c40d50c013))
2024-07-13 00:48:57 +00:00
oSumAtrIX
c40d50c013 feat: Add bio field for team members 2024-07-13 02:47:03 +02:00
semantic-release-bot
b3c82535eb chore(release): 1.0.0-dev.5 [skip ci]
# [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](2ade550d58))
* Only list public members ([97a5d11](97a5d119ec))

### Features

* Add configuration to specify public key id ([ad7d4b2](ad7d4b226f))
* Add manager route ([f814fe5](f814fe5825))
2024-07-12 23:36:46 +00:00
oSumAtrIX
f814fe5825 feat: Add manager route 2024-07-13 01:34:42 +02:00
oSumAtrIX
a34b7c8c31 ci: Correct usage of repository variable 2024-07-13 00:46:19 +02:00
oSumAtrIX
ad7d4b226f feat: Add configuration to specify public key id 2024-07-13 00:28:16 +02:00
oSumAtrIX
97a5d119ec fix: Only list public members 2024-07-13 00:28:16 +02:00
oSumAtrIX
2ade550d58 fix: Move robots.txt to root 2024-07-13 00:28:15 +02:00
semantic-release-bot
f754ebb25c chore(release): 1.0.0-dev.4 [skip ci]
# [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](19ebc827bf))
2024-07-11 20:55:17 +00:00
oSumAtrIX
19ebc827bf feat: List more repository contributors 2024-07-11 22:53:27 +02:00
semantic-release-bot
e92fbdf1f4 chore(release): 1.0.0-dev.3 [skip ci]
# [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](7e99e49af2))
2024-07-11 02:28:50 +00:00
oSumAtrIX
7e99e49af2 fix: Move old API endpoint configuration from env to configuration file 2024-07-11 04:27:02 +02:00
20 changed files with 256 additions and 88 deletions

View File

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

View File

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

View File

@@ -1,3 +1,38 @@
# [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)

View File

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

View File

@@ -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/"

View File

@@ -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.6

View File

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

View File

@@ -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 = "/")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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