Home > OS >  spring-boot TCP server: wait for `empty line` before answering a `multi-line` request
spring-boot TCP server: wait for `empty line` before answering a `multi-line` request

Time:02-13

I would like to write a custom check_policy_service (http://www.postfix.org/SMTPD_POLICY_README.html) for postfix with spring-boot.

Short: postfix sends multiple lines like:

foo=bar
me=you
year=123
[empty line]

The empty line indicates, that the request is complete, now the spring-boot app should handle the data and return something like action=ok

Problem: In my current setup the app tries to handle line 1 foo=bar immediately and does not wait until the [empty line] is sent. How can I make the app wait for the empty line?

Current setup: spring-boot 2.6.3

@Configuration
class TcpServerConfig {
//    @Value("\${tcp.server.port}")
    private val port = 6676
    @Bean
    fun serverConnectionFactory(): AbstractServerConnectionFactory {
        val serverConnectionFactory = TcpNioServerConnectionFactory(port)
        serverConnectionFactory.setUsingDirectBuffers(true)
        serverConnectionFactory.isSingleUse = false // reuse socket
        return serverConnectionFactory
    }
    @Bean
    fun inboundChannel(): MessageChannel {
        return DirectChannel()
    }

    @Bean
    fun inboundGateway(
        serverConnectionFactory: AbstractServerConnectionFactory,
        inboundChannel: MessageChannel
    ): TcpInboundGateway {
        val tcpInboundGateway = TcpInboundGateway()
        tcpInboundGateway.setConnectionFactory(serverConnectionFactory)
        tcpInboundGateway.setRequestChannel(inboundChannel)
        return tcpInboundGateway
    }
}
@MessageEndpoint
class TcpServerEndpoint {

    @ServiceActivator(inputChannel = "inboundChannel")
    fun process(message: ByteArray): ByteArray {
        
        // I would need to have `message` contain every line until empty line - not a single line        
        println(String(message))
        
        // TODO handle message accordingly
        return "action=ok\n".toByteArray()
    }
}

I am not sure where to hook in. Maybe DirectChannel needs to be something else?

I wrote a quick non spring-boot implementation which works, so basically I need the spring-boot-ified version of:


fun main(args: Array<String>) {
    val server = ServerSocket(9999)
    println("Server is running on port ${server.localPort}")

    while (true) {
        val client = server.accept()
        println("Client connected: ${client.inetAddress.hostAddress}")

        // Run client in it's own thread.
        thread { ClientHandler(client).run() }
    }

}

class ClientHandler(client: Socket) {
    private val client: Socket = client
    private val reader: BufferedReader = BufferedReader(InputStreamReader(client.getInputStream()))
    private val writer: PrintWriter = PrintWriter(client.getOutputStream(), true)
    private var running: Boolean = false

    fun run() {
        running = true
        while (running) {
            try {
                do {
                    val line = reader.readLine() ?: break
                    // TODO collect all lines in a list and handle
                } while (line.isNotEmpty())

                write("action=ok")

            } catch (ex: Exception) {
                // TODO: Implement exception handling
                ex.printStackTrace()
                shutdown()
            } finally {

            }
        }
    }

    private fun write(message: String) {
        writer.write((message   "\n\n"))
        writer.flush()
    }
    private fun shutdown() {
        running = false
        client.close()
        println("${client.inetAddress.hostAddress} closed the connection")
    }
}

The magic part is read all lines until line is empty and then handle the request(s)

CodePudding user response:

You need to set the Deserializer so that Spring knows when to release the read content (so far) to the handler as described in the docs.

In your case, you should write a custom deserializer implementing the interface org.springframework.core.serializer.Deserializer that releases the content to the handler as soon as it has detected two newlines in a row. Make it return a List<String> with the lines sent by Postfix:

class DoubleNewLineDeserializer: Deserializer<List<String>> {

    companion object {
        const val UNIX_NEWLINE = "\n\n"
        const val WINDOWS_NEWLINE = "\r\n\r\n"
        val CRLFRegex = Regex("[\r\n] ")
    }

    override fun deserialize(inputStream: InputStream): List<String> {
        val builder = StringBuilder()
        while(!builder.endsWith(UNIX_NEWLINE) && !builder.endsWith(WINDOWS_NEWLINE))
            builder.append(inputStream.read().toChar())
        return builder.toString().split(CRLFRegex).filter { it.isNotEmpty() }
    }

}

This deserializer reads input until it finds a double new line (here either \n\n or \r\n\r\n, you can modify this as you want) at which point it removes the message delimiter and then returns the message as a list of lines.

Finally, set your new deserializer on the ServerConnectionFactory:

serverConnectionFactory.deserializer = DoubleNewLineDeserializer()

Input to TCP socket:

foo=bar
me=you
year=123
[empty line]
[empty line]

Input to handler:

[foo=bar, me=you, year=123]
  • Related