diff --git a/.gitignore b/.gitignore index 674971bb..d376259d 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,6 @@ configuration.toml docker-compose.yml patches-public-key.asc integrations-public-key.asc -node_modules/ \ No newline at end of file +node_modules/ +static/ +about.json \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c4760a0e..145adc5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# [1.1.0-dev.1](https://github.com/ReVanced/revanced-api/compare/v1.0.0...v1.1.0-dev.1) (2024-07-15) + + +### Bug Fixes + +* Don't encode public keys & instead send them raw ([435beae](https://github.com/ReVanced/revanced-api/commit/435beae3831fc8ce161aec676ff20f253b1caf66)) + + +### Features + +* Add static file paths to remove env specific files in resources ([39d0b78](https://github.com/ReVanced/revanced-api/commit/39d0b78c7919f684439b6f052ab3f064159c2a70)) +* Convert static about file to documented route & add key parameter to about route ([dfe6df3](https://github.com/ReVanced/revanced-api/commit/dfe6df3ef6006d06681673bcfaf87c44c40ad446)) + # 1.0.0 (2024-07-13) diff --git a/src/main/resources/app/revanced/api/static/versioned/about.json b/about.example.json similarity index 98% rename from src/main/resources/app/revanced/api/static/versioned/about.json rename to about.example.json index a947c48a..f452fe48 100644 --- a/src/main/resources/app/revanced/api/static/versioned/about.json +++ b/about.example.json @@ -1,6 +1,7 @@ { "name": "ReVanced", "about": "ReVanced was born out of Vanced's discontinuation and it is our goal to continue the legacy of what Vanced left behind. Thanks to ReVanced Patcher, it's possible to create long-lasting patches for nearly any Android app. ReVanced's patching system is designed to allow patches to work on new versions of the apps automatically with bare minimum maintenance.", + "keys": "https://api.revanced.app/keys", "branding": { "logo": "https://raw.githubusercontent.com/ReVanced/revanced-branding/main/assets/revanced-logo/revanced-logo.svg" }, diff --git a/configuration.example.toml b/configuration.example.toml index 0ad3059d..fec352cc 100644 --- a/configuration.example.toml +++ b/configuration.example.toml @@ -1,6 +1,6 @@ organization = "revanced" -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" } +patches = { repository = "revanced-patches", asset-regex = "jar$", signature-asset-regex = "asc$", public-key-file = "patches-public-key.asc", public-key-id = 0 } +integrations = { repository = "revanced-integrations", asset-regex = "apk$", signature-asset-regex = "asc$", public-key-file = "integrations-public-key.asc", public-key-id = 0 } manager = { repository = "revanced-manager", asset-regex = "apk$" } contributors-repositories = [ "revanced-patcher", @@ -17,3 +17,6 @@ cors-allowed-hosts = [ ] endpoint = "https://api.revanced.app" old-api-endpoint = "https://old-api.revanced.app" +static-files-path = "static/root" +versioned-static-files-path = "static/versioned" +about-json-file-path = "about.json" diff --git a/docker-compose.example.yml b/docker-compose.example.yml index a82e58e8..8034e332 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -8,6 +8,8 @@ services: - /data/revanced-api/configuration.toml:/app/configuration.toml - /data/revanced-api/patches-public-key.asc:/app/patches-public-key.asc - /data/revanced-api/integrations-public-key.asc:/app/integrations-public-key.asc + - /data/revanced-api/static:/app/static + - /data/revanced-api/about.json:/app/about.json environment: - COMMAND=start ports: diff --git a/gradle.properties b/gradle.properties index 0cfc02d1..7b01b069 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true kotlin.code.style = official -version = 1.0.0 +version = 1.1.0-dev.1 diff --git a/src/main/kotlin/app/revanced/api/configuration/Extensions.kt b/src/main/kotlin/app/revanced/api/configuration/Extensions.kt index d7546939..4d387216 100644 --- a/src/main/kotlin/app/revanced/api/configuration/Extensions.kt +++ b/src/main/kotlin/app/revanced/api/configuration/Extensions.kt @@ -4,8 +4,12 @@ import io.bkbn.kompendium.core.plugin.NotarizedRoute import io.ktor.http.* import io.ktor.http.content.* import io.ktor.server.application.* +import io.ktor.server.http.content.* import io.ktor.server.plugins.cachingheaders.* import io.ktor.server.response.* +import io.ktor.server.routing.* +import java.io.File +import java.nio.file.Path import kotlin.time.Duration internal suspend fun ApplicationCall.respondOrNotFound(value: Any?) = respond(value ?: HttpStatusCode.NotFound) @@ -25,3 +29,14 @@ internal fun ApplicationCallPipeline.installCache(cacheControl: CacheControl) = internal fun ApplicationCallPipeline.installNotarizedRoute(configure: NotarizedRoute.Config.() -> Unit = {}) = install(NotarizedRoute(), configure) + +internal fun Route.staticFiles( + remotePath: String, + dir: Path, + block: StaticContentConfig.() -> Unit = { + contentType { + ContentType.Application.Json + } + extensions("json") + }, +) = staticFiles(remotePath, dir.toFile(), null, block) diff --git a/src/main/kotlin/app/revanced/api/configuration/Routing.kt b/src/main/kotlin/app/revanced/api/configuration/Routing.kt index fe60ebc0..1e0f4679 100644 --- a/src/main/kotlin/app/revanced/api/configuration/Routing.kt +++ b/src/main/kotlin/app/revanced/api/configuration/Routing.kt @@ -10,7 +10,6 @@ import io.bkbn.kompendium.core.routes.redoc import io.bkbn.kompendium.core.routes.swagger import io.ktor.http.* import io.ktor.server.application.* -import io.ktor.server.http.content.* import io.ktor.server.routing.* import kotlin.time.Duration.Companion.minutes import org.koin.ktor.ext.get as koinGet @@ -27,9 +26,31 @@ internal fun Application.configureRouting() = routing { apiRoute() } - staticResources("/", "/app/revanced/api/static/root") { - contentType { ContentType.Application.Json } - extensions("json") + staticFiles("/", configuration.staticFilesPath) { + contentType { + when (it.extension) { + "json" -> ContentType.Application.Json + "asc" -> ContentType.Text.Plain + "ico" -> ContentType.Image.XIcon + "svg" -> ContentType.Image.SVG + "jpg", "jpeg" -> ContentType.Image.JPEG + "png" -> ContentType.Image.PNG + "gif" -> ContentType.Image.GIF + "mp4" -> ContentType.Video.MP4 + "ogg" -> ContentType.Video.OGG + "mp3" -> ContentType.Audio.MPEG + "css" -> ContentType.Text.CSS + "js" -> ContentType.Application.JavaScript + "html" -> ContentType.Text.Html + "xml" -> ContentType.Application.Xml + "pdf" -> ContentType.Application.Pdf + "zip" -> ContentType.Application.Zip + "gz" -> ContentType.Application.GZip + else -> ContentType.Application.OctetStream + } + } + + extensions("json", "asc") } swagger(pageTitle = "ReVanced API", path = "/") diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt index 4e13cf64..22a654e5 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/ConfigurationRepository.kt @@ -1,7 +1,9 @@ package app.revanced.api.configuration.repository +import app.revanced.api.configuration.schema.APIAbout import app.revanced.api.configuration.services.ManagerService import app.revanced.api.configuration.services.PatchesService +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -10,7 +12,11 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonNamingStrategy +import kotlinx.serialization.json.decodeFromStream import java.io.File +import java.nio.file.Path /** * The repository storing the configuration for the API. @@ -24,6 +30,10 @@ import java.io.File * @property corsAllowedHosts The hosts allowed to make requests to the API. * @property endpoint The endpoint of the API. * @property oldApiEndpoint The endpoint of the old API to proxy requests to. + * @property staticFilesPath The path to the static files to be served under the root path. + * @property versionedStaticFilesPath The path to the static files to be served under a versioned path. + * @property about The path to the json file deserialized to [APIAbout] + * (because com.akuleshov7.ktoml.Toml does not support nested tables). */ @Serializable internal class ConfigurationRepository( @@ -40,6 +50,15 @@ internal class ConfigurationRepository( val endpoint: String, @SerialName("old-api-endpoint") val oldApiEndpoint: String, + @Serializable(with = PathSerializer::class) + @SerialName("static-files-path") + val staticFilesPath: Path, + @Serializable(with = PathSerializer::class) + @SerialName("versioned-static-files-path") + val versionedStaticFilesPath: Path, + @Serializable(with = AboutSerializer::class) + @SerialName("about-json-file-path") + val about: APIAbout, ) { /** * Am asset configuration whose asset is signed. @@ -108,3 +127,23 @@ private object FileSerializer : KSerializer { override fun deserialize(decoder: Decoder) = File(decoder.decodeString()) } + +private object PathSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Path", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: Path) = encoder.encodeString(value.toString()) + + override fun deserialize(decoder: Decoder): Path = Path.of(decoder.decodeString()) +} + +private object AboutSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("APIAbout", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: APIAbout) = error("Serializing APIAbout is not supported") + + @OptIn(ExperimentalSerializationApi::class) + val json = Json { namingStrategy = JsonNamingStrategy.SnakeCase } + + override fun deserialize(decoder: Decoder): APIAbout = + json.decodeFromStream(File(decoder.decodeString()).inputStream()) +} diff --git a/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt b/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt index 5152d654..834f8573 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/ApiRoute.kt @@ -1,9 +1,11 @@ package app.revanced.api.configuration.routes +import app.revanced.api.configuration.* import app.revanced.api.configuration.installCache import app.revanced.api.configuration.installNoCache import app.revanced.api.configuration.installNotarizedRoute import app.revanced.api.configuration.respondOrNotFound +import app.revanced.api.configuration.schema.APIAbout import app.revanced.api.configuration.schema.APIContributable import app.revanced.api.configuration.schema.APIMember import app.revanced.api.configuration.schema.APIRateLimit @@ -13,7 +15,6 @@ import io.bkbn.kompendium.core.metadata.* import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.auth.* -import io.ktor.server.http.content.* import io.ktor.server.plugins.ratelimit.* import io.ktor.server.response.* import io.ktor.server.routing.* @@ -56,6 +57,16 @@ internal fun Route.apiRoute() { } } + route("about") { + installCache(1.days) + + installAboutRouteDocumentation() + + get { + call.respond(apiService.about) + } + } + route("ping") { installNoCache() @@ -75,9 +86,21 @@ internal fun Route.apiRoute() { } } - staticResources("/", "/app/revanced/api/static/versioned") { - contentType { ContentType.Application.Json } - extensions("json") + staticFiles("/", apiService.versionedStaticFilesPath) + } +} + +private fun Route.installAboutRouteDocumentation() = installNotarizedRoute { + tags = setOf("API") + + get = GetInfo.builder { + description("Get information about the API") + summary("Get about") + response { + description("Information about the API") + mediaTypes("application/json") + responseCode(HttpStatusCode.OK) + responseType() } } } diff --git a/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt b/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt index d6d28ef3..b48af9af 100644 --- a/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt +++ b/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt @@ -120,3 +120,55 @@ class APIAssetPublicKeys( val patchesPublicKey: String, val integrationsPublicKey: String, ) + +@Serializable +class APIAbout( + val name: String, + val about: String, + val keys: String, + val branding: Branding?, + val contact: Contact?, + // Using a list instead of a set because set semantics are unnecessary here. + val socials: List?, + val donations: Donations?, +) { + @Serializable + class Branding( + val logo: String, + ) + + @Serializable + class Contact( + val email: String, + ) + + @Serializable + class Social( + val name: String, + val url: String, + val preferred: Boolean? = false, + ) + + @Serializable + class Wallet( + val network: String, + val currencyCode: String, + val address: String, + val preferred: Boolean? = false, + ) + + @Serializable + class Link( + val name: String, + val url: String, + val preferred: Boolean? = false, + ) + + @Serializable + class Donations( + // Using a list instead of a set because set semantics are unnecessary here. + val wallets: List?, + // Using a list instead of a set because set semantics are unnecessary here. + val links: List?, + ) +} diff --git a/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt b/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt index 0a73fff9..5dc9f432 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/ApiService.kt @@ -12,6 +12,9 @@ internal class ApiService( private val backendRepository: BackendRepository, private val configurationRepository: ConfigurationRepository, ) { + val versionedStaticFilesPath = configurationRepository.versionedStaticFilesPath + val about = configurationRepository.about + suspend fun contributors() = withContext(Dispatchers.IO) { configurationRepository.contributorsRepositoryNames.map { async { diff --git a/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt b/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt index 618fc335..a1e7608c 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/PatchesService.kt @@ -7,7 +7,6 @@ import app.revanced.api.configuration.schema.* import app.revanced.library.PatchUtils import app.revanced.patcher.PatchBundleLoader import com.github.benmanes.caffeine.cache.Caffeine -import io.ktor.util.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.ByteArrayOutputStream @@ -113,12 +112,13 @@ internal class PatchesService( } fun publicKeys(): APIAssetPublicKeys { - fun publicKeyBase64(getSignedAssetConfiguration: ConfigurationRepository.() -> ConfigurationRepository.SignedAssetConfiguration) = - configurationRepository.getSignedAssetConfiguration().publicKeyFile.readBytes().encodeBase64() + fun readPublicKey( + getSignedAssetConfiguration: ConfigurationRepository.() -> ConfigurationRepository.SignedAssetConfiguration, + ) = configurationRepository.getSignedAssetConfiguration().publicKeyFile.readText() return APIAssetPublicKeys( - publicKeyBase64 { patches }, - publicKeyBase64 { integrations }, + readPublicKey { patches }, + readPublicKey { integrations }, ) } } diff --git a/src/main/resources/app/revanced/api/static/root/robots.txt b/src/main/resources/app/revanced/api/static/root/robots.txt deleted file mode 100644 index 77470cb3..00000000 --- a/src/main/resources/app/revanced/api/static/root/robots.txt +++ /dev/null @@ -1,2 +0,0 @@ -User-agent: * -Disallow: / \ No newline at end of file