You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

LineBufferedSocket.kt 4.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. package com.dmdirc.ktirc.io
  2. import com.dmdirc.ktirc.util.logger
  3. import kotlinx.coroutines.*
  4. import kotlinx.coroutines.channels.Channel
  5. import kotlinx.coroutines.channels.ReceiveChannel
  6. import kotlinx.coroutines.channels.SendChannel
  7. import kotlinx.coroutines.channels.produce
  8. import kotlinx.coroutines.io.ByteWriteChannel
  9. import kotlinx.io.core.String
  10. import kotlinx.io.pool.useInstance
  11. import java.net.InetSocketAddress
  12. import java.nio.ByteBuffer
  13. import java.security.SecureRandom
  14. import java.security.cert.CertificateException
  15. import javax.net.ssl.SSLContext
  16. import javax.net.ssl.X509TrustManager
  17. internal interface LineBufferedSocket {
  18. @Throws(CertificateException::class)
  19. fun connect()
  20. fun disconnect()
  21. val sendChannel: SendChannel<ByteArray>
  22. val receiveChannel: ReceiveChannel<ByteArray>
  23. }
  24. /**
  25. * Asynchronous socket that buffers incoming data and emits individual lines.
  26. */
  27. // TODO: Expose advanced TLS options
  28. @ExperimentalCoroutinesApi
  29. internal class LineBufferedSocketImpl(coroutineScope: CoroutineScope, private val host: String, private val ip: String, private val port: Int, private val tls: Boolean = false) : CoroutineScope, LineBufferedSocket {
  30. companion object {
  31. const val CARRIAGE_RETURN = '\r'.toByte()
  32. const val LINE_FEED = '\n'.toByte()
  33. }
  34. override val coroutineContext = coroutineScope.newCoroutineContext(Dispatchers.IO)
  35. override val sendChannel: Channel<ByteArray> = Channel(Channel.UNLIMITED)
  36. var tlsTrustManager: X509TrustManager? = null
  37. private val log by logger()
  38. private lateinit var socket: Socket
  39. private lateinit var writeChannel: ByteWriteChannel
  40. override fun connect() {
  41. log.info { "Connecting..." }
  42. socket = PlainTextSocket(this)
  43. runBlocking {
  44. if (tls) {
  45. with(SSLContext.getInstance("TLSv1.2")) {
  46. init(null, tlsTrustManager?.let { arrayOf(it) }, SecureRandom())
  47. socket = TlsSocket(this@LineBufferedSocketImpl, socket, this, host)
  48. }
  49. }
  50. socket.connect(InetSocketAddress(ip, port))
  51. writeChannel = socket.write
  52. }
  53. launch { writeLines() }
  54. }
  55. override fun disconnect() {
  56. socket.close()
  57. sendChannel.close()
  58. coroutineContext.cancel()
  59. }
  60. override val receiveChannel
  61. get() = produce {
  62. byteBufferPool.useInstance { lineBuffer ->
  63. while (socket.isOpen) {
  64. val buffer = socket.read() ?: return@produce
  65. var lastLine = 0
  66. for (i in 0 until buffer.limit()) {
  67. if (buffer[i] == CARRIAGE_RETURN || buffer[i] == LINE_FEED) {
  68. val length = i - lastLine + lineBuffer.position()
  69. if (length > 1) {
  70. val output = ByteBuffer.allocate(length)
  71. lineBuffer.flip()
  72. output.put(lineBuffer)
  73. lineBuffer.clear()
  74. output.put(buffer.array(), lastLine, i - lastLine)
  75. log.fine { "<<< ${String(output.array())}" }
  76. send(output.array())
  77. }
  78. lastLine = i + 1
  79. }
  80. }
  81. lineBuffer.put(buffer.array(), lastLine, buffer.limit() - lastLine)
  82. byteBufferPool.recycle(buffer)
  83. }
  84. }
  85. }
  86. private suspend fun writeLines() {
  87. for (line in sendChannel) {
  88. with(writeChannel) {
  89. log.fine { ">>> ${String(line)}" }
  90. writeAvailable(line, 0, line.size)
  91. writeByte(CARRIAGE_RETURN)
  92. writeByte(LINE_FEED)
  93. flush()
  94. }
  95. }
  96. }
  97. }