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]