libeufin

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

commit c7689737fddf6f885ed30af975b8cacd6fa2ea29
parent 133c53fa27edfef038b743322d553e55837e0525
Author: Antoine A <>
Date:   Tue, 28 Nov 2023 12:34:59 +0000

Remove unused code and dependencies, minify fat jar and improve logs

Diffstat:
MMakefile | 4++--
Mbank/build.gradle | 16++++++++++++----
Mbank/src/main/kotlin/tech/libeufin/bank/Config.kt | 18++++++++++--------
Mbank/src/main/kotlin/tech/libeufin/bank/Error.kt | 43+++++++++++++++++++++++++++++++++++++++----
Mbank/src/main/kotlin/tech/libeufin/bank/Main.kt | 73++++++++++++++++++++++++++++++++++++++-----------------------------------
Mutil/build.gradle | 6+-----
Dutil/src/main/kotlin/UnixDomainSocket.kt | 99-------------------------------------------------------------------------------
Dutil/src/main/kotlin/startServer.kt | 86-------------------------------------------------------------------------------
Dutil/src/test/kotlin/DomainSocketTest.kt | 24------------------------
9 files changed, 102 insertions(+), 267 deletions(-)

diff --git a/Makefile b/Makefile @@ -90,9 +90,9 @@ assemble: ./gradlew assemble .PHONY: check -check: install-bank-files +check: install-nobuild-bank-files ./gradlew check .PHONY: test -test: install-bank-files +test: install-nobuild-bank-files ./gradlew test --tests $(test) -i diff --git a/bank/build.gradle b/bank/build.gradle @@ -34,20 +34,28 @@ dependencies { implementation("io.ktor:ktor-server-content-negotiation:$ktor_version") implementation("io.ktor:ktor-server-status-pages:$ktor_version") implementation("io.ktor:ktor-server-netty:$ktor_version") - implementation("io.ktor:ktor-server-test-host:$ktor_version") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version") + // UNIX domain sockets support (used to connect to PostgreSQL) + implementation("com.kohlschutter.junixsocket:junixsocket-core:2.8.1") + testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlin_version") + testImplementation("io.ktor:ktor-server-test-host:$ktor_version") testImplementation(project(":util")) - - // UNIX domain sockets support (used to connect to PostgreSQL) - implementation("com.kohlschutter.junixsocket:junixsocket-core:2.8.1") } application { mainClass = "tech.libeufin.bank.MainKt" applicationName = "libeufin-bank" applicationDefaultJvmArgs = ['-Djava.net.preferIPv6Addresses=true'] +} + +shadowJar { + minimize { + exclude(dependency("io.ktor:ktor-serialization-kotlinx-json:.*")) + exclude(dependency("com.kohlschutter.junixsocket:junixsocket-core:.*")) + exclude(dependency("ch.qos.logback:logback-classic:.*")) + } } \ No newline at end of file diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Config.kt b/bank/src/main/kotlin/tech/libeufin/bank/Config.kt @@ -71,10 +71,10 @@ data class ConversionRate ( val cashout_min_amount: TalerAmount, ) -data class ServerConfig( - val method: String, - val port: Int -) +sealed class ServerConfig { + data class Unix(val path: String, val mode: Int): ServerConfig() + data class Tcp(val port: Int): ServerConfig() +} fun talerConfig(configPath: String?): TalerConfig = catchError { val config = TalerConfig(BANK_CONFIG_SOURCE) @@ -90,10 +90,12 @@ fun TalerConfig.loadDbConfig(): DatabaseConfig = catchError { } fun TalerConfig.loadServerConfig(): ServerConfig = catchError { - ServerConfig( - method = requireString("libeufin-bank", "serve"), - port = requireNumber("libeufin-bank", "port") - ) + val method = requireString("libeufin-bank", "serve") + when (method) { + "tcp" -> ServerConfig.Tcp(requireNumber("libeufin-bank", "port")) + "unix" -> ServerConfig.Unix(requireString("libeufin-bank", "unixpath"), requireNumber("libeufin-bank", "unixpath_mode")) + else -> throw Exception("Unknown server method '$method' expected 'tcp' or 'unix'") + } } fun TalerConfig.loadBankConfig(): BankConfig = catchError { diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Error.kt b/bank/src/main/kotlin/tech/libeufin/bank/Error.kt @@ -19,6 +19,9 @@ package tech.libeufin.bank import io.ktor.http.* +import io.ktor.server.response.* +import io.ktor.server.application.ApplicationCall +import io.ktor.util.AttributeKey import kotlinx.serialization.Serializable import net.taler.common.errorcodes.TalerErrorCode import tech.libeufin.util.* @@ -42,19 +45,49 @@ class LibeufinBankException( */ @Serializable data class TalerError( + @kotlinx.serialization.Transient val err: TalerErrorCode = TalerErrorCode.END, val code: Int, val hint: String? = null, val detail: String? = null ) +private val LOG_MSG = AttributeKey<String>("log_msg"); -fun libeufinError( +fun ApplicationCall.logMsg(): String? = attributes.getOrNull(LOG_MSG) + +suspend fun ApplicationCall.err( status: HttpStatusCode, hint: String?, error: TalerErrorCode +) { + err( + LibeufinBankException( + httpStatus = status, talerError = TalerError( + code = error.code, err = error, hint = hint + ) + ) + ) +} + +suspend fun ApplicationCall.err( + err: LibeufinBankException +) { + attributes.put(LOG_MSG, "${err.talerError.err.name} ${err.talerError.hint}") + respond( + status = err.httpStatus, + message = err.talerError + ) +} + + +fun libeufinError( + status: HttpStatusCode, + hint: String?, + error: TalerErrorCode, + detail: String? = null ): LibeufinBankException = LibeufinBankException( httpStatus = status, talerError = TalerError( - code = error.code, hint = hint + code = error.code, err = error, hint = hint, detail = detail ) ) @@ -81,8 +114,10 @@ fun conflict( ): LibeufinBankException = libeufinError(HttpStatusCode.Conflict, hint, error) fun badRequest( - hint: String? = null, error: TalerErrorCode = TalerErrorCode.GENERIC_JSON_INVALID -): LibeufinBankException = libeufinError(HttpStatusCode.BadRequest, hint, error) + hint: String? = null, + error: TalerErrorCode = TalerErrorCode.GENERIC_JSON_INVALID, + detail: String? = null +): LibeufinBankException = libeufinError(HttpStatusCode.BadRequest, hint, error, detail) fun BankConfig.checkRegionalCurrency(amount: TalerAmount) { diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt @@ -112,7 +112,15 @@ fun Application.corebankWebApp(db: Database, ctx: BankConfig) { this.level = Level.DEBUG this.logger = tech.libeufin.bank.logger this.format { call -> - "${call.response.status()}, ${call.request.httpMethod.value} ${call.request.path()}" + val status = call.response.status() + val httpMethod = call.request.httpMethod.value + val path = call.request.path() + val msg = call.logMsg() + if (msg != null) { + "$status, $httpMethod $path, $msg" + } else { + "$status, $httpMethod $path" + } } } install(CORS) { @@ -169,54 +177,38 @@ fun Application.corebankWebApp(db: Database, ctx: BankConfig) { else -> TalerErrorCode.GENERIC_JSON_INVALID } - call.respond( - status = HttpStatusCode.BadRequest, - message = TalerError( - code = talerErrorCode.code, - hint = cause.message, - detail = rootCause?.message + call.err( + badRequest( + cause.message, + talerErrorCode, + rootCause?.message ) ) } exception<LibeufinBankException> { call, cause -> - logger.error(cause.talerError.hint) - call.respond( - status = cause.httpStatus, - message = cause.talerError - ) + call.err(cause) } exception<SQLException> { call, cause -> - cause.printStackTrace() - val err = when (cause.sqlState) { - PSQLState.SERIALIZATION_FAILURE.state -> libeufinError( + when (cause.sqlState) { + PSQLState.SERIALIZATION_FAILURE.state -> call.err( HttpStatusCode.InternalServerError, "Transaction serialization failure", TalerErrorCode.BANK_SOFT_EXCEPTION ) - else -> libeufinError( + else -> call.err( HttpStatusCode.InternalServerError, "Unexpected sql error with state ${cause.sqlState}", TalerErrorCode.BANK_UNMANAGED_EXCEPTION ) } - logger.error(err.talerError.hint) - call.respond( - status = err.httpStatus, - message = err.talerError - ) } // Catch-all branch to mean that the bank wasn't able to manage one error. exception<Exception> { call, cause -> - val err = libeufinError( + call.err( HttpStatusCode.InternalServerError, cause.message, TalerErrorCode.BANK_UNMANAGED_EXCEPTION ) - logger.error(err.talerError.hint) - call.respond( - status = err.httpStatus, - message = err.talerError - ) } } routing { @@ -331,10 +323,6 @@ class ServeBank : CliktCommand("Run libeufin-bank HTTP server", name = "serve") val ctx = cfg.loadBankConfig() val dbCfg = cfg.loadDbConfig() val serverCfg = cfg.loadServerConfig() - if (serverCfg.method.lowercase() != "tcp") { - logger.error("Can only serve libeufin-bank via TCP") - exitProcess(1) - } val db = Database(dbCfg.dbConnStr, ctx.regionalCurrency, ctx.fiatCurrency) runBlocking { if (ctx.allowConversion) { @@ -364,13 +352,28 @@ class ServeBank : CliktCommand("Run libeufin-bank HTTP server", name = "serve") db.conn { it.execSQLUpdate(sqlProcedures.readText()) } // Remove conversion info from the database ? } - } - embeddedServer(Netty, port = serverCfg.port) { - corebankWebApp(db, ctx) - }.start(wait = true) + } + + val env = applicationEngineEnvironment { + connector { + when (serverCfg) { + is ServerConfig.Tcp -> { + port = serverCfg.port + } + is ServerConfig.Unix -> { + logger.error("Can only serve libeufin-bank via TCP") + exitProcess(1) + } + } + } + module { corebankWebApp(db, ctx) } + } + embeddedServer(Netty, env).start(wait = true) } } + + class ChangePw : CliktCommand("Change account password", name = "passwd") { private val configFile by option( "--config", "-c", diff --git a/util/build.gradle b/util/build.gradle @@ -19,21 +19,17 @@ sourceSets.main.java.srcDirs = ["src/main/kotlin"] dependencies { implementation("ch.qos.logback:logback-classic:1.4.5") - implementation("io.ktor:ktor-server-netty:$ktor_version") // XML Stuff implementation("javax.xml.bind:jaxb-api:2.3.1") implementation("org.glassfish.jaxb:jaxb-runtime:2.3.1") implementation("org.apache.santuario:xmlsec:2.2.2") // Crypto implementation("org.bouncycastle:bcprov-jdk15on:1.69") - // Unix domain socket to serve HTTP - implementation("io.netty:netty-all:$netty_version") - implementation("io.netty:netty-transport-native-epoll:$netty_version") - implementation("io.ktor:ktor-server-test-host:$ktor_version") // Database helper implementation("org.postgresql:postgresql:42.6.0") implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version") + implementation("io.ktor:ktor-server-test-host:$ktor_version") implementation("org.jetbrains.kotlin:kotlin-test:$kotlin_version") implementation("com.github.ajalt.clikt:clikt:4.2.1") diff --git a/util/src/main/kotlin/UnixDomainSocket.kt b/util/src/main/kotlin/UnixDomainSocket.kt @@ -1,98 +0,0 @@ -import io.ktor.client.plugins.* -import io.ktor.client.request.* -import io.ktor.server.application.* -import io.ktor.client.statement.* -import io.ktor.http.HttpHeaders -import io.ktor.http.HttpMethod -import io.ktor.server.testing.* -import io.netty.bootstrap.ServerBootstrap -import io.netty.buffer.ByteBufInputStream -import io.netty.buffer.Unpooled -import io.netty.channel.* -import io.netty.channel.epoll.EpollEventLoopGroup -import io.netty.channel.epoll.EpollServerDomainSocketChannel -import io.netty.channel.unix.DomainSocketAddress -import io.netty.handler.codec.http.* -import io.netty.handler.codec.http.DefaultHttpResponse -import io.netty.handler.logging.LoggingHandler -import io.netty.handler.stream.ChunkedStream -import io.netty.handler.stream.ChunkedWriteHandler -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import java.io.ByteArrayInputStream - -private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.util.UnixDomainSocket") - -fun startServer( - unixSocketPath: String, - app: Application.() -> Unit -) { - val boss = EpollEventLoopGroup() - val worker = EpollEventLoopGroup() - try { - val serverBootstrap = ServerBootstrap() - serverBootstrap.group(boss, worker).channel( - EpollServerDomainSocketChannel::class.java - ).childHandler(LibeufinHttpInit(app)) - val socketPath = DomainSocketAddress(unixSocketPath) - logger.debug("Listening on $unixSocketPath ..") - serverBootstrap.bind(socketPath).sync().channel().closeFuture().sync() - } finally { - boss.shutdownGracefully() - worker.shutdownGracefully() - } -} - -class LibeufinHttpInit( - private val app: Application.() -> Unit -) : ChannelInitializer<Channel>() { - override fun initChannel(ch: Channel) { - ch.pipeline( - ).addLast(LoggingHandler("tech.libeufin.dev") - ).addLast(HttpServerCodec() // in- and out- bound - ).addLast(HttpObjectAggregator(Int.MAX_VALUE) // only in- bound - ).addLast(ChunkedWriteHandler() - ).addLast(LibeufinHttpHandler(app)) // in- bound, and triggers out- bound. - } -} - -class LibeufinHttpHandler( - private val app: Application.() -> Unit -) : SimpleChannelInboundHandler<FullHttpRequest>() { - // @OptIn(EngineAPI::class) - override fun channelRead0(ctx: ChannelHandlerContext, msg: FullHttpRequest) { - testApplication { - application(app) - val httpVersion = msg.protocolVersion() - // Proxying the request to Ktor API. - val r = client.request(msg.uri()) { - expectSuccess = false - method = HttpMethod(msg.method().name()) - setBody(ByteBufInputStream(msg.content()).readAllBytes()) - } - // Responding to Netty API. - val response = DefaultHttpResponse( - httpVersion, - HttpResponseStatus.valueOf(r.status.value) - ) - var chunked = false - r.headers.forEach { s, list -> - if (s == HttpHeaders.TransferEncoding && list.contains("chunked")) - chunked = true - response.headers().set(s, list.joinToString()) - } - ctx.writeAndFlush(response) - if (chunked) { - ctx.writeAndFlush( - HttpChunkedInput( - ChunkedStream( - ByteArrayInputStream(r.readBytes()) - ) - ) - ) - } else { - ctx.writeAndFlush(Unpooled.wrappedBuffer(r.readBytes())) - } - } - } -} -\ No newline at end of file diff --git a/util/src/main/kotlin/startServer.kt b/util/src/main/kotlin/startServer.kt @@ -1,85 +0,0 @@ -package tech.libeufin.util - -import io.ktor.server.application.* -import io.ktor.server.engine.* -import io.ktor.server.netty.* -import io.netty.channel.unix.Errors -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import kotlin.system.exitProcess - -private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.util.startServer") - -const val EAFNOSUPPORT = -97 // Netty defines errors negatively. -class StartServerOptions( - var ipv4OnlyOpt: Boolean, - val localhostOnlyOpt: Boolean, - val portOpt: Int -) - -// Core function starting the server. -private fun serverMain(options: StartServerOptions, app: Application.() -> Unit) { - val server = embeddedServer( - Netty, - environment = applicationEngineEnvironment { - connector { - this.port = options.portOpt - this.host = if (options.localhostOnlyOpt) "127.0.0.1" else "0.0.0.0" - } - if (!options.ipv4OnlyOpt) connector { - this.port = options.portOpt - this.host = if (options.localhostOnlyOpt) "[::1]" else "[::]" - } - module(app) - }, - // Maybe remove this? Was introduced - // to debug concurrency issues.. - configure = { - connectionGroupSize = 1 - workerGroupSize = 1 - callGroupSize = 1 - } - ) - /** - * NOTE: excepted server still need the stop(), otherwise - * it leaves the port locked and prevents the IPv4 retry. - */ - try { - server.start(wait = true) - } catch (e: Exception) { - server.stop() - logger.debug("Rethrowing: ${e.message}") - throw e // Rethrowing for retry policies. - } -} - -// Wrapper function that retries when IPv6 fails. -fun startServerWithIPv4Fallback( - options: StartServerOptions, - app: Application.() -> Unit -) { - var maybeRetry = false - try { - serverMain(options, app) - } catch (e: Exception) { - logger.warn(e.message) - // Find reasons to retry. - if (e is Errors.NativeIoException) { - logger.debug("errno: ${e.expectedErr()}") - if ((e.expectedErr() == EAFNOSUPPORT) && (!options.ipv4OnlyOpt)) - maybeRetry = true - } - } - // Fail, if no retry policy applies. The catch block above logged the error. - if (!maybeRetry) { - exitProcess(1) - } - logger.info("Retrying to start the server on IPv4") - options.ipv4OnlyOpt = true - try { - serverMain(options, app) - } catch (e: Exception) { - logger.error(e.message) - exitProcess(1) - } -} -\ No newline at end of file diff --git a/util/src/test/kotlin/DomainSocketTest.kt b/util/src/test/kotlin/DomainSocketTest.kt @@ -1,23 +0,0 @@ -import io.ktor.server.application.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import org.junit.Test -import org.junit.Ignore - -class DomainSocketTest { - @Test @Ignore - fun bind() { - startServer("/tmp/java.sock") { - routing { - get("/") { - this.call.respond(object {}) - return@get - } - post("/") { - this.call.respond(object {}) - return@post - } - } - } - } -} -\ No newline at end of file