commit 323e08712336ae3b7ef01fc5c5f813cb8182ab50 parent 0f7962af1566a37684ec31093e9964b0b9ad1569 Author: Marcello Stanisci <stanisci.m@gmail.com> Date: Tue, 8 Oct 2019 12:27:55 +0200 init nexus Diffstat:
42 files changed, 1023 insertions(+), 937 deletions(-)
diff --git a/.idea/modules.xml b/.idea/modules.xml @@ -1,8 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="ProjectModuleManager"> - <modules> - <module fileurl="file://$PROJECT_DIR$/.idea/modules/libeufin.iml" filepath="$PROJECT_DIR$/.idea/modules/libeufin.iml" /> - </modules> - </component> -</project> -\ No newline at end of file diff --git a/build.gradle b/build.gradle @@ -1,59 +1,48 @@ plugins { - id 'java' - id 'application' - id 'org.jetbrains.kotlin.jvm' version '1.3.50' + id "org.jetbrains.kotlin.jvm" version "1.3.50" } -version '1.0-SNAPSHOT' +allprojects { -sourceCompatibility = 1.8 -targetCompatibility = 1.8 - -repositories { - mavenCentral() - jcenter() -} - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" - implementation "io.ktor:ktor-gson:1.1.5" - // compile group: 'io.ktor', name: 'ktor-gson', version: '0.9.0' - compile "org.jetbrains.exposed:exposed:0.17.3" - compile "io.ktor:ktor-server-netty:1.2.4" - compile "ch.qos.logback:logback-classic:1.2.3" - compile group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.1' - compile('javax.xml.bind:jaxb-api:2.3.0') - compile('javax.activation:activation:1.1') - compile('org.glassfish.jaxb:jaxb-runtime:2.3.0') - testCompile group: 'junit', name: 'junit', version: '4.12' -} - -compileKotlin { - kotlinOptions { - jvmTarget = "1.8" - } -} -compileTestKotlin { - kotlinOptions { - jvmTarget = "1.8" + repositories { + mavenCentral() + jcenter() } } -application { - mainClassName = "tech.libeufin.MainKt" -} +subprojects { -jar { - manifest { - attributes "Main-Class": "tech.libeufin.MainKt" + version '1.0-SNAPSHOT' + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + compileKotlin { + kotlinOptions { + jvmTarget = "1.8" + } } - from { - configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } + compileTestKotlin { + kotlinOptions { + jvmTarget = "1.8" + } } -} -test { - useJUnit() -} + apply plugin : "application" + apply plugin : "java" + + dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + implementation "io.ktor:ktor-gson:1.1.5" + compile group: 'io.ktor', name: 'ktor-gson', version: '0.9.0' + compile "org.jetbrains.exposed:exposed:0.17.3" + compile "io.ktor:ktor-server-netty:1.2.4" + compile "ch.qos.logback:logback-classic:1.2.3" + compile group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.1' + compile "javax.xml.bind:jaxb-api:2.3.0" + compile "javax.activation:activation:1.1" + compile "org.glassfish.jaxb:jaxb-runtime:2.3.0" + testCompile group: 'junit', name: 'junit', version: '4.12' + } + +} +\ No newline at end of file diff --git a/nexus/build.gradle b/nexus/build.gradle @@ -0,0 +1,22 @@ +plugins { + id "org.jetbrains.kotlin.jvm" +} + +application { + mainClassName = "tech.libeufin.nexus.MainKt" +} + +dependencies { + implementation project(":sandbox") +} + +jar { + manifest { + attributes "Main-Class": "tech.libeufin.nexus.MainKt" + + } + + from { + configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } + } +} +\ No newline at end of file diff --git a/nexus/src/main/kotlin/Main.kt b/nexus/src/main/kotlin/Main.kt @@ -0,0 +1,60 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2019 Stanisci and Dold. + + * LibEuFin is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation; either version 3, or + * (at your option) any later version. + + * LibEuFin is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General + * Public License for more details. + + * You should have received a copy of the GNU Affero General Public + * License along with LibEuFin; see the file COPYING. If not, see + * <http://www.gnu.org/licenses/> + */ + +package tech.libeufin.nexus + +import io.ktor.application.call +import io.ktor.application.install +import io.ktor.features.CallLogging +import io.ktor.features.ContentNegotiation +import io.ktor.gson.gson +import io.ktor.response.respondText +import io.ktor.routing.get +import io.ktor.routing.post +import io.ktor.routing.routing +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import java.text.DateFormat + +fun main() { + val server = embeddedServer(Netty, port = 5001) { + + install(CallLogging) + install(ContentNegotiation) { + gson { + setDateFormat(DateFormat.LONG) + setPrettyPrinting() + } + } + + routing { + get("/") { + call.respondText("Hello by Nexus!\n") + return@get + } + + post("/nexus") { + call.respondText("Not implemented!\n") + return@post + } + + } + } + server.start(wait = true) +} diff --git a/sandbox/build.gradle b/sandbox/build.gradle @@ -0,0 +1,19 @@ +plugins { + // FIXME: must go into the global file. + id "org.jetbrains.kotlin.jvm" +} + +application { + mainClassName = "tech.libeufin.sandbox.MainKt" +} + +jar { + manifest { + attributes "Main-Class": "tech.libeufin.sandbox.MainKt" + + } + + from { + configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } + } +} +\ No newline at end of file diff --git a/src/main/java/tech/libeufin/messages/HEVRequestDataType.java b/sandbox/src/main/java/tech/libeufin/messages/HEVRequestDataType.java diff --git a/src/main/java/tech/libeufin/messages/HEVResponseDataType.java b/sandbox/src/main/java/tech/libeufin/messages/HEVResponseDataType.java diff --git a/src/main/java/tech/libeufin/messages/ObjectFactory.java b/sandbox/src/main/java/tech/libeufin/messages/ObjectFactory.java diff --git a/src/main/java/tech/libeufin/messages/SystemReturnCodeType.java b/sandbox/src/main/java/tech/libeufin/messages/SystemReturnCodeType.java diff --git a/sandbox/src/main/kotlin/tech/libeufin/DB.kt b/sandbox/src/main/kotlin/tech/libeufin/DB.kt @@ -0,0 +1,231 @@ +package tech.libeufin.sandbox + +import org.jetbrains.exposed.dao.* +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.transactions.transaction + +const val CUSTOMER_NAME_MAX_LENGTH = 20 +const val EBICS_USER_ID_MAX_LENGTH = 10 +const val EBICS_PARTNER_ID_MAX_LENGTH = 10 +const val EBICS_SYSTEM_ID_MAX_LENGTH = 10 +const val PUBLIC_KEY_MAX_LENGTH = 256 // FIXME review this value! +const val PRIV_KEY_MAX_LENGTH = 512 // FIXME review this value! +const val SQL_ENUM_SUBSCRIBER_STATES = "ENUM('NEW', 'PARTIALLI_INITIALIZED_INI', 'PARTIALLY_INITIALIZED_HIA', 'INITIALIZED', 'READY')" + +/** + * All the states to give a subscriber. + */ +enum class SubscriberStates { + /** + * No keys at all given to the bank. + */ + NEW, + + /** + * Only INI electronic message was succesfully sent. + */ + PARTIALLY_INITIALIZED_INI, + + /** + * Only HIA electronic message was succesfully sent. + */ + PARTIALLY_INITIALIZED_HIA, + + /** + * Both INI and HIA were electronically sent with success. + */ + INITIALIZED, + + /** + * All the keys accounted in INI and HIA have been confirmed + * via physical mail. + */ + READY +} + +/** + * All the states that one key can be assigned. + */ +enum class KeyStates { + + /** + * The key was never communicated. + */ + MISSING, + + /** + * The key has been electronically sent. + */ + NEW, + + /** + * The key has been confirmed (either via physical mail + * or electronically -- e.g. with certificates) + */ + RELEASED +} + +/** + * This table information *not* related to EBICS, for all + * its customers. + */ +object BankCustomers: IntIdTable() { + // Customer ID is the default 'id' field provided by the constructor. + val name = varchar("name", CUSTOMER_NAME_MAX_LENGTH) + val ebicsSubscriber = reference("ebicsSubscriber", EbicsSubscribers) +} + +class BankCustomer(id: EntityID<Int>) : IntEntity(id) { + companion object : IntEntityClass<BankCustomer>(BankCustomers) + + var name by BankCustomers.name + var ebicsSubscriber by EbicsSubscriber referencedOn BankCustomers.ebicsSubscriber +} + +/** + * The following tables define IDs that make a EBCIS + * 'subscriber' exist. Each EBICS subscriber is the tuple: + * + * - UserID, the human who is performing a EBICS task. + * - PartnerID, the legal entity that signed a formal agreement with the financial institution. + * - SystemID, (optional) the machine that is handling the EBICS task on behalf of the UserID. + */ + +object EbicsUsers: IntIdTable() { + /* EBICS user ID in the string form. */ + val userId = varchar("userId", EBICS_USER_ID_MAX_LENGTH).nullable() + +} + +class EbicsUser(id: EntityID<Int>) : IntEntity(id){ + companion object : IntEntityClass<EbicsUser>(EbicsUsers) { + override fun new(init: EbicsUser.() -> Unit): EbicsUser { + var row = super.new(init) + row.userId = "u${row.id}" + return row + } + } + var userId by EbicsUsers.userId +} + +/** + * Table for UserID. + */ +object EbicsPartners: IntIdTable() { + val partnerId = varchar("partnerId", EBICS_PARTNER_ID_MAX_LENGTH).nullable() +} + + +class EbicsPartner(id: EntityID<Int>) : IntEntity(id) { + companion object : IntEntityClass<EbicsPartner>(EbicsPartners) { + override fun new(init: EbicsPartner.() -> Unit): EbicsPartner { + var row = super.new(init) + row.partnerId = "p${row.id}" + return row + } + } + var partnerId by EbicsPartners.partnerId +} + + +/** + * Table for UserID. + */ +object EbicsSystems: IntIdTable() { + val systemId = EbicsPartners.varchar("systemId", EBICS_SYSTEM_ID_MAX_LENGTH).nullable() +} + +class EbicsSystem(id: EntityID<Int>) : IntEntity(id) { + companion object : IntEntityClass<EbicsSystem>(EbicsSystems) { + override fun new(init: EbicsSystem.() -> Unit): EbicsSystem { + var row = super.new(init) + row.systemId = "s${row.id}" + return row + } + } + + var systemId by EbicsSystems.systemId +} + +/** + * Subscribers table. This table associates users with partners + * and systems. Each value can appear multiple times in the same column. + */ +object EbicsSubscribers: IntIdTable() { + val userId = reference("UserId", EbicsUsers) + val partnerId = reference("PartnerId", EbicsPartners) + val systemId = reference("SystemId", EbicsSystems) + + val signatureKey = reference("signatureKey", EbicsPublicKey).nullable() + val encryptionKey = reference("encryptionKey", EbicsPublicKey).nullable() + val authorizationKey = reference("authorizationKey", EbicsPublicKey).nullable() + + val state = customEnumeration( + "state", + SQL_ENUM_SUBSCRIBER_STATES, + {SubscriberStates.values()[it as Int]}, + {it.name}) +} + +class EbicsSubscriber(id: EntityID<Int>) : IntEntity(id) { + companion object : IntEntityClass<EbicsSubscriber>(EbicsSubscribers) + + var userId by EbicsUser referencedOn EbicsSubscribers.userId + var partnerId by EbicsPartner referencedOn EbicsSubscribers.partnerId + var systemId by EbicsSystem referencedOn EbicsSubscribers.systemId + + var signatureKey by EbicsPublicKey.id + var encryptionKey by EbicsPublicKey.id + var authorizationKey by EbicsPublicKey.id + var state by EbicsSubscribers.state +} + +/** + * Helper function that makes a new subscriber + * @return new object + */ +fun createSubscriber() : EbicsSubscriber { + + return EbicsSubscriber.new { + userId = EbicsUser.new { } + partnerId = EbicsPartner.new { } + systemId = EbicsSystem.new { } + state = SubscriberStates.NEW + } +} + + +/** + * This table stores RSA public keys. + */ +object EbicsPublicKey: IntIdTable() { + val pub = binary("pub", PUBLIC_KEY_MAX_LENGTH) + val state = customEnumeration( + "state", + "ENUM('MISSING', 'NEW', 'RELEASED')", + {KeyStates.values()[it as Int]}, + {it.name}) +} + +/** + * This table stores RSA private keys. + */ +object EbicsPrivateKEy: IntIdTable() { + val pub = binary("priv", PRIV_KEY_MAX_LENGTH) +} + +fun dbCreateTables() { + Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", driver = "org.h2.Driver") + + transaction { + addLogger(StdOutSqlLogger) + + SchemaUtils.create( + BankCustomers, + EbicsUsers, + EbicsPartners, + EbicsSystems, + EbicsSubscribers + ) + } +} +\ No newline at end of file diff --git a/sandbox/src/main/kotlin/tech/libeufin/GetLogger.kt b/sandbox/src/main/kotlin/tech/libeufin/GetLogger.kt @@ -0,0 +1,35 @@ +package tech.libeufin.sandbox + +import ch.qos.logback.classic.Level +import org.slf4j.LoggerFactory +import ch.qos.logback.core.FileAppender +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.classic.Logger +import ch.qos.logback.classic.encoder.PatternLayoutEncoder +import ch.qos.logback.classic.LoggerContext + +fun getLogger(): Logger { + val lc: LoggerContext = LoggerFactory.getILoggerFactory() as LoggerContext + val logger: Logger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) as Logger + val fa = FileAppender<ILoggingEvent>() + val le = PatternLayoutEncoder() + + // appender setup + fa.context = lc + fa.name = "libeufin" + fa.isAppend = true + fa.file = "server.log" + + // encoder setup + le.context = lc + le.pattern = "%date [%level]: %msg\n" + + // link && start + le.start() + fa.encoder = le + fa.start() + logger.addAppender(fa) + + logger.level = Level.DEBUG + return logger +} diff --git a/sandbox/src/main/kotlin/tech/libeufin/HEVResponse.kt b/sandbox/src/main/kotlin/tech/libeufin/HEVResponse.kt @@ -0,0 +1,40 @@ +package tech.libeufin.sandbox + +import tech.libeufin.messages.HEVResponseDataType +import tech.libeufin.messages.ObjectFactory +import tech.libeufin.messages.SystemReturnCodeType +import javax.xml.bind.JAXBElement + + +class HEVResponse( + returnCode: String, + reportText: String, + protocolAndVersion: Array<ProtocolAndVersion>?) { + + constructor( + returnCode: String, + reportText: String + ) : this(returnCode, reportText, null) + + private val value: HEVResponseDataType = { + val srt = SystemReturnCodeType() + srt.setReturnCode(returnCode); + srt.setReportText(reportText); + val value = HEVResponseDataType(); + value.setSystemReturnCode(srt); + + protocolAndVersion?.forEach { + val entry = HEVResponseDataType.VersionNumber() + entry.setProtocolVersion(it.protocol) + entry.setValue(it.version) + value.getVersionNumber().add(entry) + } + + value + }() + + fun makeHEVResponse(): JAXBElement<HEVResponseDataType> { + val of = ObjectFactory() + return of.createEbicsHEVResponse(value) + } +} diff --git a/sandbox/src/main/kotlin/tech/libeufin/JSON.kt b/sandbox/src/main/kotlin/tech/libeufin/JSON.kt @@ -0,0 +1,81 @@ +package tech.libeufin.sandbox + +/** + * Error message. + */ +data class SandboxError( + val message: String +) + + +/** + * Request for POST /admin/customers + */ +data class CustomerRequest( + val name: String +) + +data class CustomerResponse( + val id: Int +) + +/** + * Response for GET /admin/customers/:id + */ +data class CustomerInfo( + val name: String, + val ebicsInfo: CustomerEbicsInfo +) + +data class CustomerEbicsInfo( + val userId: String +) + +/** + * Wrapper type around initialization letters + * for RSA keys. + */ +data class IniHiaLetters( + val INI: IniLetter, + val HIA: HiaLetter +) + +/** + * Request for INI letter. + */ +data class IniLetter( + + val userId: String, + val customerId: String, + val name: String, + val date: String, + val time: String, + val recipient: String, + val exp_length: Int, + val exponent: String, + val mod_length: Int, + val modulus: String, + val hash: String +) + +/** + * Request for HIA letter. + */ +data class HiaLetter( + val userId: String, + val customerId: String, + val name: String, + val date: String, + val time: String, + val recipient: String, + val ia_exp_length: Int, + val ia_exponent: String, + val ia_mod_length: Int, + val ia_modulus: String, + val ia_hash: String, + val enc_exp_length: Int, + val enc_exponent: String, + val enc_mod_length: Int, + val enc_modulus: String, + val enc_hash: String +) diff --git a/sandbox/src/main/kotlin/tech/libeufin/Main.kt b/sandbox/src/main/kotlin/tech/libeufin/Main.kt @@ -0,0 +1,230 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2019 Stanisci and Dold. + + * LibEuFin is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation; either version 3, or + * (at your option) any later version. + + * LibEuFin is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General + * Public License for more details. + + * You should have received a copy of the GNU Affero General Public + * License along with LibEuFin; see the file COPYING. If not, see + * <http://www.gnu.org/licenses/> + */ + +package tech.libeufin.sandbox + +import io.ktor.application.call +import io.ktor.application.install +import io.ktor.features.CallLogging +import io.ktor.features.ContentNegotiation +import io.ktor.gson.gson +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.request.receive +import io.ktor.request.receiveText +import io.ktor.response.respond +import io.ktor.response.respondText +import io.ktor.routing.get +import io.ktor.routing.post +import io.ktor.routing.routing +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import org.jetbrains.exposed.sql.transactions.transaction +import org.w3c.dom.Document +import tech.libeufin.messages.HEVResponseDataType +import java.text.DateFormat +import javax.xml.bind.JAXBElement + +fun main() { + + val logger = getLogger() + val xmlProcess = XML() + + dbCreateTables() + + val server = embeddedServer(Netty, port = 5000) { + + install(CallLogging) + install(ContentNegotiation) { + gson { + setDateFormat(DateFormat.LONG) + setPrettyPrinting() + } + } + routing { + get("/") { + logger.debug("GET: not implemented") + call.respondText("Hello LibEuFin!", ContentType.Text.Plain) + return@get + } + + post("/admin/customers") { + val body = try { + call.receive<CustomerRequest>() + } catch (e: Exception) { + e.printStackTrace() + call.respond( + HttpStatusCode.BadRequest, + SandboxError(e.message.toString()) + ) + return@post + } + logger.info(body.toString()) + + val returnId = transaction { + var myUserId = EbicsUser.new { } + val myPartnerId = EbicsPartner.new { } + val mySystemId = EbicsSystem.new { } + val subscriber = EbicsSubscriber.new { + userId = myUserId + partnerId = myPartnerId + systemId = mySystemId + state = SubscriberStates.NEW + } + println("subscriber ID: ${subscriber.id.value}") + val customer = BankCustomer.new { + name = body.name + ebicsSubscriber = subscriber + } + println("name: ${customer.name}") + return@transaction customer.id.value + } + + call.respond( + HttpStatusCode.OK, + CustomerResponse(id = returnId) + ) + + return@post + } + + get("/admin/customers/{id}") { + + val id: Int = try { + call.parameters["id"]!!.toInt() + } catch (e: NumberFormatException) { + call.respond( + HttpStatusCode.BadRequest, + SandboxError(e.message.toString()) + ) + return@get + } + + val customerInfo = transaction { + val customer = BankCustomer.findById(id) ?: return@transaction null + CustomerInfo( + customer.name, + ebicsInfo = CustomerEbicsInfo( + customer.ebicsSubscriber.userId.userId!! + ) + ) + } + + if (null == customerInfo) { + call.respond( + HttpStatusCode.NotFound, + SandboxError("id $id not found") + ) + return@get + } + + call.respond(HttpStatusCode.OK, customerInfo) + } + + post("/admin/customers/{id}/ebics/keyletter") { + val body = try { + call.receive<IniHiaLetters>() + } catch (e: Exception) { + e.printStackTrace() + call.respond( + HttpStatusCode.BadRequest, + SandboxError(e.message.toString()) + ) + return@post + } + logger.info(body.toString()) + + /**********************************************/ + + // Extract keys and compare them to what was + // received via the INI and HIA orders. + + /**********************************************/ + + } + + post("/ebicsweb") { + val body: String = call.receiveText() + val bodyDocument: Document? = xmlProcess.parseStringIntoDom(body) + if (bodyDocument == null) { + call.respondText( + contentType = ContentType.Application.Xml, + status = HttpStatusCode.BadRequest + ) { "Bad request / Could not parse the body" } + return@post + + } + + if (!xmlProcess.validateFromDom(bodyDocument)) { + logger.error("Invalid request received") + call.respondText( + contentType = ContentType.Application.Xml, + status = HttpStatusCode.BadRequest + ) { "Bad request / invalid document" } + return@post + } + + logger.info("Processing ${bodyDocument.documentElement.localName}") + + when (bodyDocument.documentElement.localName) { + "ebicsUnsecuredRequest" -> { + + /* Manage request. */ + + call.respond( + HttpStatusCode.NotImplemented, + SandboxError("Not implemented") + ) + return@post + } + + "ebicsHEVRequest" -> { + val hevResponse = HEVResponse( + "000000", + "EBICS_OK", + arrayOf( + ProtocolAndVersion("H003", "02.40"), + ProtocolAndVersion("H004", "02.50") + ) + ) + + val jaxbHEV: JAXBElement<HEVResponseDataType> = hevResponse.makeHEVResponse() + val responseText: String? = xmlProcess.getStringFromJaxb(jaxbHEV) + + call.respondText( + contentType = ContentType.Application.Xml, + status = HttpStatusCode.OK + ) { responseText.toString() } + return@post + } + else -> { + /* Log to console and return "unknown type" */ + logger.info("Unknown message, just logging it!") + call.respondText( + contentType = ContentType.Application.Xml, + status = HttpStatusCode.NotFound + ) { "Not found" } + return@post + } + } + } + } + } + server.start(wait = true) +} diff --git a/sandbox/src/main/kotlin/tech/libeufin/ProtocolAndVersion.kt b/sandbox/src/main/kotlin/tech/libeufin/ProtocolAndVersion.kt @@ -0,0 +1,6 @@ +package tech.libeufin.sandbox + +class ProtocolAndVersion(protocol: String, version: String) { + val protocol = protocol + val version = version +} +\ No newline at end of file diff --git a/sandbox/src/main/kotlin/tech/libeufin/XML.kt b/sandbox/src/main/kotlin/tech/libeufin/XML.kt @@ -0,0 +1,255 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2019 Stanisci and Dold. + + * LibEuFin is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation; either version 3, or + * (at your option) any later version. + + * LibEuFin is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General + * Public License for more details. + + * You should have received a copy of the GNU Affero General Public + * License along with LibEuFin; see the file COPYING. If not, see + * <http://www.gnu.org/licenses/> + */ + +package tech.libeufin.sandbox + +import com.sun.org.apache.xerces.internal.dom.DOMInputImpl +import org.w3c.dom.Document +import org.w3c.dom.ls.LSInput +import org.w3c.dom.ls.LSResourceResolver +import org.xml.sax.ErrorHandler +import org.xml.sax.InputSource +import org.xml.sax.SAXException +import org.xml.sax.SAXParseException +import java.io.* +import javax.xml.XMLConstants +import javax.xml.bind.JAXBContext +import javax.xml.bind.JAXBElement +import javax.xml.bind.JAXBException +import javax.xml.bind.Marshaller +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.parsers.ParserConfigurationException +import javax.xml.transform.* +import javax.xml.transform.dom.DOMSource +import javax.xml.transform.stream.StreamResult +import javax.xml.transform.stream.StreamSource +import javax.xml.validation.SchemaFactory + + +/** + * This class takes care of importing XSDs and validate + * XMLs against those. + */ +class XML { + /** + * Validator for EBICS messages. + */ + private val validator = try { + val classLoader = ClassLoader.getSystemClassLoader() + val sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI) + sf.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "file") + sf.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "") + sf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true) + sf.errorHandler = object : ErrorHandler { + override fun warning(p0: SAXParseException?) { + println("Warning: $p0") + } + + override fun error(p0: SAXParseException?) { + println("Error: $p0") + } + + override fun fatalError(p0: SAXParseException?) { + println("Fatal error: $p0") + } + } + sf.resourceResolver = object : LSResourceResolver { + override fun resolveResource( + type: String?, + namespaceURI: String?, + publicId: String?, + systemId: String?, + baseUri: String? + ): LSInput? { + if (type != "http://www.w3.org/2001/XMLSchema") { + return null + } + val res = classLoader.getResourceAsStream(systemId) ?: return null + return DOMInputImpl(publicId, systemId, baseUri, res, "UTF-8") + } + } + val schemaInputs: Array<Source> = listOf("ebics_H004.xsd", "ebics_hev.xsd").map { + val resUrl = classLoader.getResource(it) ?: throw FileNotFoundException("Schema file $it not found.") + StreamSource(File(resUrl.toURI())) + }.toTypedArray() + val bundle = sf.newSchema(schemaInputs) + bundle.newValidator() + } catch (e: SAXException) { + e.printStackTrace() + throw e + } + + + /** + * Parse string into XML DOM. + * @param xmlString the string to parse. + * @return the DOM representing @a xmlString + */ + fun parseStringIntoDom(xmlString: String): Document? { + + val factory = DocumentBuilderFactory.newInstance() + factory.isNamespaceAware = true + + try { + val xmlInputStream = ByteArrayInputStream(xmlString.toByteArray()) + val builder = factory.newDocumentBuilder() + val document = builder.parse(InputSource(xmlInputStream)) + + return document + + } catch (e: ParserConfigurationException) { + e.printStackTrace() + } catch (e: SAXException) { + e.printStackTrace() + } catch (e: IOException) { + e.printStackTrace() + } + return null + } + + /** + * + * @param xmlDoc the XML document to validate + * @return true when validation passes, false otherwise + */ + fun validate(xmlDoc: StreamSource): Boolean { + try { + validator?.validate(xmlDoc) + } catch (e: SAXException) { + println(e.message) + return false + } catch (e: IOException) { + e.printStackTrace() + return false + } + + return true + } + + /** + * Validates the DOM against the Schema(s) of this object. + * @param domDocument DOM to validate + * @return true/false if the document is valid/invalid + */ + fun validateFromDom(domDocument: Document): Boolean { + try { + validator?.validate(DOMSource(domDocument)) + } catch (e: SAXException) { + e.printStackTrace() + return false + } + return true + } + + /** + * Craft object to be passed to the XML validator. + * @param xmlString XML body, as read from the POST body. + * @return InputStream object, as wanted by the validator. + */ + fun validateFromString(xmlString: String): Boolean { + val xmlInputStream: InputStream = ByteArrayInputStream(xmlString.toByteArray()) + val xmlSource = StreamSource(xmlInputStream) + return this.validate(xmlSource) + } + + /** + * Return the DOM representation of the Java object, using the JAXB + * interface. FIXME: narrow input type to JAXB type! + * + * @param object to be transformed into DOM. Typically, the object + * has already got its setters called. + * @return the DOM Document, or null (if errors occur). + */ + fun convertJaxbToDom(obj: JAXBElement<Unit>): Document? { + + try { + val jc = JAXBContext.newInstance("tech.libeufin.messages") + + /* Make the target document. */ + val dbf = DocumentBuilderFactory.newInstance() + val db = dbf.newDocumentBuilder() + val document = db.newDocument() + + /* Marshalling the object into the document. */ + val m = jc.createMarshaller() + m.marshal(obj, document) // document absorbed the XML! + return document + + } catch (e: JAXBException) { + e.printStackTrace() + } catch (e: ParserConfigurationException) { + e.printStackTrace() + } + return null + } + + /** + * Extract String from DOM. + * + * @param document the DOM to extract the string from. + * @return the final String, or null if errors occur. + */ + fun getStringFromDocument(document: Document): String? { + + try { + /* Make Transformer. */ + val tf = TransformerFactory.newInstance() + val t = tf.newTransformer() + + t.setOutputProperty(OutputKeys.INDENT, "no") + + /* Make string writer. */ + val sw = StringWriter() + + /* Extract string. */ + t.transform(DOMSource(document), StreamResult(sw)) + return sw.toString() + + } catch (e: TransformerConfigurationException) { + e.printStackTrace() + } catch (e: TransformerException) { + e.printStackTrace() + } + return null + } + + /** + * Extract String from JAXB. + * + * @param obj the JAXB instance + * @return String representation of @a object, or null if errors occur + */ + fun <T> getStringFromJaxb(obj: JAXBElement<T>): String? { + val sw = StringWriter() + + try { + val jc = JAXBContext.newInstance("tech.libeufin.messages") + /* Getting the string. */ + val m = jc.createMarshaller() + m.marshal(obj, sw) + m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true) + + } catch (e: JAXBException) { + e.printStackTrace() + return "Bank fatal error." + } + + return sw.toString() + } +} +\ No newline at end of file diff --git a/src/main/python/libeufin-cli b/sandbox/src/main/python/libeufin-cli diff --git a/src/main/resources/ebics_H004.xsd b/sandbox/src/main/resources/ebics_H004.xsd diff --git a/src/main/resources/ebics_hev.xsd b/sandbox/src/main/resources/ebics_hev.xsd diff --git a/src/main/resources/ebics_keymgmt_request_H004.xsd b/sandbox/src/main/resources/ebics_keymgmt_request_H004.xsd diff --git a/src/main/resources/ebics_keymgmt_response_H004.xsd b/sandbox/src/main/resources/ebics_keymgmt_response_H004.xsd diff --git a/src/main/resources/ebics_orders_H004.xsd b/sandbox/src/main/resources/ebics_orders_H004.xsd diff --git a/src/main/resources/ebics_request_H004.xsd b/sandbox/src/main/resources/ebics_request_H004.xsd diff --git a/src/main/resources/ebics_response_H004.xsd b/sandbox/src/main/resources/ebics_response_H004.xsd diff --git a/src/main/resources/ebics_signature_S002.xsd b/sandbox/src/main/resources/ebics_signature_S002.xsd diff --git a/src/main/resources/ebics_signatures.xsd b/sandbox/src/main/resources/ebics_signatures.xsd diff --git a/src/main/resources/ebics_types_H004.xsd b/sandbox/src/main/resources/ebics_types_H004.xsd diff --git a/src/main/resources/xmldsig-core-schema.xsd b/sandbox/src/main/resources/xmldsig-core-schema.xsd diff --git a/src/test/kotlin/XmlTest.kt b/sandbox/src/test/kotlin/XmlTest.kt diff --git a/src/test/resources/HEVresponse-from-official-documentation.xml b/sandbox/src/test/resources/HEVresponse-from-official-documentation.xml diff --git a/src/test/resources/ebics_hev.xml b/sandbox/src/test/resources/ebics_hev.xml diff --git a/src/test/resources/ebics_ini_request_sample.xml b/sandbox/src/test/resources/ebics_ini_request_sample.xml diff --git a/src/test/resources/hev_resp3.0.xml b/sandbox/src/test/resources/hev_resp3.0.xml diff --git a/src/test/resources/hev_resp_libeufin.xml b/sandbox/src/test/resources/hev_resp_libeufin.xml diff --git a/settings.gradle b/settings.gradle @@ -1,2 +1,4 @@ -rootProject.name = 'sandbox' +rootProject.name = 'libeufin' +include("sandbox") +include("nexus") diff --git a/src/main/kotlin/tech/libeufin/DB.kt b/src/main/kotlin/tech/libeufin/DB.kt @@ -1,231 +0,0 @@ -package tech.libeufin - -import org.jetbrains.exposed.dao.* -import org.jetbrains.exposed.sql.* -import org.jetbrains.exposed.sql.transactions.transaction - -const val CUSTOMER_NAME_MAX_LENGTH = 20 -const val EBICS_USER_ID_MAX_LENGTH = 10 -const val EBICS_PARTNER_ID_MAX_LENGTH = 10 -const val EBICS_SYSTEM_ID_MAX_LENGTH = 10 -const val PUBLIC_KEY_MAX_LENGTH = 256 // FIXME review this value! -const val PRIV_KEY_MAX_LENGTH = 512 // FIXME review this value! -const val SQL_ENUM_SUBSCRIBER_STATES = "ENUM('NEW', 'PARTIALLI_INITIALIZED_INI', 'PARTIALLY_INITIALIZED_HIA', 'INITIALIZED', 'READY')" - -/** - * All the states to give a subscriber. - */ -enum class SubscriberStates { - /** - * No keys at all given to the bank. - */ - NEW, - - /** - * Only INI electronic message was succesfully sent. - */ - PARTIALLY_INITIALIZED_INI, - - /** - * Only HIA electronic message was succesfully sent. - */ - PARTIALLY_INITIALIZED_HIA, - - /** - * Both INI and HIA were electronically sent with success. - */ - INITIALIZED, - - /** - * All the keys accounted in INI and HIA have been confirmed - * via physical mail. - */ - READY -} - -/** - * All the states that one key can be assigned. - */ -enum class KeyStates { - - /** - * The key was never communicated. - */ - MISSING, - - /** - * The key has been electronically sent. - */ - NEW, - - /** - * The key has been confirmed (either via physical mail - * or electronically -- e.g. with certificates) - */ - RELEASED -} - -/** - * This table information *not* related to EBICS, for all - * its customers. - */ -object BankCustomers: IntIdTable() { - // Customer ID is the default 'id' field provided by the constructor. - val name = varchar("name", CUSTOMER_NAME_MAX_LENGTH) - val ebicsSubscriber = reference("ebicsSubscriber", EbicsSubscribers) -} - -class BankCustomer(id: EntityID<Int>) : IntEntity(id) { - companion object : IntEntityClass<BankCustomer>(BankCustomers) - - var name by BankCustomers.name - var ebicsSubscriber by EbicsSubscriber referencedOn BankCustomers.ebicsSubscriber -} - -/** - * The following tables define IDs that make a EBCIS - * 'subscriber' exist. Each EBICS subscriber is the tuple: - * - * - UserID, the human who is performing a EBICS task. - * - PartnerID, the legal entity that signed a formal agreement with the financial institution. - * - SystemID, (optional) the machine that is handling the EBICS task on behalf of the UserID. - */ - -object EbicsUsers: IntIdTable() { - /* EBICS user ID in the string form. */ - val userId = varchar("userId", EBICS_USER_ID_MAX_LENGTH).nullable() - -} - -class EbicsUser(id: EntityID<Int>) : IntEntity(id){ - companion object : IntEntityClass<EbicsUser>(EbicsUsers) { - override fun new(init: EbicsUser.() -> Unit): EbicsUser { - var row = super.new(init) - row.userId = "u${row.id}" - return row - } - } - var userId by EbicsUsers.userId -} - -/** - * Table for UserID. - */ -object EbicsPartners: IntIdTable() { - val partnerId = varchar("partnerId", EBICS_PARTNER_ID_MAX_LENGTH).nullable() -} - - -class EbicsPartner(id: EntityID<Int>) : IntEntity(id) { - companion object : IntEntityClass<EbicsPartner>(EbicsPartners) { - override fun new(init: EbicsPartner.() -> Unit): EbicsPartner { - var row = super.new(init) - row.partnerId = "p${row.id}" - return row - } - } - var partnerId by EbicsPartners.partnerId -} - - -/** - * Table for UserID. - */ -object EbicsSystems: IntIdTable() { - val systemId = EbicsPartners.varchar("systemId", EBICS_SYSTEM_ID_MAX_LENGTH).nullable() -} - -class EbicsSystem(id: EntityID<Int>) : IntEntity(id) { - companion object : IntEntityClass<EbicsSystem>(EbicsSystems) { - override fun new(init: EbicsSystem.() -> Unit): EbicsSystem { - var row = super.new(init) - row.systemId = "s${row.id}" - return row - } - } - - var systemId by EbicsSystems.systemId -} - -/** - * Subscribers table. This table associates users with partners - * and systems. Each value can appear multiple times in the same column. - */ -object EbicsSubscribers: IntIdTable() { - val userId = reference("UserId", EbicsUsers) - val partnerId = reference("PartnerId", EbicsPartners) - val systemId = reference("SystemId", EbicsSystems) - - val signatureKey = reference("signatureKey", EbicsPublicKey).nullable() - val encryptionKey = reference("encryptionKey", EbicsPublicKey).nullable() - val authorizationKey = reference("authorizationKey", EbicsPublicKey).nullable() - - val state = customEnumeration( - "state", - SQL_ENUM_SUBSCRIBER_STATES, - {SubscriberStates.values()[it as Int]}, - {it.name}) -} - -class EbicsSubscriber(id: EntityID<Int>) : IntEntity(id) { - companion object : IntEntityClass<EbicsSubscriber>(EbicsSubscribers) - - var userId by EbicsUser referencedOn EbicsSubscribers.userId - var partnerId by EbicsPartner referencedOn EbicsSubscribers.partnerId - var systemId by EbicsSystem referencedOn EbicsSubscribers.systemId - - var signatureKey by EbicsPublicKey.id - var encryptionKey by EbicsPublicKey.id - var authorizationKey by EbicsPublicKey.id - var state by EbicsSubscribers.state -} - -/** - * Helper function that makes a new subscriber - * @return new object - */ -fun createSubscriber() : EbicsSubscriber { - - return EbicsSubscriber.new { - userId = EbicsUser.new { } - partnerId = EbicsPartner.new { } - systemId = EbicsSystem.new { } - state = SubscriberStates.NEW - } -} - - -/** - * This table stores RSA public keys. - */ -object EbicsPublicKey: IntIdTable() { - val pub = binary("pub", PUBLIC_KEY_MAX_LENGTH) - val state = customEnumeration( - "state", - "ENUM('MISSING', 'NEW', 'RELEASED')", - {KeyStates.values()[it as Int]}, - {it.name}) -} - -/** - * This table stores RSA private keys. - */ -object EbicsPrivateKEy: IntIdTable() { - val pub = binary("priv", PRIV_KEY_MAX_LENGTH) -} - -fun dbCreateTables() { - Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", driver = "org.h2.Driver") - - transaction { - addLogger(StdOutSqlLogger) - - SchemaUtils.create( - BankCustomers, - EbicsUsers, - EbicsPartners, - EbicsSystems, - EbicsSubscribers - ) - } -} -\ No newline at end of file diff --git a/src/main/kotlin/tech/libeufin/GetLogger.kt b/src/main/kotlin/tech/libeufin/GetLogger.kt @@ -1,35 +0,0 @@ -package tech.libeufin; - -import ch.qos.logback.classic.Level -import org.slf4j.LoggerFactory -import ch.qos.logback.core.FileAppender -import ch.qos.logback.classic.spi.ILoggingEvent -import ch.qos.logback.classic.Logger -import ch.qos.logback.classic.encoder.PatternLayoutEncoder -import ch.qos.logback.classic.LoggerContext - -fun getLogger(): Logger { - val lc: LoggerContext = LoggerFactory.getILoggerFactory() as LoggerContext - val logger: Logger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) as Logger - val fa = FileAppender<ILoggingEvent>() - val le = PatternLayoutEncoder() - - // appender setup - fa.context = lc - fa.name = "libeufin" - fa.isAppend = true - fa.file = "server.log" - - // encoder setup - le.context = lc - le.pattern = "%date [%level]: %msg\n" - - // link && start - le.start() - fa.encoder = le - fa.start() - logger.addAppender(fa) - - logger.level = Level.DEBUG - return logger -} diff --git a/src/main/kotlin/tech/libeufin/HEVResponse.kt b/src/main/kotlin/tech/libeufin/HEVResponse.kt @@ -1,40 +0,0 @@ -package tech.libeufin - -import tech.libeufin.messages.HEVResponseDataType -import tech.libeufin.messages.ObjectFactory -import tech.libeufin.messages.SystemReturnCodeType -import javax.xml.bind.JAXBElement - - -class HEVResponse( - returnCode: String, - reportText: String, - protocolAndVersion: Array<ProtocolAndVersion>?) { - - constructor( - returnCode: String, - reportText: String - ) : this(returnCode, reportText, null) - - private val value: HEVResponseDataType = { - val srt = SystemReturnCodeType() - srt.setReturnCode(returnCode); - srt.setReportText(reportText); - val value = HEVResponseDataType(); - value.setSystemReturnCode(srt); - - protocolAndVersion?.forEach { - val entry = HEVResponseDataType.VersionNumber() - entry.setProtocolVersion(it.protocol) - entry.setValue(it.version) - value.getVersionNumber().add(entry) - } - - value - }() - - fun makeHEVResponse(): JAXBElement<HEVResponseDataType> { - val of = ObjectFactory() - return of.createEbicsHEVResponse(value) - } -} diff --git a/src/main/kotlin/tech/libeufin/JSON.kt b/src/main/kotlin/tech/libeufin/JSON.kt @@ -1,81 +0,0 @@ -package tech.libeufin - -/** - * Error message. - */ -data class SandboxError( - val message: String -) - - -/** - * Request for POST /admin/customers - */ -data class CustomerRequest( - val name: String -) - -data class CustomerResponse( - val id: Int -) - -/** - * Response for GET /admin/customers/:id - */ -data class CustomerInfo( - val name: String, - val ebicsInfo: CustomerEbicsInfo -) - -data class CustomerEbicsInfo( - val userId: String -) - -/** - * Wrapper type around initialization letters - * for RSA keys. - */ -data class IniHiaLetters( - val INI: IniLetter, - val HIA: HiaLetter -) - -/** - * Request for INI letter. - */ -data class IniLetter( - - val userId: String, - val customerId: String, - val name: String, - val date: String, - val time: String, - val recipient: String, - val exp_length: Int, - val exponent: String, - val mod_length: Int, - val modulus: String, - val hash: String -) - -/** - * Request for HIA letter. - */ -data class HiaLetter( - val userId: String, - val customerId: String, - val name: String, - val date: String, - val time: String, - val recipient: String, - val ia_exp_length: Int, - val ia_exponent: String, - val ia_mod_length: Int, - val ia_modulus: String, - val ia_hash: String, - val enc_exp_length: Int, - val enc_exponent: String, - val enc_mod_length: Int, - val enc_modulus: String, - val enc_hash: String -) diff --git a/src/main/kotlin/tech/libeufin/Main.kt b/src/main/kotlin/tech/libeufin/Main.kt @@ -1,230 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2019 Stanisci and Dold. - - * LibEuFin is free software; you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation; either version 3, or - * (at your option) any later version. - - * LibEuFin is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General - * Public License for more details. - - * You should have received a copy of the GNU Affero General Public - * License along with LibEuFin; see the file COPYING. If not, see - * <http://www.gnu.org/licenses/> - */ - -package tech.libeufin - -import io.ktor.application.call -import io.ktor.application.install -import io.ktor.features.CallLogging -import io.ktor.features.ContentNegotiation -import io.ktor.gson.gson -import io.ktor.http.ContentType -import io.ktor.http.HttpStatusCode -import io.ktor.request.receive -import io.ktor.request.receiveText -import io.ktor.response.respond -import io.ktor.response.respondText -import io.ktor.routing.get -import io.ktor.routing.post -import io.ktor.routing.routing -import io.ktor.server.engine.embeddedServer -import io.ktor.server.netty.Netty -import org.jetbrains.exposed.sql.transactions.transaction -import org.w3c.dom.Document -import tech.libeufin.messages.HEVResponseDataType -import java.text.DateFormat -import javax.xml.bind.JAXBElement - -fun main() { - - val logger = getLogger() - val xmlProcess = XML() - - dbCreateTables() - - val server = embeddedServer(Netty, port = 5000) { - - install(CallLogging) - install(ContentNegotiation) { - gson { - setDateFormat(DateFormat.LONG) - setPrettyPrinting() - } - } - routing { - get("/") { - logger.debug("GET: not implemented") - call.respondText("Hello LibEuFin!", ContentType.Text.Plain) - return@get - } - - post("/admin/customers") { - val body = try { - call.receive<CustomerRequest>() - } catch (e: Exception) { - e.printStackTrace() - call.respond( - HttpStatusCode.BadRequest, - SandboxError(e.message.toString()) - ) - return@post - } - logger.info(body.toString()) - - val returnId = transaction { - var myUserId = EbicsUser.new { } - val myPartnerId = EbicsPartner.new { } - val mySystemId = EbicsSystem.new { } - val subscriber = EbicsSubscriber.new { - userId = myUserId - partnerId = myPartnerId - systemId = mySystemId - state = SubscriberStates.NEW - } - println("subscriber ID: ${subscriber.id.value}") - val customer = BankCustomer.new { - name = body.name - ebicsSubscriber = subscriber - } - println("name: ${customer.name}") - return@transaction customer.id.value - } - - call.respond( - HttpStatusCode.OK, - CustomerResponse(id = returnId) - ) - - return@post - } - - get("/admin/customers/{id}") { - - val id: Int = try { - call.parameters["id"]!!.toInt() - } catch (e: NumberFormatException) { - call.respond( - HttpStatusCode.BadRequest, - SandboxError(e.message.toString()) - ) - return@get - } - - val customerInfo = transaction { - val customer = BankCustomer.findById(id) ?: return@transaction null - CustomerInfo( - customer.name, - ebicsInfo = CustomerEbicsInfo( - customer.ebicsSubscriber.userId.userId!! - ) - ) - } - - if (null == customerInfo) { - call.respond( - HttpStatusCode.NotFound, - SandboxError("id $id not found") - ) - return@get - } - - call.respond(HttpStatusCode.OK, customerInfo) - } - - post("/admin/customers/{id}/ebics/keyletter") { - val body = try { - call.receive<IniHiaLetters>() - } catch (e: Exception) { - e.printStackTrace() - call.respond( - HttpStatusCode.BadRequest, - SandboxError(e.message.toString()) - ) - return@post - } - logger.info(body.toString()) - - /**********************************************/ - - // Extract keys and compare them to what was - // received via the INI and HIA orders. - - /**********************************************/ - - } - - post("/ebicsweb") { - val body: String = call.receiveText() - val bodyDocument: Document? = xmlProcess.parseStringIntoDom(body) - if (bodyDocument == null) { - call.respondText( - contentType = ContentType.Application.Xml, - status = HttpStatusCode.BadRequest - ) { "Bad request / Could not parse the body" } - return@post - - } - - if (!xmlProcess.validateFromDom(bodyDocument)) { - logger.error("Invalid request received") - call.respondText( - contentType = ContentType.Application.Xml, - status = HttpStatusCode.BadRequest - ) { "Bad request / invalid document" } - return@post - } - - logger.info("Processing ${bodyDocument.documentElement.localName}") - - when (bodyDocument.documentElement.localName) { - "ebicsUnsecuredRequest" -> { - - /* Manage request. */ - - call.respond( - HttpStatusCode.NotImplemented, - SandboxError("Not implemented") - ) - return@post - } - - "ebicsHEVRequest" -> { - val hevResponse = HEVResponse( - "000000", - "EBICS_OK", - arrayOf( - ProtocolAndVersion("H003", "02.40"), - ProtocolAndVersion("H004", "02.50") - ) - ) - - val jaxbHEV: JAXBElement<HEVResponseDataType> = hevResponse.makeHEVResponse() - val responseText: String? = xmlProcess.getStringFromJaxb(jaxbHEV) - - call.respondText( - contentType = ContentType.Application.Xml, - status = HttpStatusCode.OK - ) { responseText.toString() } - return@post - } - else -> { - /* Log to console and return "unknown type" */ - logger.info("Unknown message, just logging it!") - call.respondText( - contentType = ContentType.Application.Xml, - status = HttpStatusCode.NotFound - ) { "Not found" } - return@post - } - } - } - } - } - server.start(wait = true) -} diff --git a/src/main/kotlin/tech/libeufin/ProtocolAndVersion.kt b/src/main/kotlin/tech/libeufin/ProtocolAndVersion.kt @@ -1,6 +0,0 @@ -package tech.libeufin - -class ProtocolAndVersion(protocol: String, version: String) { - val protocol = protocol - val version = version -} -\ No newline at end of file diff --git a/src/main/kotlin/tech/libeufin/XML.kt b/src/main/kotlin/tech/libeufin/XML.kt @@ -1,255 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2019 Stanisci and Dold. - - * LibEuFin is free software; you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation; either version 3, or - * (at your option) any later version. - - * LibEuFin is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General - * Public License for more details. - - * You should have received a copy of the GNU Affero General Public - * License along with LibEuFin; see the file COPYING. If not, see - * <http://www.gnu.org/licenses/> - */ - -package tech.libeufin - -import com.sun.org.apache.xerces.internal.dom.DOMInputImpl -import org.w3c.dom.Document -import org.w3c.dom.ls.LSInput -import org.w3c.dom.ls.LSResourceResolver -import org.xml.sax.ErrorHandler -import org.xml.sax.InputSource -import org.xml.sax.SAXException -import org.xml.sax.SAXParseException -import java.io.* -import javax.xml.XMLConstants -import javax.xml.bind.JAXBContext -import javax.xml.bind.JAXBElement -import javax.xml.bind.JAXBException -import javax.xml.bind.Marshaller -import javax.xml.parsers.DocumentBuilderFactory -import javax.xml.parsers.ParserConfigurationException -import javax.xml.transform.* -import javax.xml.transform.dom.DOMSource -import javax.xml.transform.stream.StreamResult -import javax.xml.transform.stream.StreamSource -import javax.xml.validation.SchemaFactory - - -/** - * This class takes care of importing XSDs and validate - * XMLs against those. - */ -class XML { - /** - * Validator for EBICS messages. - */ - private val validator = try { - val classLoader = ClassLoader.getSystemClassLoader() - val sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI) - sf.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "file") - sf.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "") - sf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true) - sf.errorHandler = object : ErrorHandler { - override fun warning(p0: SAXParseException?) { - println("Warning: $p0") - } - - override fun error(p0: SAXParseException?) { - println("Error: $p0") - } - - override fun fatalError(p0: SAXParseException?) { - println("Fatal error: $p0") - } - } - sf.resourceResolver = object : LSResourceResolver { - override fun resolveResource( - type: String?, - namespaceURI: String?, - publicId: String?, - systemId: String?, - baseUri: String? - ): LSInput? { - if (type != "http://www.w3.org/2001/XMLSchema") { - return null - } - val res = classLoader.getResourceAsStream(systemId) ?: return null - return DOMInputImpl(publicId, systemId, baseUri, res, "UTF-8") - } - } - val schemaInputs: Array<Source> = listOf("ebics_H004.xsd", "ebics_hev.xsd").map { - val resUrl = classLoader.getResource(it) ?: throw FileNotFoundException("Schema file $it not found.") - StreamSource(File(resUrl.toURI())) - }.toTypedArray() - val bundle = sf.newSchema(schemaInputs) - bundle.newValidator() - } catch (e: SAXException) { - e.printStackTrace() - throw e - } - - - /** - * Parse string into XML DOM. - * @param xmlString the string to parse. - * @return the DOM representing @a xmlString - */ - fun parseStringIntoDom(xmlString: String): Document? { - - val factory = DocumentBuilderFactory.newInstance() - factory.isNamespaceAware = true - - try { - val xmlInputStream = ByteArrayInputStream(xmlString.toByteArray()) - val builder = factory.newDocumentBuilder() - val document = builder.parse(InputSource(xmlInputStream)) - - return document - - } catch (e: ParserConfigurationException) { - e.printStackTrace() - } catch (e: SAXException) { - e.printStackTrace() - } catch (e: IOException) { - e.printStackTrace() - } - return null - } - - /** - * - * @param xmlDoc the XML document to validate - * @return true when validation passes, false otherwise - */ - fun validate(xmlDoc: StreamSource): Boolean { - try { - validator?.validate(xmlDoc) - } catch (e: SAXException) { - println(e.message) - return false - } catch (e: IOException) { - e.printStackTrace() - return false - } - - return true - } - - /** - * Validates the DOM against the Schema(s) of this object. - * @param domDocument DOM to validate - * @return true/false if the document is valid/invalid - */ - fun validateFromDom(domDocument: Document): Boolean { - try { - validator?.validate(DOMSource(domDocument)) - } catch (e: SAXException) { - e.printStackTrace() - return false - } - return true - } - - /** - * Craft object to be passed to the XML validator. - * @param xmlString XML body, as read from the POST body. - * @return InputStream object, as wanted by the validator. - */ - fun validateFromString(xmlString: String): Boolean { - val xmlInputStream: InputStream = ByteArrayInputStream(xmlString.toByteArray()) - val xmlSource = StreamSource(xmlInputStream) - return this.validate(xmlSource) - } - - /** - * Return the DOM representation of the Java object, using the JAXB - * interface. FIXME: narrow input type to JAXB type! - * - * @param object to be transformed into DOM. Typically, the object - * has already got its setters called. - * @return the DOM Document, or null (if errors occur). - */ - fun convertJaxbToDom(obj: JAXBElement<Unit>): Document? { - - try { - val jc = JAXBContext.newInstance("tech.libeufin.messages") - - /* Make the target document. */ - val dbf = DocumentBuilderFactory.newInstance() - val db = dbf.newDocumentBuilder() - val document = db.newDocument() - - /* Marshalling the object into the document. */ - val m = jc.createMarshaller() - m.marshal(obj, document) // document absorbed the XML! - return document - - } catch (e: JAXBException) { - e.printStackTrace() - } catch (e: ParserConfigurationException) { - e.printStackTrace() - } - return null - } - - /** - * Extract String from DOM. - * - * @param document the DOM to extract the string from. - * @return the final String, or null if errors occur. - */ - fun getStringFromDocument(document: Document): String? { - - try { - /* Make Transformer. */ - val tf = TransformerFactory.newInstance() - val t = tf.newTransformer() - - t.setOutputProperty(OutputKeys.INDENT, "no") - - /* Make string writer. */ - val sw = StringWriter() - - /* Extract string. */ - t.transform(DOMSource(document), StreamResult(sw)) - return sw.toString() - - } catch (e: TransformerConfigurationException) { - e.printStackTrace() - } catch (e: TransformerException) { - e.printStackTrace() - } - return null - } - - /** - * Extract String from JAXB. - * - * @param obj the JAXB instance - * @return String representation of @a object, or null if errors occur - */ - fun <T> getStringFromJaxb(obj: JAXBElement<T>): String? { - val sw = StringWriter() - - try { - val jc = JAXBContext.newInstance("tech.libeufin.messages") - /* Getting the string. */ - val m = jc.createMarshaller() - m.marshal(obj, sw) - m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true) - - } catch (e: JAXBException) { - e.printStackTrace() - return "Bank fatal error." - } - - return sw.toString() - } -} -\ No newline at end of file