Compare commits

..

21 Commits

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

### Bug Fixes

* Add missing OK response to routes ([1181be1](1181be12e2))
* Respond with JSON when returning token ([1e3e46f](1e3e46ff4f))
* Specify a validation function to fix authentication ([53c3600](53c36002e9))

### Features

* Customize logging level through environment variable ([8b17d88](8b17d8894d))
* Improve response info description wording ([977d252](977d252497))
2024-09-29 21:19:18 +00:00
oSumAtrIX
1181be12e2 fix: Add missing OK response to routes 2024-09-29 23:15:35 +02:00
oSumAtrIX
53c36002e9 fix: Specify a validation function to fix authentication 2024-09-29 23:13:13 +02:00
oSumAtrIX
8b17d8894d feat: Customize logging level through environment variable 2024-09-29 01:03:49 +02:00
oSumAtrIX
1e3e46ff4f fix: Respond with JSON when returning token 2024-09-27 20:19:27 +02:00
oSumAtrIX
977d252497 feat: Improve response info description wording 2024-09-27 19:18:32 +02:00
oSumAtrIX
96bcd7719a chore: Remove unnecessary JWT token field 2024-09-27 19:16:34 +02:00
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
19 changed files with 285 additions and 112 deletions

View File

@@ -1,3 +1,69 @@
# [1.3.0-dev.3](https://github.com/ReVanced/revanced-api/compare/v1.3.0-dev.2...v1.3.0-dev.3) (2024-09-29)
### Bug Fixes
* Add missing OK response to routes ([1181be1](https://github.com/ReVanced/revanced-api/commit/1181be12e2223b245019f64570bc8f7bef4e7dc2))
* Respond with JSON when returning token ([1e3e46f](https://github.com/ReVanced/revanced-api/commit/1e3e46ff4f7c12569b88fcd1bc252aeb5a611b63))
* Specify a validation function to fix authentication ([53c3600](https://github.com/ReVanced/revanced-api/commit/53c36002e9af89aa5fed71f831470b42d5d777c9))
### Features
* Customize logging level through environment variable ([8b17d88](https://github.com/ReVanced/revanced-api/commit/8b17d8894db8db4a168c30be50af91c04e173e14))
* Improve response info description wording ([977d252](https://github.com/ReVanced/revanced-api/commit/977d25249738b24cb6a3530543349efe1d71a9ba))
# [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) # [1.2.0-dev.1](https://github.com/ReVanced/revanced-api/compare/v1.1.0...v1.2.0-dev.1) (2024-08-16)

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 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 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 3. Create an `about.json` file using [about.example.json](about.example.json) as a template
4. Run `docker-compose up -d` to start the server 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 ### 💻 Docker CLI
1. Create an `.env` file using [.env.example](.env.example) as a template 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 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 ```shell
docker run -d --name revanced-api \ docker run -d --name revanced-api \
# Mount the .env file # Mount the .env file
-v $(pwd)/.env:/app/.env \ -v $(pwd)/.env:/app/.env \
# Mount the configuration.toml file # Mount the configuration.toml file
-v $(pwd)/configuration.toml:/app/configuration.toml \ -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 # Mount the persistence folder
-v $(pwd)/persistence:/app/persistence \ -v $(pwd)/persistence:/app/persistence \
# Expose the port 8888 # 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 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 3. In the same folder, create a `configuration.toml` file
using [configuration.example.toml](configuration.example.toml) as a template 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 ### 🛠️ 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 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 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 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 ## 📚 Everything else

View File

@@ -13,5 +13,5 @@ services:
environment: environment:
- COMMAND=start - COMMAND=start
ports: ports:
- 8888:8888 - "8888:8888"
restart: unless-stopped restart: unless-stopped

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.2.0-dev.1 version = 1.3.0-dev.3

View File

@@ -1,6 +1,7 @@
package app.revanced.api.command package app.revanced.api.command
import app.revanced.api.configuration.* import app.revanced.api.configuration.*
import io.github.cdimascio.dotenv.Dotenv
import io.ktor.server.engine.* import io.ktor.server.engine.*
import io.ktor.server.jetty.* import io.ktor.server.jetty.*
import picocli.CommandLine import picocli.CommandLine
@@ -33,6 +34,8 @@ internal object StartAPICommand : Runnable {
private var configFile = File("configuration.toml") private var configFile = File("configuration.toml")
override fun run() { override fun run() {
Dotenv.configure().systemProperties().load()
embeddedServer(Jetty, port, host) { embeddedServer(Jetty, port, host) {
configureDependencies(configFile) configureDependencies(configFile)
configureHTTP() configureHTTP()

View File

@@ -7,12 +7,11 @@ import app.revanced.api.configuration.repository.GitHubBackendRepository
import app.revanced.api.configuration.services.* import app.revanced.api.configuration.services.*
import app.revanced.api.configuration.services.AnnouncementService import app.revanced.api.configuration.services.AnnouncementService
import app.revanced.api.configuration.services.ApiService 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.OldApiService
import app.revanced.api.configuration.services.PatchesService import app.revanced.api.configuration.services.PatchesService
import com.akuleshov7.ktoml.Toml import com.akuleshov7.ktoml.Toml
import com.akuleshov7.ktoml.source.decodeFromStream import com.akuleshov7.ktoml.source.decodeFromStream
import io.github.cdimascio.dotenv.Dotenv
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.engine.okhttp.* import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.* import io.ktor.client.plugins.*
@@ -38,11 +37,7 @@ import java.io.File
fun Application.configureDependencies( fun Application.configureDependencies(
configFile: File, configFile: File,
) { ) {
val globalModule = module { val miscModule = module {
single {
Dotenv.configure().load()
}
factory { params -> factory { params ->
val defaultRequestUri: String = params.get<String>() val defaultRequestUri: String = params.get<String>()
val configBlock = params.getOrNull<(HttpClientConfig<OkHttpConfig>.() -> Unit)>() ?: {} val configBlock = params.getOrNull<(HttpClientConfig<OkHttpConfig>.() -> Unit)>() ?: {}
@@ -72,7 +67,7 @@ fun Application.configureDependencies(
) )
} }
get<Dotenv>()["BACKEND_API_TOKEN"]?.let { System.getProperty("BACKEND_API_TOKEN")?.let {
install(Auth) { install(Auth) {
bearer { bearer {
loadTokens { loadTokens {
@@ -98,12 +93,10 @@ fun Application.configureDependencies(
} }
single { single {
val dotenv = get<Dotenv>()
TransactionManager.defaultDatabase = Database.connect( TransactionManager.defaultDatabase = Database.connect(
url = dotenv["DB_URL"], url = System.getProperty("DB_URL"),
user = dotenv["DB_USER"], user = System.getProperty("DB_USER"),
password = dotenv["DB_PASSWORD"], password = System.getProperty("DB_PASSWORD"),
) )
AnnouncementRepository() AnnouncementRepository()
@@ -112,15 +105,13 @@ fun Application.configureDependencies(
val serviceModule = module { val serviceModule = module {
single { single {
val dotenv = get<Dotenv>() val jwtSecret = System.getProperty("JWT_SECRET")
val issuer = System.getProperty("JWT_ISSUER")
val validityInMin = System.getProperty("JWT_VALIDITY_IN_MIN").toLong()
val jwtSecret = dotenv["JWT_SECRET"] val authSHA256DigestString = System.getProperty("AUTH_SHA256_DIGEST")
val issuer = dotenv["JWT_ISSUER"]
val validityInMin = dotenv["JWT_VALIDITY_IN_MIN"].toInt()
val authSHA256DigestString = dotenv["AUTH_SHA256_DIGEST"] AuthenticationService(issuer, validityInMin, jwtSecret, authSHA256DigestString)
AuthService(issuer, validityInMin, jwtSecret, authSHA256DigestString)
} }
single { single {
val configuration = get<ConfigurationRepository>() val configuration = get<ConfigurationRepository>()
@@ -140,7 +131,7 @@ fun Application.configureDependencies(
install(Koin) { install(Koin) {
modules( modules(
globalModule, miscModule,
repositoryModule, repositoryModule,
serviceModule, serviceModule,
) )

View File

@@ -1,5 +1,6 @@
package app.revanced.api.configuration package app.revanced.api.configuration
import io.bkbn.kompendium.core.metadata.MethodInfo
import io.bkbn.kompendium.core.plugin.NotarizedRoute import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.ktor.http.* import io.ktor.http.*
import io.ktor.http.content.* import io.ktor.http.content.*
@@ -40,3 +41,11 @@ internal fun Route.staticFiles(
extensions("json") extensions("json")
}, },
) = staticFiles(remotePath, dir.toFile(), null, block) ) = staticFiles(remotePath, dir.toFile(), null, block)
internal fun MethodInfo.Builder<*>.canRespondUnauthorized() {
canRespond {
responseCode(HttpStatusCode.Unauthorized)
description("Unauthorized")
responseType<Unit>()
}
}

View File

@@ -16,7 +16,7 @@ fun Application.configureHTTP() {
configurationRepository.corsAllowedHosts.forEach { host -> configurationRepository.corsAllowedHosts.forEach { host ->
allowHost( allowHost(
host = host, host = host,
schemes = listOf("http", "https") schemes = listOf("http", "https"),
) )
} }
} }

View File

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

View File

@@ -17,6 +17,7 @@ import kotlinx.serialization.json.JsonNamingStrategy
import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.decodeFromStream
import java.io.File import java.io.File
import java.nio.file.Path import java.nio.file.Path
import kotlin.io.path.createDirectories
/** /**
* The repository storing the configuration for the API. * The repository storing the configuration for the API.
@@ -60,6 +61,11 @@ internal class ConfigurationRepository(
@SerialName("about-json-file-path") @SerialName("about-json-file-path")
val about: APIAbout, val about: APIAbout,
) { ) {
init {
staticFilesPath.createDirectories()
versionedStaticFilesPath.createDirectories()
}
/** /**
* Am asset configuration whose asset is signed. * Am asset configuration whose asset is signed.
* *

View File

@@ -98,7 +98,7 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) {
gpgKeys = gpgKeys =
BackendMember.GpgKeys( BackendMember.GpgKeys(
ids = gpgKeys.map { it.keyId }, 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 package app.revanced.api.configuration.routes
import app.revanced.api.configuration.canRespondUnauthorized
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.respondOrNotFound 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.APIResponseAnnouncement
import app.revanced.api.configuration.schema.APIResponseAnnouncementId import app.revanced.api.configuration.schema.APIResponseAnnouncementId
import app.revanced.api.configuration.services.AnnouncementService import app.revanced.api.configuration.services.AnnouncementService
import io.bkbn.kompendium.core.metadata.DeleteInfo import io.bkbn.kompendium.core.metadata.*
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.json.schema.definition.TypeDefinition import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.oas.payload.Parameter import io.bkbn.kompendium.oas.payload.Parameter
import io.ktor.http.* import io.ktor.http.*
@@ -96,6 +94,8 @@ internal fun Route.announcementsRoute() = route("announcements") {
post<APIAnnouncement> { announcement -> post<APIAnnouncement> { announcement ->
announcementService.new(announcement) announcementService.new(announcement)
call.respond(HttpStatusCode.OK)
} }
route("{id}") { route("{id}") {
@@ -105,12 +105,16 @@ internal fun Route.announcementsRoute() = route("announcements") {
val id: Int by call.parameters val id: Int by call.parameters
announcementService.update(id, announcement) announcementService.update(id, announcement)
call.respond(HttpStatusCode.OK)
} }
delete { delete {
val id: Int by call.parameters val id: Int by call.parameters
announcementService.delete(id) announcementService.delete(id)
call.respond(HttpStatusCode.OK)
} }
route("archive") { route("archive") {
@@ -121,6 +125,8 @@ internal fun Route.announcementsRoute() = route("announcements") {
val archivedAt = call.receiveNullable<APIAnnouncementArchivedAt>()?.archivedAt val archivedAt = call.receiveNullable<APIAnnouncementArchivedAt>()?.archivedAt
announcementService.archive(id, archivedAt) announcementService.archive(id, archivedAt)
call.respond(HttpStatusCode.OK)
} }
} }
@@ -131,6 +137,8 @@ internal fun Route.announcementsRoute() = route("announcements") {
val id: Int by call.parameters val id: Int by call.parameters
announcementService.unarchive(id) announcementService.unarchive(id)
call.respond(HttpStatusCode.OK)
} }
} }
} }
@@ -138,9 +146,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 { private fun Route.installAnnouncementRouteDocumentation() = installNotarizedRoute {
tags = setOf("Announcements") tags = setOf("Announcements")
parameters = listOf(authHeaderParameter)
post = PostInfo.builder { post = PostInfo.builder {
description("Create a new announcement") description("Create a new announcement")
summary("Create announcement") summary("Create announcement")
@@ -149,10 +167,11 @@ private fun Route.installAnnouncementRouteDocumentation() = installNotarizedRout
description("The new announcement") description("The new announcement")
} }
response { response {
description("When the announcement was created") description("The announcement is created")
responseCode(HttpStatusCode.OK) responseCode(HttpStatusCode.OK)
responseType<Unit>() responseType<Unit>()
} }
canRespondUnauthorized()
} }
} }
@@ -239,16 +258,18 @@ private fun Route.installAnnouncementArchiveRouteDocumentation() = installNotari
description = "The date and time the announcement to be archived", description = "The date and time the announcement to be archived",
required = false, required = false,
), ),
authHeaderParameter,
) )
post = PostInfo.builder { post = PostInfo.builder {
description("Archive an announcement") description("Archive an announcement")
summary("Archive announcement") summary("Archive announcement")
response { response {
description("When the announcement was archived") description("The announcement is archived")
responseCode(HttpStatusCode.OK) responseCode(HttpStatusCode.OK)
responseType<Unit>() responseType<Unit>()
} }
canRespondUnauthorized()
} }
} }
@@ -263,16 +284,18 @@ private fun Route.installAnnouncementUnarchiveRouteDocumentation() = installNota
description = "The id of the announcement to unarchive", description = "The id of the announcement to unarchive",
required = true, required = true,
), ),
authHeaderParameter,
) )
post = PostInfo.builder { post = PostInfo.builder {
description("Unarchive an announcement") description("Unarchive an announcement")
summary("Unarchive announcement") summary("Unarchive announcement")
response { response {
description("When announcement was unarchived") description("The announcement is unarchived")
responseCode(HttpStatusCode.OK) responseCode(HttpStatusCode.OK)
responseType<Unit>() responseType<Unit>()
} }
canRespondUnauthorized()
} }
} }
@@ -287,6 +310,7 @@ private fun Route.installAnnouncementIdRouteDocumentation() = installNotarizedRo
description = "The id of the announcement to update", description = "The id of the announcement to update",
required = true, required = true,
), ),
authHeaderParameter,
) )
patch = PatchInfo.builder { patch = PatchInfo.builder {
@@ -297,20 +321,22 @@ private fun Route.installAnnouncementIdRouteDocumentation() = installNotarizedRo
description("The new announcement") description("The new announcement")
} }
response { response {
description("When announcement was updated") description("The announcement is updated")
responseCode(HttpStatusCode.OK) responseCode(HttpStatusCode.OK)
responseType<Unit>() responseType<Unit>()
} }
canRespondUnauthorized()
} }
delete = DeleteInfo.builder { delete = DeleteInfo.builder {
description("Delete an announcement") description("Delete an announcement")
summary("Delete announcement") summary("Delete announcement")
response { response {
description("When the announcement was deleted") description("The announcement is deleted")
responseCode(HttpStatusCode.OK) responseCode(HttpStatusCode.OK)
responseType<Unit>() responseType<Unit>()
} }
canRespondUnauthorized()
} }
} }

View File

@@ -4,14 +4,14 @@ import app.revanced.api.configuration.*
import app.revanced.api.configuration.installCache import app.revanced.api.configuration.installCache
import app.revanced.api.configuration.installNoCache import app.revanced.api.configuration.installNoCache
import app.revanced.api.configuration.installNotarizedRoute import app.revanced.api.configuration.installNotarizedRoute
import app.revanced.api.configuration.repository.ConfigurationRepository
import app.revanced.api.configuration.respondOrNotFound import app.revanced.api.configuration.respondOrNotFound
import app.revanced.api.configuration.schema.APIAbout import app.revanced.api.configuration.schema.*
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.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.core.metadata.*
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.oas.payload.Parameter
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.auth.* import io.ktor.server.auth.*
@@ -23,7 +23,7 @@ import org.koin.ktor.ext.get as koinGet
internal fun Route.apiRoute() { internal fun Route.apiRoute() {
val apiService = koinGet<ApiService>() val apiService = koinGet<ApiService>()
val authService = koinGet<AuthService>() val authenticationService = koinGet<AuthenticationService>()
rateLimit(RateLimitName("strong")) { rateLimit(RateLimitName("strong")) {
authenticate("auth-digest") { authenticate("auth-digest") {
@@ -31,7 +31,7 @@ internal fun Route.apiRoute() {
installTokenRouteDocumentation() installTokenRouteDocumentation()
get { get {
call.respond(authService.newToken()) call.respond(authenticationService.newToken())
} }
} }
} }
@@ -72,7 +72,7 @@ internal fun Route.apiRoute() {
installPingRouteDocumentation() installPingRouteDocumentation()
head { handle {
call.respond(HttpStatusCode.NoContent) call.respond(HttpStatusCode.NoContent)
} }
} }
@@ -165,16 +165,38 @@ private fun Route.installContributorsRouteDocumentation() = installNotarizedRout
} }
private fun Route.installTokenRouteDocumentation() = installNotarizedRoute { private fun Route.installTokenRouteDocumentation() = installNotarizedRoute {
val configuration = koinGet<ConfigurationRepository>()
tags = setOf("API") tags = setOf("API")
get = GetInfo.builder { get = GetInfo.builder {
description("Get a new authorization token") description("Get a new authorization token")
summary("Get 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 { response {
description("The authorization token") description("The authorization token")
mediaTypes("application/json") mediaTypes("application/json")
responseCode(HttpStatusCode.OK) responseCode(HttpStatusCode.OK)
responseType<String>() responseType<APIToken>()
} }
canRespondUnauthorized()
} }
} }

View File

@@ -14,9 +14,18 @@ import io.ktor.server.routing.*
import org.koin.ktor.ext.get as koinGet import org.koin.ktor.ext.get as koinGet
internal fun Route.managerRoute() = route("manager") { 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>() val managerService = koinGet<ManagerService>()
installManagerRouteDocumentation() installManagerRouteDocumentation(deprecated)
rateLimit(RateLimitName("weak")) { rateLimit(RateLimitName("weak")) {
get { get {
@@ -24,7 +33,7 @@ internal fun Route.managerRoute() = route("manager") {
} }
route("version") { route("version") {
installManagerVersionRouteDocumentation() installManagerVersionRouteDocumentation(deprecated)
get { get {
call.respond(managerService.latestVersion()) call.respond(managerService.latestVersion())
@@ -33,10 +42,11 @@ internal fun Route.managerRoute() = route("manager") {
} }
} }
private fun Route.installManagerRouteDocumentation() = installNotarizedRoute { private fun Route.installManagerRouteDocumentation(deprecated: Boolean) = installNotarizedRoute {
tags = setOf("Manager") tags = setOf("Manager")
get = GetInfo.builder { get = GetInfo.builder {
if (deprecated) isDeprecated()
description("Get the current manager release") description("Get the current manager release")
summary("Get current manager release") summary("Get current manager release")
response { response {
@@ -48,10 +58,11 @@ private fun Route.installManagerRouteDocumentation() = installNotarizedRoute {
} }
} }
private fun Route.installManagerVersionRouteDocumentation() = installNotarizedRoute { private fun Route.installManagerVersionRouteDocumentation(deprecated: Boolean) = installNotarizedRoute {
tags = setOf("Manager") tags = setOf("Manager")
get = GetInfo.builder { get = GetInfo.builder {
if (deprecated) isDeprecated()
description("Get the current manager release version") description("Get the current manager release version")
summary("Get current manager release version") summary("Get current manager release version")
response { response {

View File

@@ -17,9 +17,18 @@ 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.patchesRoute() = route("patches") { 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>() val patchesService = koinGet<PatchesService>()
installPatchesRouteDocumentation() installPatchesRouteDocumentation(deprecated)
rateLimit(RateLimitName("weak")) { rateLimit(RateLimitName("weak")) {
get { get {
@@ -27,7 +36,7 @@ internal fun Route.patchesRoute() = route("patches") {
} }
route("version") { route("version") {
installPatchesVersionRouteDocumentation() installPatchesVersionRouteDocumentation(deprecated)
get { get {
call.respond(patchesService.latestVersion()) call.respond(patchesService.latestVersion())
@@ -37,7 +46,7 @@ internal fun Route.patchesRoute() = route("patches") {
rateLimit(RateLimitName("strong")) { rateLimit(RateLimitName("strong")) {
route("list") { route("list") {
installPatchesListRouteDocumentation() installPatchesListRouteDocumentation(deprecated)
get { get {
call.respondBytes(ContentType.Application.Json) { patchesService.list() } call.respondBytes(ContentType.Application.Json) { patchesService.list() }
@@ -49,7 +58,7 @@ internal fun Route.patchesRoute() = route("patches") {
route("keys") { route("keys") {
installCache(356.days) installCache(356.days)
installPatchesPublicKeyRouteDocumentation() installPatchesPublicKeyRouteDocumentation(deprecated)
get { get {
call.respond(patchesService.publicKeys()) call.respond(patchesService.publicKeys())
@@ -58,10 +67,11 @@ internal fun Route.patchesRoute() = route("patches") {
} }
} }
private fun Route.installPatchesRouteDocumentation() = installNotarizedRoute { private fun Route.installPatchesRouteDocumentation(deprecated: Boolean) = installNotarizedRoute {
tags = setOf("Patches") tags = setOf("Patches")
get = GetInfo.builder { get = GetInfo.builder {
if (deprecated) isDeprecated()
description("Get the current patches release") description("Get the current patches release")
summary("Get current patches release") summary("Get current patches release")
response { response {
@@ -73,10 +83,11 @@ private fun Route.installPatchesRouteDocumentation() = installNotarizedRoute {
} }
} }
private fun Route.installPatchesVersionRouteDocumentation() = installNotarizedRoute { private fun Route.installPatchesVersionRouteDocumentation(deprecated: Boolean) = installNotarizedRoute {
tags = setOf("Patches") tags = setOf("Patches")
get = GetInfo.builder { get = GetInfo.builder {
if (deprecated) isDeprecated()
description("Get the current patches release version") description("Get the current patches release version")
summary("Get current patches release version") summary("Get current patches release version")
response { response {
@@ -88,10 +99,11 @@ private fun Route.installPatchesVersionRouteDocumentation() = installNotarizedRo
} }
} }
private fun Route.installPatchesListRouteDocumentation() = installNotarizedRoute { private fun Route.installPatchesListRouteDocumentation(deprecated: Boolean) = installNotarizedRoute {
tags = setOf("Patches") tags = setOf("Patches")
get = GetInfo.builder { get = GetInfo.builder {
if (deprecated) isDeprecated()
description("Get the list of patches from the current patches release") description("Get the list of patches from the current patches release")
summary("Get list of patches from current patches release") summary("Get list of patches from current patches release")
response { response {
@@ -103,10 +115,11 @@ private fun Route.installPatchesListRouteDocumentation() = installNotarizedRoute
} }
} }
private fun Route.installPatchesPublicKeyRouteDocumentation() = installNotarizedRoute { private fun Route.installPatchesPublicKeyRouteDocumentation(deprecated: Boolean) = installNotarizedRoute {
tags = setOf("Patches") tags = setOf("Patches")
get = GetInfo.builder { get = GetInfo.builder {
if (deprecated) isDeprecated()
description("Get the public keys for verifying patches and integrations assets") description("Get the public keys for verifying patches and integrations assets")
summary("Get patches and integrations public keys") summary("Get patches and integrations public keys")
response { response {

View File

@@ -172,3 +172,6 @@ class APIAbout(
val links: List<Link>?, val links: List<Link>?,
) )
} }
@Serializable
class APIToken(val token: String)

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,52 @@
package app.revanced.api.configuration.services
import app.revanced.api.configuration.schema.APIToken
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())
// This is required and not optional. Authentication will fail if this is not present.
validate { JWTPrincipal(it.payload) }
}
}
fun AuthenticationConfig.digest() {
digest("auth-digest") {
realm = "ReVanced"
algorithmName = "SHA-256"
digestProvider { _, _ ->
authSHA256Digest
}
}
}
fun newToken() = APIToken(
JWT.create()
.withIssuer(issuer)
.withExpiresAt(Instant.now().plus(validityInMin, ChronoUnit.MINUTES))
.sign(Algorithm.HMAC256(jwtSecret)),
)
}

View File

@@ -4,7 +4,7 @@
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder> </encoder>
</appender> </appender>
<root level="info"> <root level="\${LOG_LEVEL:-INFO}">
<appender-ref ref="STDOUT"/> <appender-ref ref="STDOUT"/>
</root> </root>
</configuration> </configuration>