commit ffef8c7dcf3af4d5c81cb907e7c0005eced0da87
parent fe314003549494e604685b37d6d1d92cb959ad7e
Author: ms <ms@taler.net>
Date: Wed, 6 Oct 2021 12:07:10 +0200
Unix domain socket server.
This version takes HTTP requests from a Unix domain socket managed
by Netty, and proxies the requests - via the Ktor TestEngine - to a
Web application.
The TestEngine was chosen since it bridges conveniently between
raw HTTP requests and the Ktor ApplicationCall API. Eventually,
this implementation will disappear once Ktor will offer the API to
bind to Unix domain sockets.
Diffstat:
2 files changed, 92 insertions(+), 35 deletions(-)
diff --git a/sandbox/src/test/kotlin/CamtTest.kt b/sandbox/src/test/kotlin/CamtTest.kt
@@ -1,34 +0,0 @@
-import org.junit.Test
-import tech.libeufin.sandbox.buildCamtString
-import tech.libeufin.util.RawPayment
-import tech.libeufin.util.XMLUtil
-import kotlin.test.assertTrue
-
-class CamtTest {
-
- @Test
- fun validationTest() {
- val payment = RawPayment(
- creditorIban = "GB33BUKB20201222222222",
- creditorName = "Oliver Smith",
- creditorBic = "BUKBGB33",
- debtorIban = "GB33BUKB20201333333333",
- debtorName = "John Doe",
- debtorBic = "BUKBGB33",
- amount = "2",
- currency = "EUR",
- subject = "reimbursement",
- date = "1000-02-02",
- uid = "0",
- direction = "DBIT"
- )
- val xml = buildCamtString(
- 53,
- "GB33BUKB20201222222222",
- mutableListOf(payment)
- )
- assertTrue {
- XMLUtil.validateFromString(xml)
- }
- }
-}
-\ No newline at end of file
diff --git a/util/src/main/kotlin/UnixDomainSocket.kt b/util/src/main/kotlin/UnixDomainSocket.kt
@@ -0,0 +1,91 @@
+import io.ktor.application.*
+import io.ktor.client.statement.*
+import io.ktor.http.*
+import io.ktor.http.HttpMethod
+import io.ktor.server.engine.*
+import io.ktor.server.testing.*
+import io.netty.bootstrap.ServerBootstrap
+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.util.AttributeKey
+
+fun startServer(unixSocketPath: String, app: Application.() -> Unit) {
+
+ val boss = EpollEventLoopGroup()
+ val worker = EpollEventLoopGroup()
+ val serverBootstrap = ServerBootstrap()
+ serverBootstrap.group(boss, worker).channel(
+ EpollServerDomainSocketChannel::class.java
+ ).childHandler(LibeufinHttpInit(app))
+
+ val socketPath = DomainSocketAddress(unixSocketPath)
+ serverBootstrap.bind(socketPath).sync().channel().closeFuture().sync()
+}
+
+private val ktorApplicationKey = AttributeKey.newInstance<Application.() -> Unit>("KtorApplicationCall")
+
+class LibeufinHttpInit(private val app: Application.() -> Unit) : ChannelInitializer<Channel>() {
+ override fun initChannel(ch: Channel) {
+ val libeufinHandler = LibeufinHttpHandler()
+ ch.pipeline(
+ ).addLast(
+ HttpServerCodec()
+ ).addLast(
+ HttpObjectAggregator(Int.MAX_VALUE)
+ ).addLast(
+ libeufinHandler
+ )
+ val libeufinCtx: ChannelHandlerContext = ch.pipeline().context(libeufinHandler)
+ libeufinCtx.attr(ktorApplicationKey).set(app)
+ }
+}
+
+class LibeufinHttpHandler : SimpleChannelInboundHandler<FullHttpRequest>() {
+
+ @OptIn(EngineAPI::class)
+ override fun channelRead0(ctx: ChannelHandlerContext?, msg: FullHttpRequest) {
+ val app = ctx?.attr(ktorApplicationKey)?.get()
+ if (app == null) throw UtilError(
+ HttpStatusCode.InternalServerError,
+ "custom libEufin Unix-domain-socket+HTTP handler lost its Web app",
+ null
+ )
+ /**
+ * Below is only a echo of what euFin gets from the network. All
+ * the checks should then occur at the Web app + Ktor level. Hence,
+ * a HTTP call of GET with a non-empty body is not to be blocked / warned
+ * at this level.
+ *
+ * The only exception is the HTTP version value in the response, as the
+ * response returned by the Web app does not set it. Therefore, this
+ * proxy echoes back the HTTP version that was read in the request.
+ */
+ withTestApplication(app) {
+ val httpVersion = msg.protocolVersion()
+ // Proxying the request with Ktor API.
+ val call = handleRequest(closeRequest = false) {
+ msg.headers().forEach { addHeader(it.key, it.value) }
+ method = HttpMethod(msg.method().name())
+ uri = msg.uri()
+ version = httpVersion.text()
+ setBody(msg.content().array())
+ }
+ val statusCode: Int = call.response.status()?.value ?: throw UtilError(
+ HttpStatusCode.InternalServerError,
+ "app proxied via Unix domain socket did not include a response status code",
+ ec = null // FIXME: to be defined.
+ )
+ // Responding with Netty API.
+ val response = DefaultFullHttpResponse(httpVersion, HttpResponseStatus.valueOf(statusCode))
+ call.response.headers.allValues().forEach { s, list ->
+ response.headers().set(s, list.joinToString()) // joinToString() separates with ", " by default.
+ }
+ ctx.write(response)
+ ctx.flush()
+ }
+ }
+}
+\ No newline at end of file