summaryrefslogtreecommitdiff
path: root/nexus
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2021-01-20 20:32:29 +0100
committerFlorian Dold <florian@dold.me>2021-01-20 20:32:29 +0100
commitceab4b823f98ea34fdec46784537ad120ae50e73 (patch)
tree7397b2d396a01e685f40f31012c9733450250042 /nexus
parentccf1edff26adc9f4b4a9a16856ab1347f6120205 (diff)
downloadlibeufin-ceab4b823f98ea34fdec46784537ad120ae50e73.tar.gz
libeufin-ceab4b823f98ea34fdec46784537ad120ae50e73.tar.bz2
libeufin-ceab4b823f98ea34fdec46784537ad120ae50e73.zip
rudimentary permissions, code cleanup
Diffstat (limited to 'nexus')
-rw-r--r--nexus/build.gradle5
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/Auth.kt116
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt84
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt1
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt65
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt49
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt206
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/server/RequestBodyDecompression.kt47
8 files changed, 393 insertions, 180 deletions
diff --git a/nexus/build.gradle b/nexus/build.gradle
index e048a85b..e46e6cf6 100644
--- a/nexus/build.gradle
+++ b/nexus/build.gradle
@@ -51,13 +51,13 @@ compileTestKotlin {
}
}
-def ktor_version = "1.3.2"
+def ktor_version = "1.5.0"
def exposed_version = "0.25.1"
dependencies {
// Core language libraries
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
- implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2'
// LibEuFin util library
implementation project(":util")
@@ -72,7 +72,6 @@ dependencies {
implementation "org.glassfish.jaxb:jaxb-runtime:2.3.1"
implementation 'org.apache.santuario:xmlsec:2.1.4'
- //implementation "javax.activation:activation:1.1"
// Compression
implementation group: 'org.apache.commons', name: 'commons-compress', version: '1.20'
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Auth.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Auth.kt
new file mode 100644
index 00000000..cc371a12
--- /dev/null
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Auth.kt
@@ -0,0 +1,116 @@
+package tech.libeufin.nexus
+
+import io.ktor.application.*
+import io.ktor.http.*
+import io.ktor.request.*
+import org.jetbrains.exposed.sql.and
+import org.jetbrains.exposed.sql.transactions.transaction
+import tech.libeufin.nexus.server.Permission
+import tech.libeufin.nexus.server.PermissionQuery
+import tech.libeufin.util.CryptoUtil
+import tech.libeufin.util.base64ToBytes
+import tech.libeufin.util.constructXml
+
+
+/**
+ * This helper function parses a Authorization:-header line, decode the credentials
+ * and returns a pair made of username and hashed (sha256) password. The hashed value
+ * will then be compared with the one kept into the database.
+ */
+private fun extractUserAndPassword(authorizationHeader: String): Pair<String, String> {
+ logger.debug("Authenticating: $authorizationHeader")
+ val (username, password) = try {
+ val split = authorizationHeader.split(" ")
+ val plainUserAndPass = String(base64ToBytes(split[1]), Charsets.UTF_8)
+ plainUserAndPass.split(":")
+ } catch (e: java.lang.Exception) {
+ throw NexusError(
+ HttpStatusCode.BadRequest,
+ "invalid Authorization:-header received"
+ )
+ }
+ return Pair(username, password)
+}
+
+
+/**
+ * Test HTTP basic auth. Throws error if password is wrong,
+ * and makes sure that the user exists in the system.
+ *
+ * @return user entity
+ */
+fun authenticateRequest(request: ApplicationRequest): NexusUserEntity {
+ return transaction {
+ val authorization = request.headers["Authorization"]
+ val headerLine = if (authorization == null) throw NexusError(
+ HttpStatusCode.BadRequest, "Authorization header not found"
+ ) else authorization
+ val (username, password) = extractUserAndPassword(headerLine)
+ val user = NexusUserEntity.find {
+ NexusUsersTable.id eq username
+ }.firstOrNull()
+ if (user == null) {
+ throw NexusError(HttpStatusCode.Unauthorized, "Unknown user '$username'")
+ }
+ if (!CryptoUtil.checkpw(password, user.passwordHash)) {
+ throw NexusError(HttpStatusCode.Forbidden, "Wrong password")
+ }
+ user
+ }
+}
+
+fun requireSuperuser(request: ApplicationRequest): NexusUserEntity {
+ return transaction {
+ val user = authenticateRequest(request)
+ if (!user.superuser) {
+ throw NexusError(HttpStatusCode.Forbidden, "must be superuser")
+ }
+ user
+ }
+}
+
+fun findPermission(p: Permission): NexusPermissionEntity? {
+ return transaction {
+ NexusPermissionEntity.find {
+ ((NexusPermissionsTable.subjectType eq p.subjectType)
+ and (NexusPermissionsTable.subjectId eq p.subjectId)
+ and (NexusPermissionsTable.resourceType eq p.resourceType)
+ and (NexusPermissionsTable.resourceId eq p.resourceId)
+ and (NexusPermissionsTable.permissionName eq p.permissionName))
+
+ }.firstOrNull()
+ }
+}
+
+
+/**
+ * Require that the authenticated user has at least one of the listed permissions.
+ *
+ * Throws a NexusError if the authenticated user for the request doesn't have any of
+ * listed the permissions.
+ */
+fun ApplicationRequest.requirePermission(vararg perms: PermissionQuery) {
+ transaction {
+ val user = authenticateRequest(this@requirePermission)
+ if (user.superuser) {
+ return@transaction
+ }
+ var foundPermission = false
+ for (pr in perms) {
+ val p = Permission("user", user.id.value, pr.resourceType, pr.resourceId, pr.permissionName)
+ val existingPerm = findPermission(p)
+ if (existingPerm != null) {
+ foundPermission = true
+ break
+ }
+ }
+ if (!foundPermission) {
+ val possiblePerms =
+ perms.joinToString(" | ") { "${it.resourceId} ${it.resourceType} ${it.permissionName}" }
+ throw NexusError(
+ HttpStatusCode.Forbidden,
+ "User ${user.id.value} has insufficient permissions (needs $possiblePerms."
+ )
+ }
+ }
+} \ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
index c164aee8..c51926eb 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
@@ -38,7 +38,8 @@ import java.sql.Connection
* whether a pain.001 document was sent or not to the bank is indicated
* in the PAIN-table.
*/
-object TalerRequestedPayments : LongIdTable() {
+object TalerRequestedPaymentsTable : LongIdTable() {
+ val facade = reference("facade", FacadesTable)
val preparedPayment = reference("payment", PaymentInitiationsTable)
val requestUId = text("request_uid")
val amount = text("amount")
@@ -48,21 +49,22 @@ object TalerRequestedPayments : LongIdTable() {
}
class TalerRequestedPaymentEntity(id: EntityID<Long>) : LongEntity(id) {
- companion object : LongEntityClass<TalerRequestedPaymentEntity>(TalerRequestedPayments)
-
- var preparedPayment by PaymentInitiationEntity referencedOn TalerRequestedPayments.preparedPayment
- var requestUId by TalerRequestedPayments.requestUId
- var amount by TalerRequestedPayments.amount
- var exchangeBaseUrl by TalerRequestedPayments.exchangeBaseUrl
- var wtid by TalerRequestedPayments.wtid
- var creditAccount by TalerRequestedPayments.creditAccount
+ companion object : LongEntityClass<TalerRequestedPaymentEntity>(TalerRequestedPaymentsTable)
+
+ var facade by FacadeEntity referencedOn TalerRequestedPaymentsTable.facade
+ var preparedPayment by PaymentInitiationEntity referencedOn TalerRequestedPaymentsTable.preparedPayment
+ var requestUId by TalerRequestedPaymentsTable.requestUId
+ var amount by TalerRequestedPaymentsTable.amount
+ var exchangeBaseUrl by TalerRequestedPaymentsTable.exchangeBaseUrl
+ var wtid by TalerRequestedPaymentsTable.wtid
+ var creditAccount by TalerRequestedPaymentsTable.creditAccount
}
/**
* This is the table of the incoming payments. Entries are merely "pointers" to the
- * entries from the raw payments table. Fixme: name should end with "-table".
+ * entries from the raw payments table.
*/
-object TalerIncomingPayments : LongIdTable() {
+object TalerIncomingPaymentsTable : LongIdTable() {
val payment = reference("payment", NexusBankTransactionsTable)
val reservePublicKey = text("reservePublicKey")
val timestampMs = long("timestampMs")
@@ -70,12 +72,12 @@ object TalerIncomingPayments : LongIdTable() {
}
class TalerIncomingPaymentEntity(id: EntityID<Long>) : LongEntity(id) {
- companion object : LongEntityClass<TalerIncomingPaymentEntity>(TalerIncomingPayments)
+ companion object : LongEntityClass<TalerIncomingPaymentEntity>(TalerIncomingPaymentsTable)
- var payment by NexusBankTransactionEntity referencedOn TalerIncomingPayments.payment
- var reservePublicKey by TalerIncomingPayments.reservePublicKey
- var timestampMs by TalerIncomingPayments.timestampMs
- var debtorPaytoUri by TalerIncomingPayments.debtorPaytoUri
+ var payment by NexusBankTransactionEntity referencedOn TalerIncomingPaymentsTable.payment
+ var reservePublicKey by TalerIncomingPaymentsTable.reservePublicKey
+ var timestampMs by TalerIncomingPaymentsTable.timestampMs
+ var debtorPaytoUri by TalerIncomingPaymentsTable.debtorPaytoUri
}
/**
@@ -93,6 +95,7 @@ object NexusBankMessagesTable : IntIdTable() {
class NexusBankMessageEntity(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<NexusBankMessageEntity>(NexusBankMessagesTable)
+
var bankConnection by NexusBankConnectionEntity referencedOn NexusBankMessagesTable.bankConnection
var messageId by NexusBankMessagesTable.messageId
var code by NexusBankMessagesTable.code
@@ -218,6 +221,7 @@ object OfferedBankAccountsTable : Table() {
val iban = text("iban")
val bankCode = text("bankCode")
val accountHolder = text("holderName")
+
// column below gets defined only WHEN the user imports the bank account.
val imported = reference("imported", NexusBankAccountsTable).nullable()
@@ -237,6 +241,7 @@ object NexusBankAccountsTable : IdTable<String>() {
val lastStatementCreationTimestamp = long("lastStatementCreationTimestamp").nullable()
val lastReportCreationTimestamp = long("lastReportCreationTimestamp").nullable()
val lastNotificationCreationTimestamp = long("lastNotificationCreationTimestamp").nullable()
+
// Highest bank message ID that this bank account is aware of.
val highestSeenBankMessageId = integer("highestSeenBankMessageId")
val pain001Counter = long("pain001counter").default(1)
@@ -244,6 +249,7 @@ object NexusBankAccountsTable : IdTable<String>() {
class NexusBankAccountEntity(id: EntityID<String>) : Entity<String>(id) {
companion object : EntityClass<String, NexusBankAccountEntity>(NexusBankAccountsTable)
+
var accountHolder by NexusBankAccountsTable.accountHolder
var iban by NexusBankAccountsTable.iban
var bankCode by NexusBankAccountsTable.bankCode
@@ -297,6 +303,7 @@ object NexusUsersTable : IdTable<String>() {
class NexusUserEntity(id: EntityID<String>) : Entity<String>(id) {
companion object : EntityClass<String, NexusUserEntity>(NexusUsersTable)
+
var passwordHash by NexusUsersTable.passwordHash
var superuser by NexusUsersTable.superuser
}
@@ -309,6 +316,7 @@ object NexusBankConnectionsTable : IdTable<String>() {
class NexusBankConnectionEntity(id: EntityID<String>) : Entity<String>(id) {
companion object : EntityClass<String, NexusBankConnectionEntity>(NexusBankConnectionsTable)
+
var type by NexusBankConnectionsTable.type
var owner by NexusUserEntity referencedOn NexusBankConnectionsTable.owner
}
@@ -376,6 +384,34 @@ class NexusScheduledTaskEntity(id: EntityID<Int>) : IntEntity(id) {
var prevScheduledExecutionSec by NexusScheduledTasksTable.prevScheduledExecutionSec
}
+/**
+ * Generic permissions table that determines access of a subject
+ * identified by (subjectType, subjectName) to a resource (resourceType, resourceId).
+ *
+ * Subjects are typically of type "user", but this may change in the future.
+ */
+object NexusPermissionsTable : IntIdTable() {
+ val resourceType = text("resourceType")
+ val resourceId = text("resourceId")
+ val subjectType = text("subjectType")
+ val subjectId = text("subjectName")
+ val permissionName = text("permissionName")
+
+ init {
+ uniqueIndex(resourceType, resourceId, subjectType, subjectId, permissionName)
+ }
+}
+
+class NexusPermissionEntity(id: EntityID<Int>) : IntEntity(id) {
+ companion object : IntEntityClass<NexusPermissionEntity>(NexusPermissionsTable)
+
+ var resourceType by NexusPermissionsTable.resourceType
+ var resourceId by NexusPermissionsTable.resourceId
+ var subjectType by NexusPermissionsTable.subjectType
+ var subjectId by NexusPermissionsTable.subjectId
+ var permissionName by NexusPermissionsTable.permissionName
+}
+
fun dbDropTables(dbConnectionString: String) {
Database.connect(dbConnectionString)
transaction {
@@ -385,20 +421,21 @@ fun dbDropTables(dbConnectionString: String) {
NexusEbicsSubscribersTable,
NexusBankAccountsTable,
NexusBankTransactionsTable,
- TalerIncomingPayments,
- TalerRequestedPayments,
+ TalerIncomingPaymentsTable,
+ TalerRequestedPaymentsTable,
NexusBankConnectionsTable,
NexusBankMessagesTable,
FacadesTable,
TalerFacadeStateTable,
NexusScheduledTasksTable,
- OfferedBankAccountsTable
+ OfferedBankAccountsTable,
+ NexusPermissionsTable,
)
}
}
fun dbCreateTables(dbConnectionString: String) {
- Database.connect("$dbConnectionString")
+ Database.connect(dbConnectionString)
TransactionManager.manager.defaultIsolationLevel = Connection.TRANSACTION_SERIALIZABLE
transaction {
SchemaUtils.create(
@@ -407,15 +444,16 @@ fun dbCreateTables(dbConnectionString: String) {
NexusEbicsSubscribersTable,
NexusBankAccountsTable,
NexusBankTransactionsTable,
- TalerIncomingPayments,
- TalerRequestedPayments,
+ TalerIncomingPaymentsTable,
+ TalerRequestedPaymentsTable,
NexusBankConnectionsTable,
NexusBankMessagesTable,
FacadesTable,
TalerFacadeStateTable,
NexusScheduledTasksTable,
OfferedBankAccountsTable,
- NexusScheduledTasksTable
+ NexusScheduledTasksTable,
+ NexusPermissionsTable,
)
}
}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
index 82c3efe1..2d17b8ef 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
@@ -46,7 +46,6 @@ const val DEFAULT_DB_CONNECTION = "jdbc:sqlite:/tmp/libeufin-nexus.sqlite3"
class NexusCommand : CliktCommand() {
init {
- // FIXME: Obtain actual version number!
versionOption(getVersion())
}
override fun run() = Unit
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt
index ddf67979..9e6f6fd1 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt
@@ -197,32 +197,20 @@ private fun getTalerFacadeState(fcid: String): TalerFacadeStateEntity {
HttpStatusCode.NotFound,
"Could not find facade '${fcid}'"
)
- val facadeState = TalerFacadeStateEntity.find {
+ return TalerFacadeStateEntity.find {
TalerFacadeStateTable.facade eq facade.id.value
}.firstOrNull() ?: throw NexusError(
HttpStatusCode.NotFound,
- "Could not find any state for facade: ${fcid}"
+ "Could not find any state for facade: $fcid"
)
- return facadeState
}
private fun getTalerFacadeBankAccount(fcid: String): NexusBankAccountEntity {
- val facade = FacadeEntity.find { FacadesTable.id eq fcid }.firstOrNull() ?: throw NexusError(
- HttpStatusCode.NotFound,
- "Could not find facade '${fcid}'"
- )
- val facadeState = TalerFacadeStateEntity.find {
- TalerFacadeStateTable.facade eq facade.id.value
- }.firstOrNull() ?: throw NexusError(
- HttpStatusCode.NotFound,
- "Could not find any state for facade: ${fcid}"
- )
- val bankAccount = NexusBankAccountEntity.findById(facadeState.bankAccount) ?: throw NexusError(
+ val facadeState = getTalerFacadeState(fcid)
+ return NexusBankAccountEntity.findById(facadeState.bankAccount) ?: throw NexusError(
HttpStatusCode.NotFound,
"Could not find any bank account named ${facadeState.bankAccount}"
)
-
- return bankAccount
}
/**
@@ -232,13 +220,19 @@ private suspend fun talerTransfer(call: ApplicationCall) {
val transferRequest = call.receive<TalerTransferRequest>()
val amountObj = parseAmount(transferRequest.amount)
val creditorObj = parsePayto(transferRequest.credit_account)
+ val facadeId = expectNonNull(call.parameters["fcid"])
val opaqueRowId = transaction {
// FIXME: re-enable authentication (https://bugs.gnunet.org/view.php?id=6703)
// val exchangeUser = authenticateRequest(call.request)
+ call.request.requirePermission(PermissionQuery("facade", facadeId, "facade.talerWireGateway.transfer"))
+ val facade = FacadeEntity.find { FacadesTable.id eq facadeId }.firstOrNull() ?: throw NexusError(
+ HttpStatusCode.NotFound,
+ "Could not find facade '${facadeId}'"
+ )
val creditorData = parsePayto(transferRequest.credit_account)
/** Checking the UID has the desired characteristics */
TalerRequestedPaymentEntity.find {
- TalerRequestedPayments.requestUId eq transferRequest.request_uid
+ TalerRequestedPaymentsTable.requestUId eq transferRequest.request_uid
}.forEach {
if (
(it.amount != transferRequest.amount) or
@@ -251,7 +245,7 @@ private suspend fun talerTransfer(call: ApplicationCall) {
)
}
}
- val exchangeBankAccount = getTalerFacadeBankAccount(expectNonNull(call.parameters["fcid"]))
+ val exchangeBankAccount = getTalerFacadeBankAccount(facadeId)
val pain001 = addPaymentInitiation(
Pain001Data(
creditorIban = creditorData.iban,
@@ -265,6 +259,7 @@ private suspend fun talerTransfer(call: ApplicationCall) {
)
logger.debug("Taler requests payment: ${transferRequest.wtid}")
val row = TalerRequestedPaymentEntity.new {
+ this.facade = facade
preparedPayment = pain001 // not really used/needed, just here to silence warnings
exchangeBaseUrl = transferRequest.exchange_base_url
requestUId = transferRequest.request_uid
@@ -299,11 +294,11 @@ fun roundTimestamp(t: GnunetTimestamp): GnunetTimestamp {
* Serve a /taler/admin/add-incoming
*/
private suspend fun talerAddIncoming(call: ApplicationCall, httpClient: HttpClient): Unit {
+ val facadeID = expectNonNull(call.parameters["fcid"])
+ call.request.requirePermission(PermissionQuery("facade", facadeID, "facade.talerWireGateway.addIncoming"))
val addIncomingData = call.receive<TalerAdminAddIncoming>()
val debtor = parsePayto(addIncomingData.debit_account)
val res = transaction {
- val user = authenticateRequest(call.request)
- val facadeID = expectNonNull(call.parameters["fcid"])
val facadeState = getTalerFacadeState(facadeID)
val facadeBankAccount = getTalerFacadeBankAccount(facadeID)
return@transaction object {
@@ -313,6 +308,7 @@ private suspend fun talerAddIncoming(call: ApplicationCall, httpClient: HttpClie
val facadeHolderName = facadeBankAccount.accountHolder
}
}
+
/** forward the payment information to the sandbox. */
val response = httpClient.post<HttpResponse>(
urlString = "http://localhost:5000/admin/payments",
@@ -366,19 +362,19 @@ private fun ingestIncoming(payment: NexusBankTransactionEntity, txDtls: Transact
val debtorAcct = txDtls.debtorAccount
if (debtorAcct == null) {
// FIXME: Report payment, we can't even send it back
- logger.warn("empty debitor account")
+ logger.warn("empty debtor account")
return
}
val debtorIban = debtorAcct.iban
if (debtorIban == null) {
// FIXME: Report payment, we can't even send it back
- logger.warn("non-iban debitor account")
+ logger.warn("non-iban debtor account")
return
}
val debtorAgent = txDtls.debtorAgent
if (debtorAgent == null) {
// FIXME: Report payment, we can't even send it back
- logger.warn("missing debitor agent")
+ logger.warn("missing debtor agent")
return
}
val reservePub = extractReservePubFromSubject(subject)
@@ -440,6 +436,7 @@ fun ingestTalerTransactions() {
}
when (tx.creditDebitIndicator) {
CreditDebitIndicator.CRDT -> ingestIncoming(it, txDtls = details)
+ else -> Unit
}
lastId = it.id.value
}
@@ -460,6 +457,8 @@ fun ingestTalerTransactions() {
* Handle a /taler/history/outgoing request.
*/
private suspend fun historyOutgoing(call: ApplicationCall) {
+ val facadeId = expectNonNull(call.parameters["fcid"])
+ call.request.requirePermission(PermissionQuery("facade", facadeId, "facade.talerWireGateway.history"))
val param = call.expectUrlParameter("delta")
val delta: Int = try {
param.toInt()
@@ -467,14 +466,12 @@ private suspend fun historyOutgoing(call: ApplicationCall) {
throw EbicsProtocolError(HttpStatusCode.BadRequest, "'${param}' is not Int")
}
val start: Long = handleStartArgument(call.request.queryParameters["start"], delta)
- val startCmpOp = getComparisonOperator(delta, start, TalerRequestedPayments)
+ val startCmpOp = getComparisonOperator(delta, start, TalerRequestedPaymentsTable)
/* retrieve database elements */
val history = TalerOutgoingHistory()
transaction {
- val user = authenticateRequest(call.request)
-
/** Retrieve all the outgoing payments from the _clean Taler outgoing table_ */
- val subscriberBankAccount = getTalerFacadeBankAccount(expectNonNull(call.parameters["fcid"]))
+ val subscriberBankAccount = getTalerFacadeBankAccount(facadeId)
val reqPayments = mutableListOf<TalerRequestedPaymentEntity>()
val reqPaymentsWithUnconfirmed = TalerRequestedPaymentEntity.find {
startCmpOp
@@ -509,9 +506,11 @@ private suspend fun historyOutgoing(call: ApplicationCall) {
}
/**
- * Handle a /taler/history/incoming request.
+ * Handle a /taler-wire-gateway/history/incoming request.
*/
private suspend fun historyIncoming(call: ApplicationCall): Unit {
+ val facadeId = expectNonNull(call.parameters["fcid"])
+ call.request.requirePermission(PermissionQuery("facade", facadeId, "facade.talerWireGateway.history"))
val param = call.expectUrlParameter("delta")
val delta: Int = try {
param.toInt()
@@ -520,7 +519,7 @@ private suspend fun historyIncoming(call: ApplicationCall): Unit {
}
val start: Long = handleStartArgument(call.request.queryParameters["start"], delta)
val history = TalerIncomingHistory()
- val startCmpOp = getComparisonOperator(delta, start, TalerIncomingPayments)
+ val startCmpOp = getComparisonOperator(delta, start, TalerIncomingPaymentsTable)
transaction {
val orderedPayments = TalerIncomingPaymentEntity.find {
startCmpOp
@@ -562,12 +561,14 @@ private fun getCurrency(facadeName: String): String {
}
fun talerFacadeRoutes(route: Route, httpClient: HttpClient) {
+
route.get("/config") {
- val facadeName = ensureNonNull(call.parameters["fcid"])
+ val facadeId = ensureNonNull(call.parameters["fcid"])
+ call.request.requirePermission(PermissionQuery("facade", facadeId, "facade.talerWireGateway.addIncoming"))
call.respond(object {
val version = "0.0.0"
- val name = facadeName
- val currency = getCurrency(facadeName)
+ val name = "taler-wire-gateway"
+ val currency = getCurrency(facadeId)
})
return@get
}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt
index fa827f82..23dccf31 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt
@@ -32,13 +32,9 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.ser.std.StdSerializer
-import tech.libeufin.nexus.NexusScheduledTasksTable
-import tech.libeufin.nexus.NexusScheduledTasksTable.nullable
import tech.libeufin.nexus.iso20022.CamtBankAccountEntry
-import tech.libeufin.nexus.iso20022.CreditDebitIndicator
import tech.libeufin.nexus.iso20022.EntryStatus
import tech.libeufin.util.*
-import java.lang.UnsupportedOperationException
import java.math.BigDecimal
import java.time.Instant
import java.time.ZoneId
@@ -88,13 +84,13 @@ object EbicsDateFormat {
.parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
.parseDefaulting(ChronoField.OFFSET_SECONDS, ZoneId.systemDefault().rules.getOffset(Instant.now()).totalSeconds.toLong())
- .toFormatter()
+ .toFormatter()!!
}
@JsonTypeName("standard-date-range")
class EbicsStandardOrderParamsDateJson(
- val start: String,
- val end: String
+ private val start: String,
+ private val end: String
) : EbicsOrderParamsJson() {
override fun toOrderParams(): EbicsOrderParams {
val dateRange: EbicsDateRange? =
@@ -149,6 +145,29 @@ data class EbicsKeysBackupJson(
val sigBlob: String
)
+enum class PermissionChangeAction(@get:JsonValue val jsonName: String) {
+ GRANT("grant"), REVOKE("revoke")
+}
+
+data class Permission(
+ val subjectType: String,
+ val subjectId: String,
+ val resourceType: String,
+ val resourceId: String,
+ val permissionName: String
+)
+
+data class PermissionQuery(
+ val resourceType: String,
+ val resourceId: String,
+ val permissionName: String,
+)
+
+data class ChangePermissionsRequest(
+ val action: PermissionChangeAction,
+ val permission: Permission
+)
+
enum class FetchLevel(@get:JsonValue val jsonName: String) {
REPORT("report"), STATEMENT("statement"), ALL("all");
}
@@ -270,7 +289,7 @@ data class UserResponse(
)
/** Request type of "POST /users" */
-data class User(
+data class CreateUserRequest(
val username: String,
val password: String
)
@@ -408,20 +427,6 @@ data class CurrencyAmount(
val value: BigDecimal // allows calculations
)
-/**
- * Account entry item as returned by the /bank-accounts/{acctId}/transactions API.
- */
-data class AccountEntryItemJson(
- val nexusEntryId: String,
- val nexusStatusSequenceId: Int,
-
- val entryId: String?,
- val accountServicerRef: String?,
- val creditDebitIndicator: CreditDebitIndicator,
- val entryAmount: CurrencyAmount,
- val status: EntryStatus
-)
-
data class InitiatedPayments(
val initiatedPayments: MutableList<PaymentStatus> = mutableListOf()
)
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
index 3c8d06bc..8c65427a 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
@@ -46,9 +46,13 @@ import io.ktor.response.respondText
import io.ktor.routing.*
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
-import io.ktor.utils.io.ByteReadChannel
+import io.ktor.util.*
+import io.ktor.util.pipeline.*
+import io.ktor.utils.io.*
import io.ktor.utils.io.jvm.javaio.toByteReadChannel
import io.ktor.utils.io.jvm.javaio.toInputStream
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
@@ -66,10 +70,11 @@ import tech.libeufin.util.*
import tech.libeufin.nexus.logger
import java.lang.IllegalArgumentException
import java.net.URLEncoder
-import java.nio.file.Paths
import java.util.zip.InflaterInputStream
-// Return facade state depending on the type.
+/**
+ * Return facade state depending on the type.
+ */
fun getFacadeState(type: String, facade: FacadeEntity): JsonNode {
return transaction {
when (type) {
@@ -83,7 +88,7 @@ fun getFacadeState(type: String, facade: FacadeEntity): JsonNode {
node.put("bankAccount", state.bankAccount)
node
}
- else -> throw NexusError(HttpStatusCode.NotFound, "Facade type ${type} not supported")
+ else -> throw NexusError(HttpStatusCode.NotFound, "Facade type $type not supported")
}
}
}
@@ -91,21 +96,14 @@ fun getFacadeState(type: String, facade: FacadeEntity): JsonNode {
fun ensureNonNull(param: String?): String {
return param ?: throw NexusError(
- HttpStatusCode.BadRequest, "Bad ID given: ${param}"
+ HttpStatusCode.BadRequest, "Bad ID given: $param"
)
}
fun ensureLong(param: String?): Long {
val asString = ensureNonNull(param)
return asString.toLongOrNull() ?: throw NexusError(
- HttpStatusCode.BadRequest, "Parameter is not Long: ${param}"
- )
-}
-
-fun ensureInt(param: String?): Int {
- val asString = ensureNonNull(param)
- return asString.toIntOrNull() ?: throw NexusError(
- HttpStatusCode.BadRequest, "Parameter is not Int: ${param}"
+ HttpStatusCode.BadRequest, "Parameter is not Long: $param"
)
}
@@ -116,52 +114,6 @@ fun <T> expectNonNull(param: T?): T {
)
}
-/**
- * This helper function parses a Authorization:-header line, decode the credentials
- * and returns a pair made of username and hashed (sha256) password. The hashed value
- * will then be compared with the one kept into the database.
- */
-fun extractUserAndPassword(authorizationHeader: String): Pair<String, String> {
- logger.debug("Authenticating: $authorizationHeader")
- val (username, password) = try {
- val split = authorizationHeader.split(" ")
- val plainUserAndPass = String(base64ToBytes(split[1]), Charsets.UTF_8)
- plainUserAndPass.split(":")
- } catch (e: java.lang.Exception) {
- throw NexusError(
- HttpStatusCode.BadRequest,
- "invalid Authorization:-header received"
- )
- }
- return Pair(username, password)
-}
-
-
-/**
- * Test HTTP basic auth. Throws error if password is wrong,
- * and makes sure that the user exists in the system.
- *
- * @param authorization the Authorization:-header line.
- * @return user id
- */
-fun authenticateRequest(request: ApplicationRequest): NexusUserEntity {
- val authorization = request.headers["Authorization"]
- val headerLine = if (authorization == null) throw NexusError(
- HttpStatusCode.BadRequest, "Authorization header not found"
- ) else authorization
- val (username, password) = extractUserAndPassword(headerLine)
- val user = NexusUserEntity.find {
- NexusUsersTable.id eq username
- }.firstOrNull()
- if (user == null) {
- throw NexusError(HttpStatusCode.Unauthorized, "Unknown user '$username'")
- }
- if (!CryptoUtil.checkpw(password, user.passwordHash)) {
- throw NexusError(HttpStatusCode.Forbidden, "Wrong password")
- }
- return user
-}
-
fun ApplicationRequest.hasBody(): Boolean {
if (this.isChunked()) {
@@ -169,11 +121,11 @@ fun ApplicationRequest.hasBody(): Boolean {
}
val contentLengthHeaderStr = this.headers["content-length"]
if (contentLengthHeaderStr != null) {
- try {
+ return try {
val cl = contentLengthHeaderStr.toInt()
- return cl != 0
+ cl != 0
} catch (e: NumberFormatException) {
- return false
+ false
}
}
return false
@@ -287,6 +239,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
)
}
}
+ install(RequestBodyDecompression)
intercept(ApplicationCallPipeline.Fallback) {
if (this.call.response.status() == null) {
call.respondText("Not found (no route matched).\n", ContentType.Text.Plain, HttpStatusCode.NotFound)
@@ -294,34 +247,12 @@ fun serverMain(dbName: String, host: String, port: Int) {
}
}
- // Allow request body compression. Needed by Taler.
- receivePipeline.intercept(ApplicationReceivePipeline.Before) {
- if (this.context.request.headers["Content-Encoding"] == "deflate") {
- logger.debug("About to inflate received data")
- val deflated = this.subject.value as ByteReadChannel
- val inflated = InflaterInputStream(deflated.toInputStream())
- proceedWith(ApplicationReceiveRequest(this.subject.typeInfo, inflated.toByteReadChannel()))
- return@intercept
- }
- proceed()
- return@intercept
- }
startOperationScheduler(client)
routing {
- get("/service-config") {
- call.respond(
- object {
- val dbConn = "sqlite://${Paths.get(dbName).toAbsolutePath()}"
- }
- )
- return@get
- }
-
get("/config") {
call.respond(
object {
- val version = "0.0.0"
- val currency = "EUR"
+ val version = getVersion()
}
)
return@get
@@ -339,7 +270,58 @@ fun serverMain(dbName: String, host: String, port: Int) {
return@get
}
+ get("/permissions") {
+ val resp = object {
+ val permissions = mutableListOf<Permission>()
+ }
+ transaction {
+ requireSuperuser(call.request)
+ NexusPermissionEntity.all().map {
+ resp.permissions.add(
+ Permission(
+ subjectType = it.subjectType,
+ subjectId = it.subjectId,
+ resourceType = it.resourceType,
+ resourceId = it.resourceId,
+ permissionName = it.permissionName,
+ )
+ )
+ }
+ }
+ call.respond(resp)
+ }
+
+ post("/permissions") {
+ val req = call.receive<ChangePermissionsRequest>()
+ transaction {
+ requireSuperuser(call.request)
+ val existingPerm = findPermission(req.permission)
+ when (req.action) {
+ PermissionChangeAction.GRANT -> {
+ if (existingPerm == null) {
+ NexusPermissionEntity.new() {
+ subjectType = req.permission.subjectType
+ subjectId = req.permission.subjectId
+ resourceType = req.permission.resourceType
+ resourceId = req.permission.resourceId
+ permissionName = req.permission.permissionName
+
+ }
+ }
+ }
+ PermissionChangeAction.REVOKE -> {
+ existingPerm?.delete()
+ }
+ }
+ null
+ }
+ call.respond(object {})
+ }
+
get("/users") {
+ transaction {
+ requireSuperuser(call.request)
+ }
val users = transaction {
transaction {
NexusUserEntity.all().map {
@@ -354,19 +336,16 @@ fun serverMain(dbName: String, host: String, port: Int) {
// Add a new ordinary user in the system (requires superuser privileges)
post("/users") {
- val body = call.receiveJson<User>()
+ val body = call.receiveJson<CreateUserRequest>()
transaction {
- val currentUser = authenticateRequest(call.request)
- if (!currentUser.superuser) {
- throw NexusError(HttpStatusCode.Forbidden, "only superuser can do that")
- }
+ requireSuperuser(call.request)
NexusUserEntity.new(body.username) {
passwordHash = CryptoUtil.hashpw(body.password)
superuser = false
}
}
call.respondText(
- "New NEXUS user registered. ID: ${body.username}",
+ "New user '${body.username}' registered",
ContentType.Text.Plain,
HttpStatusCode.OK
)
@@ -374,6 +353,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
}
get("/bank-connection-protocols") {
+ requireSuperuser(call.request)
call.respond(
HttpStatusCode.OK,
BankProtocolsResponse(listOf("ebics", "loopback"))
@@ -406,21 +386,23 @@ fun serverMain(dbName: String, host: String, port: Int) {
return@get
}
post("/bank-accounts/{accountId}/test-camt-ingestion/{type}") {
+ requireSuperuser(call.request)
processCamtMessage(
ensureNonNull(call.parameters["accountId"]),
XMLUtil.parseStringIntoDom(call.receiveText()),
ensureNonNull(call.parameters["type"])
)
- call.respond({ })
+ call.respond(object {})
return@post
}
get("/bank-accounts/{accountid}/schedule") {
+ requireSuperuser(call.request)
val resp = jacksonObjectMapper().createObjectNode()
val ops = jacksonObjectMapper().createObjectNode()
val accountId = ensureNonNull(call.parameters["accountid"])
resp.set<JsonNode>("schedule", ops)
transaction {
- val bankAccount = NexusBankAccountEntity.findById(accountId)
+ NexusBankAccountEntity.findById(accountId)
?: throw NexusError(HttpStatusCode.NotFound, "unknown bank account")
NexusScheduledTaskEntity.find {
(NexusScheduledTasksTable.resourceType eq "bank-account") and
@@ -440,6 +422,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
}
post("/bank-accounts/{accountid}/schedule") {
+ requireSuperuser(call.request)
val schedSpec = call.receive<CreateAccountTaskRequest>()
val accountId = ensureNonNull(call.parameters["accountid"])
transaction {
@@ -486,6 +469,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
}
get("/bank-accounts/{accountId}/schedule/{taskId}") {
+ requireSuperuser(call.request)
val task = transaction {
NexusScheduledTaskEntity.find {
NexusScheduledTasksTable.taskName eq ensureNonNull(call.parameters["taskId"])
@@ -511,6 +495,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
}
delete("/bank-accounts/{accountId}/schedule/{taskId}") {
+ requireSuperuser(call.request)
logger.info("schedule delete requested")
val accountId = ensureNonNull(call.parameters["accountId"])
val taskId = ensureNonNull(call.parameters["taskId"])
@@ -525,14 +510,13 @@ fun serverMain(dbName: String, host: String, port: Int) {
(NexusScheduledTasksTable.resourceId eq accountId)
}.firstOrNull()
- if (oldSchedTask != null) {
- oldSchedTask.delete()
- }
+ oldSchedTask?.delete()
}
call.respond(object {})
}
get("/bank-accounts/{accountid}") {
+ requireSuperuser(call.request)
val accountId = ensureNonNull(call.parameters["accountid"])
val res = transaction {
val user = authenticateRequest(call.request)
@@ -551,17 +535,19 @@ fun serverMain(dbName: String, host: String, port: Int) {
// Submit one particular payment to the bank.
post("/bank-accounts/{accountid}/payment-initiations/{uuid}/submit") {
+ requireSuperuser(call.request)
val uuid = ensureLong(call.parameters["uuid"])
val accountId = ensureNonNull(call.parameters["accountid"])
val res = transaction {
authenticateRequest(call.request)
}
submitPaymentInitiation(client, uuid)
- call.respondText("Payment ${uuid} submitted")
+ call.respondText("Payment $uuid submitted")
return@post
}
post("/bank-accounts/{accountid}/submit-all-payment-initiations") {
+ requireSuperuser(call.request)
val accountId = ensureNonNull(call.parameters["accountid"])
val res = transaction {
authenticateRequest(call.request)
@@ -572,6 +558,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
}
get("/bank-accounts/{accountid}/payment-initiations") {
+ requireSuperuser(call.request)
val ret = InitiatedPayments()
transaction {
val bankAccount = requireBankAccount(call, "accountid")
@@ -603,6 +590,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
// Shows information about one particular payment initiation.
get("/bank-accounts/{accountid}/payment-initiations/{uuid}") {
+ requireSuperuser(call.request)
val res = transaction {
val user = authenticateRequest(call.request)
val paymentInitiation = getPaymentInitiation(ensureLong(call.parameters["uuid"]))
@@ -633,6 +621,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
// Adds a new payment initiation.
post("/bank-accounts/{accountid}/payment-initiations") {
+ requireSuperuser(call.request)
val body = call.receive<CreatePaymentInitiationRequest>()
val accountId = ensureNonNull(call.parameters["accountid"])
val res = transaction {
@@ -666,6 +655,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
// Downloads new transactions from the bank.
post("/bank-accounts/{accountid}/fetch-transactions") {
+ requireSuperuser(call.request)
val accountid = call.parameters["accountid"]
if (accountid == null) {
throw NexusError(
@@ -691,6 +681,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
// Asks list of transactions ALREADY downloaded from the bank.
get("/bank-accounts/{accountid}/transactions") {
+ requireSuperuser(call.request)
val bankAccountId = expectNonNull(call.parameters["accountid"])
val start = call.request.queryParameters["start"]
val end = call.request.queryParameters["end"]
@@ -714,6 +705,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
// Adds a new bank transport.
post("/bank-connections") {
+ requireSuperuser(call.request)
// user exists and is authenticated.
val body = call.receive<CreateBankConnectionRequestJson>()
transaction {
@@ -759,6 +751,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
}
post("/bank-connections/delete-connection") {
+ requireSuperuser(call.request)
val body = call.receive<BankConnectionDeletion>()
transaction {
val conn = NexusBankConnectionEntity.findById(body.bankConnectionId) ?: throw NexusError(
@@ -771,6 +764,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
}
get("/bank-connections") {
+ requireSuperuser(call.request)
val connList = BankConnectionsList()
transaction {
NexusBankConnectionEntity.all().forEach {
@@ -785,10 +779,11 @@ fun serverMain(dbName: String, host: String, port: Int) {
call.respond(connList)
}
- get("/bank-connections/{connid}") {
+ get("/bank-connections/{connectionId}") {
+ requireSuperuser(call.request)
val resp = transaction {
val user = authenticateRequest(call.request)
- val conn = requireBankConnection(call, "connid")
+ val conn = requireBankConnection(call, "connectionId")
when (conn.type) {
"ebics" -> {
getEbicsConnectionDetails(conn)
@@ -805,6 +800,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
}
post("/bank-connections/{connid}/export-backup") {
+ requireSuperuser(call.request)
transaction { authenticateRequest(call.request) }
val body = call.receive<BackupRequestJson>()
val response = run {
@@ -829,6 +825,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
}
post("/bank-connections/{connid}/connect") {
+ requireSuperuser(call.request)
val conn = transaction {
authenticateRequest(call.request)
requireBankConnection(call, "connid")
@@ -842,6 +839,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
}
get("/bank-connections/{connid}/keyletter") {
+ requireSuperuser(call.request)
val conn = transaction {
authenticateRequest(call.request)
requireBankConnection(call, "connid")
@@ -856,6 +854,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
}
get("/bank-connections/{connid}/messages") {
+ requireSuperuser(call.request)
val ret = transaction {
val list = BankMessageList()
val conn = requireBankConnection(call, "connid")
@@ -874,6 +873,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
}
get("/bank-connections/{connid}/messages/{msgid}") {
+ requireSuperuser(call.request)
val ret = transaction {
val msgid = call.parameters["msgid"]
if (msgid == null || msgid == "") {
@@ -889,6 +889,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
}
get("/facades/{fcid}") {
+ requireSuperuser(call.request)
val fcid = ensureNonNull(call.parameters["fcid"])
val ret = transaction {
val f = FacadeEntity.findById(fcid) ?: throw NexusError(
@@ -906,6 +907,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
}
get("/facades") {
+ requireSuperuser(call.request)
val ret = object {
val facades = mutableListOf<FacadeShowInfo>()
}
@@ -929,6 +931,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
}
post("/facades") {
+ requireSuperuser(call.request)
val body = call.receive<FacadeInfo>()
if (body.type != "taler-wire-gateway") throw NexusError(
HttpStatusCode.NotImplemented,
@@ -955,11 +958,13 @@ fun serverMain(dbName: String, host: String, port: Int) {
}
route("/bank-connections/{connid}") {
+
// only ebics specific tasks under this part.
route("/ebics") {
ebicsBankConnectionRoutes(client)
}
post("/fetch-accounts") {
+ requireSuperuser(call.request)
val conn = transaction {
authenticateRequest(call.request)
requireBankConnection(call, "connid")
@@ -978,6 +983,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
// show all the offered accounts (both imported and non)
get("/accounts") {
+ requireSuperuser(call.request)
val ret = OfferedBankAccounts()
transaction {
val conn = requireBankConnection(call, "connid")
@@ -997,8 +1003,10 @@ fun serverMain(dbName: String, host: String, port: Int) {
}
call.respond(ret)
}
+
// import one account into libeufin.
post("/import-account") {
+ requireSuperuser(call.request)
val body = call.receive<ImportBankAccount>()
importBankAccount(call, body.offeredAccountId, body.nexusBankAccountId)
call.respond(object {})
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/RequestBodyDecompression.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/server/RequestBodyDecompression.kt
new file mode 100644
index 00000000..e6fc9cac
--- /dev/null
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/server/RequestBodyDecompression.kt
@@ -0,0 +1,47 @@
+package tech.libeufin.nexus.server
+
+import io.ktor.application.*
+import io.ktor.features.*
+import io.ktor.request.*
+import io.ktor.util.*
+import io.ktor.util.pipeline.*
+import io.ktor.utils.io.*
+import io.ktor.utils.io.jvm.javaio.*
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import java.util.zip.InflaterInputStream
+
+/**
+ * Decompress request bodies.
+ */
+class RequestBodyDecompression private constructor() {
+ companion object Feature :
+ ApplicationFeature<Application, RequestBodyDecompression.Configuration, RequestBodyDecompression> {
+ override val key: AttributeKey<RequestBodyDecompression> = AttributeKey("Request Body Decompression")
+ override fun install(
+ pipeline: Application,
+ configure: RequestBodyDecompression.Configuration.() -> Unit
+ ): RequestBodyDecompression {
+ pipeline.receivePipeline.intercept(ApplicationReceivePipeline.Before) {
+ if (this.context.request.headers["Content-Encoding"] == "deflate") {
+ val deflated = this.subject.value as ByteReadChannel
+ val brc = withContext(Dispatchers.IO) {
+ val inflated = InflaterInputStream(deflated.toInputStream())
+ // False positive in current Kotlin version, we're already in Dispatchers.IO!
+ @Suppress("BlockingMethodInNonBlockingContext") val bytes = inflated.readAllBytes()
+ ByteReadChannel(bytes)
+ }
+ proceedWith(ApplicationReceiveRequest(this.subject.typeInfo, brc))
+ return@intercept
+ }
+ proceed()
+ return@intercept
+ }
+ return RequestBodyDecompression()
+ }
+ }
+
+ class Configuration {
+
+ }
+} \ No newline at end of file