libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit 5d537b70396d9ef4d3f0982efd9d0bd0e4839a28
parent 3e204b0b4d7d3aa54ecfd6f796ddda456f4a0d4f
Author: ms <ms@taler.net>
Date:   Mon, 14 Feb 2022 10:15:29 +0100

Setting JSON request parser as the default.

Diffstat:
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt | 5+++--
Msandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt | 48+++++++++++++++++++++++++++++++-----------------
Msandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt | 48++++++++++++++++++++----------------------------
Msandbox/src/main/kotlin/tech/libeufin/sandbox/XMLEbicsConverter.kt | 33+++++++++++++++++++++------------
4 files changed, 75 insertions(+), 59 deletions(-)

diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt @@ -24,8 +24,9 @@ package tech.libeufin.nexus.ebics import io.ktor.client.HttpClient import io.ktor.client.features.* -import io.ktor.client.request.post -import io.ktor.http.HttpStatusCode +import io.ktor.client.request.* +import io.ktor.http.* +import io.ktor.util.* import org.slf4j.Logger import org.slf4j.LoggerFactory import tech.libeufin.nexus.NexusError diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt @@ -23,7 +23,7 @@ package tech.libeufin.sandbox import io.ktor.application.* import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode -import io.ktor.request.receiveText +import io.ktor.request.* import io.ktor.response.respond import io.ktor.response.respondText import io.ktor.util.AttributeKey @@ -33,8 +33,6 @@ import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.statements.api.ExposedBlob import org.jetbrains.exposed.sql.transactions.transaction -import org.slf4j.Logger -import org.slf4j.LoggerFactory import org.w3c.dom.Document import tech.libeufin.util.* import tech.libeufin.util.XMLUtil.Companion.signEbicsResponse @@ -46,8 +44,6 @@ import tech.libeufin.util.ebics_s001.UserSignatureData import java.math.BigDecimal import java.security.interfaces.RSAPrivateCrtKey import java.security.interfaces.RSAPublicKey -import java.time.Instant -import java.time.LocalDateTime import java.util.* import java.util.zip.DeflaterInputStream import java.util.zip.InflaterInputStream @@ -124,6 +120,21 @@ suspend fun respondEbicsTransfer( errorText: String, errorCode: String ) { + /** + * Because this handler runs for any error, it could + * handle the case where the Ebics host ID is unknown due + * to an invalid request. Recall: Sandbox is multi-host, and + * which Ebics host was requested belongs to the request document. + * + * Therefore, because any (? Please verify!) Ebics response + * should speak for one Ebics host, we won't respond any Ebics + * type when the Ebics host ID remains unknown due to invalid + * request. Instead, we'll respond plain text: + */ + if (!call.attributes.contains(EbicsHostIdAttribute)) { + call.respondText("Invalid document.", status = HttpStatusCode.BadRequest) + return + } val resp = EbicsResponse.createForUploadWithError( errorText, errorCode, @@ -935,7 +946,7 @@ private suspend fun ApplicationCall.handleEbicsHpb( /** * Find the ebics host corresponding to the one specified in the header. */ -private fun ApplicationCall.ensureEbicsHost(requestHostID: String): EbicsHostPublicInfo { +private fun ensureEbicsHost(requestHostID: String): EbicsHostPublicInfo { return transaction { val ebicsHost = EbicsHostEntity.find { EbicsHostsTable.hostID.upperCase() eq requestHostID.uppercase(Locale.getDefault()) }.firstOrNull() @@ -952,22 +963,19 @@ private fun ApplicationCall.ensureEbicsHost(requestHostID: String): EbicsHostPub ) } } - -private suspend fun ApplicationCall.receiveEbicsXml(): Document { - val body: String = receiveText() - logger.debug("Data received: $body") - val requestDocument: Document? = XMLUtil.parseStringIntoDom(body) +fun receiveEbicsXmlInternal(xmlData: String): Document { + logger.debug("Data received: $xmlData") + val requestDocument: Document? = XMLUtil.parseStringIntoDom(xmlData) if (requestDocument == null || (!XMLUtil.validateFromDom(requestDocument))) { println("Problematic document was: $requestDocument") throw EbicsInvalidXmlError() } - val requestedHostID = requestDocument.getElementsByTagName("HostID") - this.attributes.put( - EbicsHostIdAttribute, - requestedHostID.item(0).textContent - ) return requestDocument } +suspend fun ApplicationCall.receiveEbicsXml(): Document { + val body: String = receiveText() + return receiveEbicsXmlInternal(body) +} private fun makePartnerInfo(subscriber: EbicsSubscriberEntity): EbicsTypes.PartnerInfo { val bankAccount = getBankAccountFromSubscriber(subscriber) @@ -1324,7 +1332,13 @@ private fun makeRequestContext(requestObject: EbicsRequest): RequestContext { } suspend fun ApplicationCall.ebicsweb() { - val requestDocument = receiveEbicsXml() + val requestDocument = this.request.call.receive<Document>() + val requestedHostID = requestDocument.getElementsByTagName("HostID") + this.attributes.put( + EbicsHostIdAttribute, + requestedHostID.item(0).textContent + ) + // val requestDocument = receiveEbicsXml() logger.info("Processing ${requestDocument.documentElement.localName}") when (requestDocument.documentElement.localName) { "ebicsUnsecuredRequest" -> { diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt @@ -370,11 +370,6 @@ inline fun <reified T> Document.toObject(): T { return m.unmarshal(this, T::class.java).value } -fun BigDecimal.signToString(): String { - return if (this.signum() > 0) "+" else "" - // minus sign is added by default already. -} - fun ensureNonNull(param: String?): String { return param ?: throw SandboxError( HttpStatusCode.BadRequest, "Bad ID given: $param" @@ -426,25 +421,22 @@ val sandboxApp: Application.() -> Unit = { logger.info("Enabling CORS (assuming no endpoint uses cookies).") allowCredentials = true } - install(Authentication) { - // Web-based authentication for Bank customers. - form("auth-form") { - userParamName = "username" - passwordParamName = "password" - validate { credentials -> - if (credentials.name == "test") { - UserIdPrincipal(credentials.name) - } else { - null - } - } - } - } install(ContentNegotiation) { - jackson { + register(ContentType.Text.Xml, XMLEbicsConverter()) + /** + * Content type "text" must go to the XML parser + * because Nexus can't set explicitly the Content-Type + * (see https://github.com/ktorio/ktor/issues/1127) to + * "xml" and the request made gets somehow assigned the + * "text/plain" type: */ + register(ContentType.Text.Plain, XMLEbicsConverter()) + /** + * Make jackson the default parser. It runs also when + * the Content-Type request header is missing. */ + jackson(contentType = ContentType.Any) { enable(com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT) setDefaultPrettyPrinter(DefaultPrettyPrinter().apply { - indentArraysWith(com.fasterxml.jackson.core.util.DefaultPrettyPrinter.FixedSpaceIndenter.instance) + indentArraysWith(DefaultPrettyPrinter.FixedSpaceIndenter.instance) indentObjectsWith(DefaultIndenter(" ", "\n")) }) registerModule(KotlinModule(nullisSameAsDefault = true)) @@ -493,8 +485,8 @@ val sandboxApp: Application.() -> Unit = { logger.error("Exception while handling '${call.request.uri}'", cause) call.respondText( "Internal server error.", - io.ktor.http.ContentType.Text.Plain, - io.ktor.http.HttpStatusCode.InternalServerError + ContentType.Text.Plain, + HttpStatusCode.InternalServerError ) } } @@ -530,7 +522,10 @@ val sandboxApp: Application.() -> Unit = { routing { get("/") { - call.respondText("Hello, this is the Sandbox\n", ContentType.Text.Plain) + call.respondText( + "Hello, this is the Sandbox\n", + ContentType.Text.Plain + ) } // Respond with the last statement of the requesting account. @@ -937,9 +932,7 @@ val sandboxApp: Application.() -> Unit = { } catch (e: Exception) { logger.error(e) - if (e !is EbicsRequestError) { - throw EbicsProcessingError("Unmanaged error: $e") - } + throw EbicsProcessingError("Unmanaged error: $e") } return@post } @@ -990,7 +983,6 @@ val sandboxApp: Application.() -> Unit = { call.respond(getJsonFromDemobankConfig(demobank)) return@get } - route("/demobanks/{demobankid}") { // NOTE: TWG assumes that username == bank account label. diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/XMLEbicsConverter.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/XMLEbicsConverter.kt @@ -7,16 +7,22 @@ import io.ktor.http.content.* import io.ktor.request.* import io.ktor.response.* 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 tech.libeufin.util.XMLUtil import java.io.OutputStream +import java.nio.channels.ByteChannel -public class EbicsConverter : ContentConverter { - override suspend fun convertForReceive(context: PipelineContext<ApplicationReceiveRequest, ApplicationCall>): Any { - return context.context.receiveEbicsXml() +class XMLEbicsConverter : ContentConverter { + override suspend fun convertForReceive( + context: PipelineContext<ApplicationReceiveRequest, ApplicationCall>): Any? { + val value = context.subject.value as? ByteReadChannel ?: return null + return withContext(Dispatchers.IO) { + receiveEbicsXmlInternal(value.toInputStream().reader().readText()) + } } - override suspend fun convertForSend( context: PipelineContext<Any, ApplicationCall>, contentType: ContentType, @@ -25,16 +31,19 @@ public class EbicsConverter : ContentConverter { val conv = try { XMLUtil.convertJaxbToString(value) } catch (e: Exception) { - logger.warn("Could not convert XML to string with custom converter.") + /** + * Not always a error: the content negotiation might have + * only checked if this handler could convert the response. + */ + logger.debug("Could not convert XML to string with custom converter.") return null } return OutputStreamContent({ - suspend fun writeAsync(out: OutputStream) { - withContext(Dispatchers.IO) { - out.write(conv.toByteArray()) - } - } - writeAsync(this) - }) + val out = this; + withContext(Dispatchers.IO) { + out.write(conv.toByteArray()) + }}, + contentType.withCharset(context.call.suitableCharset()) + ) } } \ No newline at end of file