Compare commits

...

24 Commits

Author SHA1 Message Date
semantic-release-bot
2d85ce17f6 chore(release): 1.3.0-dev.2 [skip ci]
# [1.3.0-dev.2](https://github.com/ReVanced/revanced-api/compare/v1.3.0-dev.1...v1.3.0-dev.2) (2024-09-27)

### Bug Fixes

* Expire token relative to current date time instead of just time ([c26e129](c26e129bda))
2024-09-27 12:23:28 +00:00
oSumAtrIX
c26e129bda fix: Expire token relative to current date time instead of just time 2024-09-27 14:16:45 +02:00
semantic-release-bot
84ea5e4a41 chore(release): 1.3.0-dev.1 [skip ci]
# [1.3.0-dev.1](https://github.com/ReVanced/revanced-api/compare/v1.2.0...v1.3.0-dev.1) (2024-09-11)

### Features

* Add missing parameter and response documentation ([491533d](491533d3f4))
2024-09-11 14:22:06 +00:00
oSumAtrIX
491533d3f4 feat: Add missing parameter and response documentation 2024-09-09 11:59:03 +02:00
semantic-release-bot
ef5f0b5ddd chore(release): 1.2.0 [skip ci]
# [1.2.0](https://github.com/ReVanced/revanced-api/compare/v1.1.0...v1.2.0) (2024-09-06)

### Bug Fixes

* Add back deprecated routes for backwards compatibility ([9f0eb5b](9f0eb5bfe9))
* Make sure, expected paths in configuration exist ([32bedb7](32bedb7fad))
* Return correct GPG keys url ([#187](https://github.com/ReVanced/revanced-api/issues/187)) ([74e5389](74e53891a1))

### Features

* Move /latest routes to parent ([4e8e83d](4e8e83db1a))
* Respond to all ping request methods ([df116bd](df116bd221))
2024-09-06 23:23:38 +00:00
oSumAtrIX
c0dc763f99 chore: Merge branch dev to main (#188)
This pull request will Merge branch `dev` to `main`.
2024-09-07 03:21:40 +04:00
semantic-release-bot
11327af879 chore(release): 1.2.0-dev.4 [skip ci]
# [1.2.0-dev.4](https://github.com/ReVanced/revanced-api/compare/v1.2.0-dev.3...v1.2.0-dev.4) (2024-09-06)

### Bug Fixes

* Add back deprecated routes for backwards compatibility ([9f0eb5b](9f0eb5bfe9))
* Make sure, expected paths in configuration exist ([32bedb7](32bedb7fad))
2024-09-06 08:27:20 +00:00
oSumAtrIX
9f0eb5bfe9 fix: Add back deprecated routes for backwards compatibility 2024-09-06 12:25:10 +04:00
oSumAtrIX
d2575d5191 docs: Add missing documentation and setup steps 2024-09-06 11:59:33 +04:00
oSumAtrIX
32bedb7fad fix: Make sure, expected paths in configuration exist 2024-09-06 11:40:10 +04:00
semantic-release-bot
d605efd54a chore(release): 1.2.0-dev.3 [skip ci]
# [1.2.0-dev.3](https://github.com/ReVanced/revanced-api/compare/v1.2.0-dev.2...v1.2.0-dev.3) (2024-09-04)

### Bug Fixes

* Return correct GPG keys url ([#187](https://github.com/ReVanced/revanced-api/issues/187)) ([74e5389](74e53891a1))
2024-09-04 15:20:13 +00:00
Ushie
74e53891a1 fix: Return correct GPG keys url (#187)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2024-09-04 17:18:21 +02:00
semantic-release-bot
27b18c62f5 chore(release): 1.2.0-dev.2 [skip ci]
# [1.2.0-dev.2](https://github.com/ReVanced/revanced-api/compare/v1.2.0-dev.1...v1.2.0-dev.2) (2024-08-24)

### Features

* Respond to all ping request methods ([df116bd](df116bd221))
2024-08-24 20:34:53 +00:00
oSumAtrIX
df116bd221 feat: Respond to all ping request methods 2024-08-24 22:32:37 +02:00
semantic-release-bot
2d7b4e7b7f chore(release): 1.2.0-dev.1 [skip ci]
# [1.2.0-dev.1](https://github.com/ReVanced/revanced-api/compare/v1.1.0...v1.2.0-dev.1) (2024-08-16)

### Features

* Move /latest routes to parent ([4e8e83d](4e8e83db1a))
2024-08-16 23:03:43 +00:00
oSumAtrIX
4e8e83db1a feat: Move /latest routes to parent
There is only ever current releases, so a latest route doesn't make sense.
2024-08-17 01:01:33 +02:00
oSumAtrIX
e113daa089 build: Bump dependencies to correctly serialize patches as JSON 2024-08-17 00:53:13 +02:00
oSumAtrIX
170edd3157 build: Bump Gradle 2024-08-17 00:20:07 +02:00
semantic-release-bot
d18e09cba3 chore(release): 1.1.0 [skip ci]
# [1.1.0](https://github.com/ReVanced/revanced-api/compare/v1.0.0...v1.1.0) (2024-07-15)

### Bug Fixes

* Don't encode public keys & instead send them raw ([435beae](435beae383))

### Features

* Add static file paths to remove env specific files in resources ([39d0b78](39d0b78c79))
* Convert static about file to documented route & add key parameter to about route ([dfe6df3](dfe6df3ef6))
2024-07-15 18:20:46 +00:00
oSumAtrIX
bc919b85f5 chore: Merge branch dev to main (#183) 2024-07-15 20:18:36 +02:00
semantic-release-bot
665de7bcd0 chore(release): 1.1.0-dev.1 [skip ci]
# [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](435beae383))

### Features

* Add static file paths to remove env specific files in resources ([39d0b78](39d0b78c79))
* Convert static about file to documented route & add key parameter to about route ([dfe6df3](dfe6df3ef6))
2024-07-15 01:15:03 +00:00
oSumAtrIX
dfe6df3ef6 feat: Convert static about file to documented route & add key parameter to about route 2024-07-15 03:12:39 +02:00
oSumAtrIX
39d0b78c79 feat: Add static file paths to remove env specific files in resources 2024-07-15 02:30:21 +02:00
oSumAtrIX
435beae383 fix: Don't encode public keys & instead send them raw 2024-07-15 01:52:16 +02:00
28 changed files with 497 additions and 149 deletions

4
.gitignore vendored
View File

@@ -42,4 +42,6 @@ configuration.toml
docker-compose.yml
patches-public-key.asc
integrations-public-key.asc
node_modules/
node_modules/
static/
about.json

View File

@@ -1,3 +1,87 @@
# [1.3.0-dev.2](https://github.com/ReVanced/revanced-api/compare/v1.3.0-dev.1...v1.3.0-dev.2) (2024-09-27)
### Bug Fixes
* Expire token relative to current date time instead of just time ([c26e129](https://github.com/ReVanced/revanced-api/commit/c26e129bda09345761f291917f026c13e89a2572))
# [1.3.0-dev.1](https://github.com/ReVanced/revanced-api/compare/v1.2.0...v1.3.0-dev.1) (2024-09-11)
### Features
* Add missing parameter and response documentation ([491533d](https://github.com/ReVanced/revanced-api/commit/491533d3f44ccd716eee80123d0875a05eb9435b))
# [1.2.0](https://github.com/ReVanced/revanced-api/compare/v1.1.0...v1.2.0) (2024-09-06)
### Bug Fixes
* Add back deprecated routes for backwards compatibility ([9f0eb5b](https://github.com/ReVanced/revanced-api/commit/9f0eb5bfe9d0436e76462b9c094f8b1158e04a44))
* Make sure, expected paths in configuration exist ([32bedb7](https://github.com/ReVanced/revanced-api/commit/32bedb7fad3eef8116625964e5e1f0a2543ea2a4))
* Return correct GPG keys url ([#187](https://github.com/ReVanced/revanced-api/issues/187)) ([74e5389](https://github.com/ReVanced/revanced-api/commit/74e53891a17bd3f76f358477e4228550e6f70149))
### Features
* Move /latest routes to parent ([4e8e83d](https://github.com/ReVanced/revanced-api/commit/4e8e83db1a20c76a81967af4e7e3a8634649790a))
* Respond to all ping request methods ([df116bd](https://github.com/ReVanced/revanced-api/commit/df116bd22134c8222c72b28e9387bc9871d3473e))
# [1.2.0-dev.4](https://github.com/ReVanced/revanced-api/compare/v1.2.0-dev.3...v1.2.0-dev.4) (2024-09-06)
### Bug Fixes
* Add back deprecated routes for backwards compatibility ([9f0eb5b](https://github.com/ReVanced/revanced-api/commit/9f0eb5bfe9d0436e76462b9c094f8b1158e04a44))
* Make sure, expected paths in configuration exist ([32bedb7](https://github.com/ReVanced/revanced-api/commit/32bedb7fad3eef8116625964e5e1f0a2543ea2a4))
# [1.2.0-dev.3](https://github.com/ReVanced/revanced-api/compare/v1.2.0-dev.2...v1.2.0-dev.3) (2024-09-04)
### Bug Fixes
* Return correct GPG keys url ([#187](https://github.com/ReVanced/revanced-api/issues/187)) ([74e5389](https://github.com/ReVanced/revanced-api/commit/74e53891a17bd3f76f358477e4228550e6f70149))
# [1.2.0-dev.2](https://github.com/ReVanced/revanced-api/compare/v1.2.0-dev.1...v1.2.0-dev.2) (2024-08-24)
### Features
* Respond to all ping request methods ([df116bd](https://github.com/ReVanced/revanced-api/commit/df116bd22134c8222c72b28e9387bc9871d3473e))
# [1.2.0-dev.1](https://github.com/ReVanced/revanced-api/compare/v1.1.0...v1.2.0-dev.1) (2024-08-16)
### Features
* Move /latest routes to parent ([4e8e83d](https://github.com/ReVanced/revanced-api/commit/4e8e83db1a20c76a81967af4e7e3a8634649790a))
# [1.1.0](https://github.com/ReVanced/revanced-api/compare/v1.0.0...v1.1.0) (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.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)

View File

@@ -96,20 +96,30 @@ so before you can pull the image, you need to [authenticate to the Container reg
1. Create an `.env` file using [.env.example](.env.example) as a template
2. Create a `configuration.toml` file using [configuration.example.toml](configuration.example.toml) as a template
3. Create a `docker-compose.yml` file using [docker-compose.example.yml](docker-compose.example.yml) as a template
4. Run `docker-compose up -d` to start the server
3. Create an `about.json` file using [about.example.json](about.example.json) as a template
4. Create a `docker-compose.yml` file using [docker-compose.example.yml](docker-compose.example.yml) as a template
5. Run `docker-compose up -d` to start the server
### 💻 Docker CLI
1. Create an `.env` file using [.env.example](.env.example) as a template
2. Create a `configuration.toml` file using [configuration.example.toml](configuration.example.toml) as a template
3. Start the container using the following command:
3. Create an `about.json` file using [about.example.json](about.example.json) as a template
4. Start the container using the following command:
```shell
docker run -d --name revanced-api \
# Mount the .env file
-v $(pwd)/.env:/app/.env \
# Mount the configuration.toml file
-v $(pwd)/configuration.toml:/app/configuration.toml \
# Mount the patches public key
-v $(pwd)/patches-public-key.asc:/app/patches-public-key.asc \
# Mount the integrations public key
-v $(pwd)/integrations-public-key.asc:/app/integrations-public-key.asc \
# Mount the static folder
-v $(pwd)/static:/app/static \
# Mount the about.json file
-v $(pwd)/about.json:/app/about.json \
# Mount the persistence folder
-v $(pwd)/persistence:/app/persistence \
# Expose the port 8888
@@ -132,7 +142,8 @@ A Java Runtime Environment (JRE) must be installed.
2. In the same folder, create an `.env` file using [.env.example](.env.example) as a template
3. In the same folder, create a `configuration.toml` file
using [configuration.example.toml](configuration.example.toml) as a template
4. Run `java -jar revanced-api.jar start` to start the server
4. In the same folder, create an `about.json` file using [about.example.json](about.example.json) as a template
5. Run `java -jar revanced-api.jar start` to start the server
### 🛠️ From source
@@ -141,7 +152,8 @@ A Java Development Kit (JDK) and Git must be installed.
1. Run `git clone git@github.com:ReVanced/revanced-api.git` to clone the repository
2. Copy [.env.example](.env.example) to `.env` and fill in the required values
3. Copy [configuration.example.toml](configuration.example.toml) to `configuration.toml` and fill in the required values
4. Run `gradlew run --args=start` to start the server
4. Copy [about.example.json](about.example.json) to `about.json` and fill in the required values
5. Run `gradlew run --args=start` to start the server
## 📚 Everything else

View File

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

View File

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

View File

@@ -8,8 +8,10 @@ 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:
- 8888:8888
- "8888:8888"
restart: unless-stopped

View File

@@ -1,4 +1,4 @@
org.gradle.parallel = true
org.gradle.caching = true
kotlin.code.style = official
version = 1.0.0
version = 1.3.0-dev.2

View File

@@ -10,8 +10,8 @@ ktor = "2.3.7"
ktoml = "0.5.2"
picocli = "4.7.6"
datetime = "0.6.0"
revanced-patcher = "19.3.1"
revanced-library = "2.3.0"
revanced-patcher = "20.0.0"
revanced-library = "3.0.1-dev.1"
caffeine = "3.1.8"
bouncy-castle = "1.78.1"

Binary file not shown.

View File

@@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=a4b4158601f8636cdeeab09bd76afb640030bb5b144aafe261a5e8af027dc612
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

5
gradlew vendored
View File

@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@@ -84,7 +86,8 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum

2
gradlew.bat vendored
View File

@@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################

View File

@@ -7,7 +7,7 @@ import app.revanced.api.configuration.repository.GitHubBackendRepository
import app.revanced.api.configuration.services.*
import app.revanced.api.configuration.services.AnnouncementService
import app.revanced.api.configuration.services.ApiService
import app.revanced.api.configuration.services.AuthService
import app.revanced.api.configuration.services.AuthenticationService
import app.revanced.api.configuration.services.OldApiService
import app.revanced.api.configuration.services.PatchesService
import com.akuleshov7.ktoml.Toml
@@ -116,11 +116,11 @@ fun Application.configureDependencies(
val jwtSecret = dotenv["JWT_SECRET"]
val issuer = dotenv["JWT_ISSUER"]
val validityInMin = dotenv["JWT_VALIDITY_IN_MIN"].toInt()
val validityInMin = dotenv["JWT_VALIDITY_IN_MIN"].toLong()
val authSHA256DigestString = dotenv["AUTH_SHA256_DIGEST"]
AuthService(issuer, validityInMin, jwtSecret, authSHA256DigestString)
AuthenticationService(issuer, validityInMin, jwtSecret, authSHA256DigestString)
}
single {
val configuration = get<ConfigurationRepository>()

View File

@@ -1,11 +1,16 @@
package app.revanced.api.configuration
import io.bkbn.kompendium.core.metadata.MethodInfo
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 +30,22 @@ 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<File>.() -> Unit = {
contentType {
ContentType.Application.Json
}
extensions("json")
},
) = staticFiles(remotePath, dir.toFile(), null, block)
internal fun MethodInfo.Builder<*>.canRespondUnauthorized() {
canRespond {
responseCode(HttpStatusCode.Unauthorized)
description("Unauthorized")
responseType<Unit>()
}
}

View File

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

View File

@@ -1,9 +1,17 @@
package app.revanced.api.configuration
import app.revanced.api.configuration.services.AuthService
import app.revanced.api.configuration.services.AuthenticationService
import io.ktor.server.application.*
import io.ktor.server.auth.*
import org.koin.ktor.ext.get
fun Application.configureSecurity() {
get<AuthService>().configureSecurity(this)
val authenticationService = get<AuthenticationService>()
install(Authentication) {
with(authenticationService) {
jwt()
digest()
}
}
}

View File

@@ -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,12 @@ 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
import kotlin.io.path.createDirectories
/**
* The repository storing the configuration for the API.
@@ -24,6 +31,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,7 +51,21 @@ 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,
) {
init {
staticFilesPath.createDirectories()
versionedStaticFilesPath.createDirectories()
}
/**
* Am asset configuration whose asset is signed.
*
@@ -108,3 +133,23 @@ private object FileSerializer : KSerializer<File> {
override fun deserialize(decoder: Decoder) = File(decoder.decodeString())
}
private object PathSerializer : KSerializer<Path> {
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<APIAbout> {
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())
}

View File

@@ -98,7 +98,7 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) {
gpgKeys =
BackendMember.GpgKeys(
ids = gpgKeys.map { it.keyId },
url = "https://api.github.com/users/${user.login}.gpg",
url = "https://github.com/${user.login}.gpg",
),
)
}

View File

@@ -1,5 +1,6 @@
package app.revanced.api.configuration.routes
import app.revanced.api.configuration.canRespondUnauthorized
import app.revanced.api.configuration.installCache
import app.revanced.api.configuration.installNotarizedRoute
import app.revanced.api.configuration.respondOrNotFound
@@ -8,10 +9,7 @@ import app.revanced.api.configuration.schema.APIAnnouncementArchivedAt
import app.revanced.api.configuration.schema.APIResponseAnnouncement
import app.revanced.api.configuration.schema.APIResponseAnnouncementId
import app.revanced.api.configuration.services.AnnouncementService
import io.bkbn.kompendium.core.metadata.DeleteInfo
import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.metadata.PatchInfo
import io.bkbn.kompendium.core.metadata.PostInfo
import io.bkbn.kompendium.core.metadata.*
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.oas.payload.Parameter
import io.ktor.http.*
@@ -138,9 +136,19 @@ internal fun Route.announcementsRoute() = route("announcements") {
}
}
private val authHeaderParameter = Parameter(
name = "Authorization",
`in` = Parameter.Location.header,
schema = TypeDefinition.STRING,
required = true,
examples = mapOf("Bearer authentication" to Parameter.Example("Bearer abc123")),
)
private fun Route.installAnnouncementRouteDocumentation() = installNotarizedRoute {
tags = setOf("Announcements")
parameters = listOf(authHeaderParameter)
post = PostInfo.builder {
description("Create a new announcement")
summary("Create announcement")
@@ -153,6 +161,7 @@ private fun Route.installAnnouncementRouteDocumentation() = installNotarizedRout
responseCode(HttpStatusCode.OK)
responseType<Unit>()
}
canRespondUnauthorized()
}
}
@@ -239,6 +248,7 @@ private fun Route.installAnnouncementArchiveRouteDocumentation() = installNotari
description = "The date and time the announcement to be archived",
required = false,
),
authHeaderParameter,
)
post = PostInfo.builder {
@@ -249,6 +259,7 @@ private fun Route.installAnnouncementArchiveRouteDocumentation() = installNotari
responseCode(HttpStatusCode.OK)
responseType<Unit>()
}
canRespondUnauthorized()
}
}
@@ -263,6 +274,7 @@ private fun Route.installAnnouncementUnarchiveRouteDocumentation() = installNota
description = "The id of the announcement to unarchive",
required = true,
),
authHeaderParameter,
)
post = PostInfo.builder {
@@ -273,6 +285,7 @@ private fun Route.installAnnouncementUnarchiveRouteDocumentation() = installNota
responseCode(HttpStatusCode.OK)
responseType<Unit>()
}
canRespondUnauthorized()
}
}
@@ -287,6 +300,7 @@ private fun Route.installAnnouncementIdRouteDocumentation() = installNotarizedRo
description = "The id of the announcement to update",
required = true,
),
authHeaderParameter,
)
patch = PatchInfo.builder {
@@ -301,6 +315,7 @@ private fun Route.installAnnouncementIdRouteDocumentation() = installNotarizedRo
responseCode(HttpStatusCode.OK)
responseType<Unit>()
}
canRespondUnauthorized()
}
delete = DeleteInfo.builder {
@@ -311,6 +326,7 @@ private fun Route.installAnnouncementIdRouteDocumentation() = installNotarizedRo
responseCode(HttpStatusCode.OK)
responseType<Unit>()
}
canRespondUnauthorized()
}
}

View File

@@ -1,19 +1,23 @@
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.repository.ConfigurationRepository
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
import app.revanced.api.configuration.services.ApiService
import app.revanced.api.configuration.services.AuthService
import app.revanced.api.configuration.services.AuthenticationService
import io.bkbn.kompendium.core.metadata.*
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.oas.payload.Parameter
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.*
@@ -22,7 +26,7 @@ import org.koin.ktor.ext.get as koinGet
internal fun Route.apiRoute() {
val apiService = koinGet<ApiService>()
val authService = koinGet<AuthService>()
val authenticationService = koinGet<AuthenticationService>()
rateLimit(RateLimitName("strong")) {
authenticate("auth-digest") {
@@ -30,7 +34,7 @@ internal fun Route.apiRoute() {
installTokenRouteDocumentation()
get {
call.respond(authService.newToken())
call.respond(authenticationService.newToken())
}
}
}
@@ -56,12 +60,22 @@ internal fun Route.apiRoute() {
}
}
route("about") {
installCache(1.days)
installAboutRouteDocumentation()
get {
call.respond(apiService.about)
}
}
route("ping") {
installNoCache()
installPingRouteDocumentation()
head {
handle {
call.respond(HttpStatusCode.NoContent)
}
}
@@ -75,9 +89,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<APIAbout>()
}
}
}
@@ -142,16 +168,38 @@ private fun Route.installContributorsRouteDocumentation() = installNotarizedRout
}
private fun Route.installTokenRouteDocumentation() = installNotarizedRoute {
val configuration = koinGet<ConfigurationRepository>()
tags = setOf("API")
get = GetInfo.builder {
description("Get a new authorization token")
summary("Get authorization token")
parameters(
Parameter(
name = "Authorization",
`in` = Parameter.Location.header,
schema = TypeDefinition.STRING,
required = true,
examples = mapOf(
"Digest access authentication" to Parameter.Example(
value = "Digest " +
"username=\"ReVanced\", " +
"realm=\"ReVanced\", " +
"nonce=\"abc123\", " +
"uri=\"/v${configuration.apiVersion}/token\", " +
"algorithm=SHA-256, " +
"response=\"yxz456\"",
),
), // Provide an example for the header
),
)
response {
description("The authorization token")
mediaTypes("application/json")
responseCode(HttpStatusCode.OK)
responseType<String>()
}
canRespondUnauthorized()
}
}

View File

@@ -14,33 +14,41 @@ import io.ktor.server.routing.*
import org.koin.ktor.ext.get as koinGet
internal fun Route.managerRoute() = route("manager") {
configure()
// TODO: Remove this deprecated route eventually.
route("latest") {
configure(deprecated = true)
}
}
private fun Route.configure(deprecated: Boolean = false) {
val managerService = koinGet<ManagerService>()
route("latest") {
installLatestManagerRouteDocumentation()
installManagerRouteDocumentation(deprecated)
rateLimit(RateLimitName("weak")) {
get {
call.respond(managerService.latestRelease())
}
route("version") {
installManagerVersionRouteDocumentation(deprecated)
rateLimit(RateLimitName("weak")) {
get {
call.respond(managerService.latestRelease())
}
route("version") {
installLatestManagerVersionRouteDocumentation()
get {
call.respond(managerService.latestVersion())
}
call.respond(managerService.latestVersion())
}
}
}
}
private fun Route.installLatestManagerRouteDocumentation() = installNotarizedRoute {
private fun Route.installManagerRouteDocumentation(deprecated: Boolean) = installNotarizedRoute {
tags = setOf("Manager")
get = GetInfo.builder {
description("Get the latest manager release")
summary("Get latest manager release")
if (deprecated) isDeprecated()
description("Get the current manager release")
summary("Get current manager release")
response {
description("The latest manager release")
mediaTypes("application/json")
@@ -50,14 +58,15 @@ private fun Route.installLatestManagerRouteDocumentation() = installNotarizedRou
}
}
private fun Route.installLatestManagerVersionRouteDocumentation() = installNotarizedRoute {
private fun Route.installManagerVersionRouteDocumentation(deprecated: Boolean) = installNotarizedRoute {
tags = setOf("Manager")
get = GetInfo.builder {
description("Get the latest manager release version")
summary("Get latest manager release version")
if (deprecated) isDeprecated()
description("Get the current manager release version")
summary("Get current manager release version")
response {
description("The latest manager release version")
description("The current manager release version")
mediaTypes("application/json")
responseCode(HttpStatusCode.OK)
responseType<APIReleaseVersion>()

View File

@@ -17,32 +17,39 @@ import kotlin.time.Duration.Companion.days
import org.koin.ktor.ext.get as koinGet
internal fun Route.patchesRoute() = route("patches") {
configure()
// TODO: Remove this deprecated route eventually.
route("latest") {
configure(deprecated = true)
}
}
private fun Route.configure(deprecated: Boolean = false) {
val patchesService = koinGet<PatchesService>()
route("latest") {
installLatestPatchesRouteDocumentation()
installPatchesRouteDocumentation(deprecated)
rateLimit(RateLimitName("weak")) {
get {
call.respond(patchesService.latestRelease())
}
route("version") {
installLatestPatchesVersionRouteDocumentation()
get {
call.respond(patchesService.latestVersion())
}
}
rateLimit(RateLimitName("weak")) {
get {
call.respond(patchesService.latestRelease())
}
rateLimit(RateLimitName("strong")) {
route("list") {
installLatestPatchesListRouteDocumentation()
route("version") {
installPatchesVersionRouteDocumentation(deprecated)
get {
call.respondBytes(ContentType.Application.Json) { patchesService.list() }
}
get {
call.respond(patchesService.latestVersion())
}
}
}
rateLimit(RateLimitName("strong")) {
route("list") {
installPatchesListRouteDocumentation(deprecated)
get {
call.respondBytes(ContentType.Application.Json) { patchesService.list() }
}
}
}
@@ -51,7 +58,7 @@ internal fun Route.patchesRoute() = route("patches") {
route("keys") {
installCache(356.days)
installPatchesPublicKeyRouteDocumentation()
installPatchesPublicKeyRouteDocumentation(deprecated)
get {
call.respond(patchesService.publicKeys())
@@ -60,14 +67,15 @@ internal fun Route.patchesRoute() = route("patches") {
}
}
private fun Route.installLatestPatchesRouteDocumentation() = installNotarizedRoute {
private fun Route.installPatchesRouteDocumentation(deprecated: Boolean) = installNotarizedRoute {
tags = setOf("Patches")
get = GetInfo.builder {
description("Get the latest patches release")
summary("Get latest patches release")
if (deprecated) isDeprecated()
description("Get the current patches release")
summary("Get current patches release")
response {
description("The latest patches release")
description("The current patches release")
mediaTypes("application/json")
responseCode(HttpStatusCode.OK)
responseType<APIRelease<APIPatchesAsset>>()
@@ -75,14 +83,15 @@ private fun Route.installLatestPatchesRouteDocumentation() = installNotarizedRou
}
}
private fun Route.installLatestPatchesVersionRouteDocumentation() = installNotarizedRoute {
private fun Route.installPatchesVersionRouteDocumentation(deprecated: Boolean) = installNotarizedRoute {
tags = setOf("Patches")
get = GetInfo.builder {
description("Get the latest patches release version")
summary("Get latest patches release version")
if (deprecated) isDeprecated()
description("Get the current patches release version")
summary("Get current patches release version")
response {
description("The latest patches release version")
description("The current patches release version")
mediaTypes("application/json")
responseCode(HttpStatusCode.OK)
responseType<APIReleaseVersion>()
@@ -90,12 +99,13 @@ private fun Route.installLatestPatchesVersionRouteDocumentation() = installNotar
}
}
private fun Route.installLatestPatchesListRouteDocumentation() = installNotarizedRoute {
private fun Route.installPatchesListRouteDocumentation(deprecated: Boolean) = installNotarizedRoute {
tags = setOf("Patches")
get = GetInfo.builder {
description("Get the list of patches from the latest patches release")
summary("Get list of patches from latest patches release")
if (deprecated) isDeprecated()
description("Get the list of patches from the current patches release")
summary("Get list of patches from current patches release")
response {
description("The list of patches")
mediaTypes("application/json")
@@ -105,10 +115,11 @@ private fun Route.installLatestPatchesListRouteDocumentation() = installNotarize
}
}
private fun Route.installPatchesPublicKeyRouteDocumentation() = installNotarizedRoute {
private fun Route.installPatchesPublicKeyRouteDocumentation(deprecated: Boolean) = installNotarizedRoute {
tags = setOf("Patches")
get = GetInfo.builder {
if (deprecated) isDeprecated()
description("Get the public keys for verifying patches and integrations assets")
summary("Get patches and integrations public keys")
response {

View File

@@ -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<Social>?,
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<Wallet>?,
// Using a list instead of a set because set semantics are unnecessary here.
val links: List<Link>?,
)
}

View File

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

View File

@@ -1,49 +0,0 @@
package app.revanced.api.configuration.services
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import java.util.*
import kotlin.text.HexFormat
import kotlin.time.Duration.Companion.minutes
internal class AuthService private constructor(
private val issuer: String,
private val validityInMin: Int,
private val jwtSecret: String,
private val authSHA256Digest: ByteArray,
) {
@OptIn(ExperimentalStdlibApi::class)
constructor(issuer: String, validityInMin: Int, jwtSecret: String, authSHA256DigestString: String) : this(
issuer,
validityInMin,
jwtSecret,
authSHA256DigestString.hexToByteArray(HexFormat.Default),
)
val configureSecurity: Application.() -> Unit = {
install(Authentication) {
jwt("jwt") {
realm = "ReVanced"
verifier(JWT.require(Algorithm.HMAC256(jwtSecret)).withIssuer(issuer).build())
}
digest("auth-digest") {
realm = "ReVanced"
algorithmName = "SHA-256"
digestProvider { _, _ ->
authSHA256Digest
}
}
}
}
fun newToken(): String = JWT.create()
.withIssuer(issuer)
.withExpiresAt(Date(System.currentTimeMillis() + validityInMin.minutes.inWholeMilliseconds))
.sign(Algorithm.HMAC256(jwtSecret))
}

View File

@@ -0,0 +1,53 @@
package app.revanced.api.configuration.services
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import java.time.Instant
import java.time.temporal.ChronoUnit
import kotlin.text.HexFormat
internal class AuthenticationService private constructor(
private val issuer: String,
private val validityInMin: Long,
private val jwtSecret: String,
private val authSHA256Digest: ByteArray,
) {
@OptIn(ExperimentalStdlibApi::class)
constructor(issuer: String, validityInMin: Long, jwtSecret: String, authSHA256DigestString: String) : this(
issuer,
validityInMin,
jwtSecret,
authSHA256DigestString.hexToByteArray(HexFormat.Default),
)
fun AuthenticationConfig.jwt() {
jwt("jwt") {
realm = "ReVanced"
verifier(JWT.require(Algorithm.HMAC256(jwtSecret)).withIssuer(issuer).build())
}
}
fun AuthenticationConfig.digest() {
digest("auth-digest") {
realm = "ReVanced"
algorithmName = "SHA-256"
digestProvider { _, _ ->
authSHA256Digest
}
}
}
fun newToken(): String {
val issuedAt = Instant.now()
return JWT.create()
.withIssuer(issuer)
.withIssuedAt(issuedAt)
.withExpiresAt(issuedAt.plus(validityInMin, ChronoUnit.MINUTES))
.sign(Algorithm.HMAC256(jwtSecret))
}
}

View File

@@ -4,10 +4,9 @@ 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.*
import app.revanced.library.PatchUtils
import app.revanced.patcher.PatchBundleLoader
import app.revanced.library.serializeTo
import app.revanced.patcher.patch.loadPatchesFromJar
import com.github.benmanes.caffeine.cache.Caffeine
import io.ktor.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.ByteArrayOutputStream
@@ -95,7 +94,7 @@ internal class PatchesService(
configurationRepository.patches.publicKeyId,
)
) {
PatchBundleLoader.Jar(patchesFile)
loadPatchesFromJar(setOf(patchesFile))
} else {
// Use an empty set of patches if the signature is invalid.
emptySet()
@@ -104,7 +103,7 @@ internal class PatchesService(
patchesFile.delete()
ByteArrayOutputStream().use { stream ->
PatchUtils.Json.serialize(patches, outputStream = stream)
patches.serializeTo(outputStream = stream)
stream.toByteArray()
}
@@ -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 },
)
}
}

View File

@@ -1,2 +0,0 @@
User-agent: *
Disallow: /