Refactor KMP networking backends

This commit is contained in:
sergeych committed 2026-04-02 20:04:25 +03:00
1 parent d0aaa2c256
commit cd7e001f41
22 files changed
+706 -851

No files matched your search

+8
View File
@@ -86,6 +86,9 @@ kotlin {
}
val nativeMain by creating {
dependsOn(commonMain)
dependencies {
implementation(libs.ktor.network)
}
}
val darwinMain by creating {
dependsOn(nativeMain)
@@ -117,6 +120,9 @@ kotlin {
implementation(libs.kotlinx.coroutines.test)
}
}
val linuxTest by creating {
dependsOn(commonTest)
}
val iosX64Main by getting { dependsOn(iosMain) }
val iosArm64Main by getting { dependsOn(iosMain) }
val iosSimulatorArm64Main by getting { dependsOn(iosMain) }
@@ -124,6 +130,8 @@ kotlin {
val mingwX64Main by getting { dependsOn(mingwMain) }
val linuxX64Main by getting { dependsOn(linuxMain) }
val linuxArm64Main by getting { dependsOn(linuxMain) }
val linuxX64Test by getting { dependsOn(linuxTest) }
val linuxArm64Test by getting { dependsOn(linuxTest) }
// JS: use runtime detection in jsMain to select Node vs Browser implementation
val jsMain by getting {
@@ -1,58 +1,5 @@
package net.sergeych.lyngio.http
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.timeout
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.request
import io.ktor.client.request.setBody
import io.ktor.http.HttpMethod
import io.ktor.http.headers
import io.ktor.http.takeFrom
actual fun getSystemHttpEngine(): LyngHttpEngine = AndroidKtorLyngHttpEngine
private object AndroidKtorLyngHttpEngine : LyngHttpEngine {
private val clientResult by lazy {
runCatching {
HttpClient(CIO) {
expectSuccess = false
}
}
}
override val isSupported: Boolean
get() = clientResult.isSuccess
override suspend fun request(request: LyngHttpRequest): LyngHttpResponse {
val httpClient = clientResult.getOrElse {
throw UnsupportedOperationException(it.message ?: "HTTP client is not supported")
}
val response = httpClient.request {
applyRequest(request)
}
return LyngHttpResponse(
status = response.status.value,
statusText = response.status.description,
headers = response.headers.entries().associate { it.key to it.value.toList() },
bodyBytes = response.body<ByteArray>(),
)
}
private fun HttpRequestBuilder.applyRequest(request: LyngHttpRequest) {
method = HttpMethod.parse(request.method.uppercase())
url.takeFrom(request.url)
headers {
request.headers.forEach { (name, value) -> append(name, value) }
}
request.timeoutMillis?.let { timeout { requestTimeoutMillis = it } }
when {
request.bodyBytes != null && request.bodyText != null ->
throw IllegalArgumentException("Only one of bodyText or bodyBytes may be set")
request.bodyBytes != null -> setBody(request.bodyBytes)
request.bodyText != null -> setBody(request.bodyText)
}
}
}
actual fun getSystemHttpEngine(): LyngHttpEngine = createKtorHttpEngine(CIO)
@@ -1,92 +1,5 @@
package net.sergeych.lyngio.ws
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.client.plugins.websocket.webSocketSession
import io.ktor.client.request.header
import io.ktor.client.request.url
import io.ktor.websocket.CloseReason
import io.ktor.websocket.DefaultWebSocketSession
import io.ktor.websocket.Frame
import io.ktor.websocket.close
import io.ktor.websocket.readText
import io.ktor.websocket.send
import kotlinx.coroutines.channels.ClosedReceiveChannelException
actual fun getSystemWsEngine(): LyngWsEngine = AndroidKtorWsEngine
private object AndroidKtorWsEngine : LyngWsEngine {
private val clientResult by lazy {
runCatching {
HttpClient(CIO) {
install(WebSockets)
}
}
}
override val isSupported: Boolean
get() = clientResult.isSuccess
override suspend fun connect(url: String, headers: Map<String, String>): LyngWsSession {
val client = clientResult.getOrElse {
throw UnsupportedOperationException(it.message ?: "WebSocket client is not supported")
}
val session = client.webSocketSession {
url(url)
headers.forEach { (name, value) -> header(name, value) }
}
return AndroidLyngWsSession(url, session)
}
}
private class AndroidLyngWsSession(
private val targetUrl: String,
private val session: DefaultWebSocketSession,
) : LyngWsSession {
@Volatile
private var closed = false
override fun isOpen(): Boolean = !closed
override fun url(): String = targetUrl
override suspend fun sendText(text: String) {
ensureOpen()
session.send(text)
}
override suspend fun sendBytes(data: ByteArray) {
ensureOpen()
session.send(data)
}
override suspend fun receive(): LyngWsMessage? {
if (closed) return null
val frame = try {
session.incoming.receive()
} catch (_: ClosedReceiveChannelException) {
closed = true
return null
}
return when (frame) {
is Frame.Text -> LyngWsMessage(isText = true, text = frame.readText())
is Frame.Binary -> LyngWsMessage(isText = false, data = frame.data.copyOf())
is Frame.Close -> {
closed = true
null
}
else -> receive()
}
}
override suspend fun close(code: Int, reason: String) {
if (closed) return
closed = true
session.close(CloseReason(code.toShort(), reason))
}
private fun ensureOpen() {
if (closed) throw IllegalStateException("websocket session is closed")
}
}
actual fun getSystemWsEngine(): LyngWsEngine = createKtorWsEngine(CIO)
@@ -0,0 +1,61 @@
package net.sergeych.lyngio.http
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.HttpClientEngineConfig
import io.ktor.client.engine.HttpClientEngineFactory
import io.ktor.client.plugins.timeout
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.request
import io.ktor.client.request.setBody
import io.ktor.http.HttpMethod
import io.ktor.http.headers
import io.ktor.http.takeFrom
internal fun createKtorHttpEngine(
engineFactory: HttpClientEngineFactory<HttpClientEngineConfig>,
): LyngHttpEngine = KtorLyngHttpEngine(engineFactory)
private class KtorLyngHttpEngine(
engineFactory: HttpClientEngineFactory<HttpClientEngineConfig>,
) : LyngHttpEngine {
private val clientResult = runCatching {
HttpClient(engineFactory) {
expectSuccess = false
}
}
override val isSupported: Boolean
get() = clientResult.isSuccess
override suspend fun request(request: LyngHttpRequest): LyngHttpResponse {
val httpClient = clientResult.getOrElse {
throw UnsupportedOperationException(it.message ?: "HTTP client is not supported")
}
val response = httpClient.request {
applyRequest(request)
}
return LyngHttpResponse(
status = response.status.value,
statusText = response.status.description,
headers = response.headers.entries().associate { it.key to it.value.toList() },
bodyBytes = response.body<ByteArray>(),
)
}
private fun HttpRequestBuilder.applyRequest(request: LyngHttpRequest) {
method = HttpMethod.parse(request.method.uppercase())
url.takeFrom(request.url)
headers {
request.headers.forEach { (name, value) -> append(name, value) }
}
request.timeoutMillis?.let { timeout { requestTimeoutMillis = it } }
when {
request.bodyBytes != null && request.bodyText != null ->
throw IllegalArgumentException("Only one of bodyText or bodyBytes may be set")
request.bodyBytes != null -> setBody(request.bodyBytes)
request.bodyText != null -> setBody(request.bodyText)
}
}
}
@@ -0,0 +1,94 @@
package net.sergeych.lyngio.ws
import io.ktor.client.HttpClient
import io.ktor.client.engine.HttpClientEngineConfig
import io.ktor.client.engine.HttpClientEngineFactory
import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.client.plugins.websocket.webSocketSession
import io.ktor.client.request.header
import io.ktor.client.request.url
import io.ktor.websocket.CloseReason
import io.ktor.websocket.DefaultWebSocketSession
import io.ktor.websocket.Frame
import io.ktor.websocket.close
import io.ktor.websocket.readText
import io.ktor.websocket.send
import kotlinx.coroutines.channels.ClosedReceiveChannelException
internal fun createKtorWsEngine(
engineFactory: HttpClientEngineFactory<HttpClientEngineConfig>,
): LyngWsEngine = KtorLyngWsEngine(engineFactory)
private class KtorLyngWsEngine(
engineFactory: HttpClientEngineFactory<HttpClientEngineConfig>,
) : LyngWsEngine {
private val clientResult = runCatching {
HttpClient(engineFactory) {
install(WebSockets)
}
}
override val isSupported: Boolean
get() = clientResult.isSuccess
override suspend fun connect(url: String, headers: Map<String, String>): LyngWsSession {
val client = clientResult.getOrElse {
throw UnsupportedOperationException(it.message ?: "WebSocket client is not supported")
}
val session = client.webSocketSession {
url(url)
headers.forEach { (name, value) -> header(name, value) }
}
return KtorLyngWsSession(url, session)
}
}
private class KtorLyngWsSession(
private val targetUrl: String,
private val session: DefaultWebSocketSession,
) : LyngWsSession {
private var closed = false
override fun isOpen(): Boolean = !closed
override fun url(): String = targetUrl
override suspend fun sendText(text: String) {
ensureOpen()
session.send(text)
}
override suspend fun sendBytes(data: ByteArray) {
ensureOpen()
session.send(data)
}
override suspend fun receive(): LyngWsMessage? {
if (closed) return null
val frame = try {
session.incoming.receive()
} catch (_: ClosedReceiveChannelException) {
closed = true
return null
}
return when (frame) {
is Frame.Text -> LyngWsMessage(isText = true, text = frame.readText())
is Frame.Binary -> LyngWsMessage(isText = false, data = frame.data.copyOf())
is Frame.Close -> {
closed = true
null
}
else -> receive()
}
}
override suspend fun close(code: Int, reason: String) {
if (closed) return
closed = true
session.close(CloseReason(code.toShort(), reason))
}
private fun ensureOpen() {
if (closed) throw IllegalStateException("websocket session is closed")
}
}
@@ -1,58 +1,5 @@
package net.sergeych.lyngio.http
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.darwin.Darwin
import io.ktor.client.plugins.timeout
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.request
import io.ktor.client.request.setBody
import io.ktor.http.HttpMethod
import io.ktor.http.headers
import io.ktor.http.takeFrom
actual fun getSystemHttpEngine(): LyngHttpEngine = DarwinLyngHttpEngine
private object DarwinLyngHttpEngine : LyngHttpEngine {
private val clientResult by lazy {
runCatching {
HttpClient(Darwin) {
expectSuccess = false
}
}
}
override val isSupported: Boolean
get() = clientResult.isSuccess
override suspend fun request(request: LyngHttpRequest): LyngHttpResponse {
val httpClient = clientResult.getOrElse {
throw UnsupportedOperationException(it.message ?: "HTTP client is not supported")
}
val response = httpClient.request {
applyRequest(request)
}
return LyngHttpResponse(
status = response.status.value,
statusText = response.status.description,
headers = response.headers.entries().associate { it.key to it.value.toList() },
bodyBytes = response.body<ByteArray>(),
)
}
private fun HttpRequestBuilder.applyRequest(request: LyngHttpRequest) {
method = HttpMethod.parse(request.method.uppercase())
url.takeFrom(request.url)
headers {
request.headers.forEach { (name, value) -> append(name, value) }
}
request.timeoutMillis?.let { timeout { requestTimeoutMillis = it } }
when {
request.bodyBytes != null && request.bodyText != null ->
throw IllegalArgumentException("Only one of bodyText or bodyBytes may be set")
request.bodyBytes != null -> setBody(request.bodyBytes)
request.bodyText != null -> setBody(request.bodyText)
}
}
}
actual fun getSystemHttpEngine(): LyngHttpEngine = createKtorHttpEngine(Darwin)
@@ -0,0 +1,8 @@
package net.sergeych.lyngio.net
actual fun getSystemNetEngine(): LyngNetEngine = createNativeKtorNetEngine(
isSupported = true,
isTcpAvailable = true,
isTcpServerAvailable = true,
isUdpAvailable = true,
)
@@ -1,91 +1,5 @@
package net.sergeych.lyngio.ws
import io.ktor.client.HttpClient
import io.ktor.client.engine.darwin.Darwin
import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.client.plugins.websocket.webSocketSession
import io.ktor.client.request.header
import io.ktor.client.request.url
import io.ktor.websocket.CloseReason
import io.ktor.websocket.DefaultWebSocketSession
import io.ktor.websocket.Frame
import io.ktor.websocket.close
import io.ktor.websocket.readText
import io.ktor.websocket.send
import kotlinx.coroutines.channels.ClosedReceiveChannelException
actual fun getSystemWsEngine(): LyngWsEngine = DarwinKtorWsEngine
private object DarwinKtorWsEngine : LyngWsEngine {
private val clientResult by lazy {
runCatching {
HttpClient(Darwin) {
install(WebSockets)
}
}
}
override val isSupported: Boolean
get() = clientResult.isSuccess
override suspend fun connect(url: String, headers: Map<String, String>): LyngWsSession {
val client = clientResult.getOrElse {
throw UnsupportedOperationException(it.message ?: "WebSocket client is not supported")
}
val session = client.webSocketSession {
url(url)
headers.forEach { (name, value) -> header(name, value) }
}
return DarwinLyngWsSession(url, session)
}
}
private class DarwinLyngWsSession(
private val targetUrl: String,
private val session: DefaultWebSocketSession,
) : LyngWsSession {
private var closed = false
override fun isOpen(): Boolean = !closed
override fun url(): String = targetUrl
override suspend fun sendText(text: String) {
ensureOpen()
session.send(text)
}
override suspend fun sendBytes(data: ByteArray) {
ensureOpen()
session.send(data)
}
override suspend fun receive(): LyngWsMessage? {
if (closed) return null
val frame = try {
session.incoming.receive()
} catch (_: ClosedReceiveChannelException) {
closed = true
return null
}
return when (frame) {
is Frame.Text -> LyngWsMessage(isText = true, text = frame.readText())
is Frame.Binary -> LyngWsMessage(isText = false, data = frame.data.copyOf())
is Frame.Close -> {
closed = true
null
}
else -> receive()
}
}
override suspend fun close(code: Int, reason: String) {
if (closed) return
closed = true
session.close(CloseReason(code.toShort(), reason))
}
private fun ensureOpen() {
if (closed) throw IllegalStateException("websocket session is closed")
}
}
actual fun getSystemWsEngine(): LyngWsEngine = createKtorWsEngine(Darwin)
@@ -1,58 +1,5 @@
package net.sergeych.lyngio.http
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.js.Js
import io.ktor.client.plugins.timeout
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.request
import io.ktor.client.request.setBody
import io.ktor.http.HttpMethod
import io.ktor.http.headers
import io.ktor.http.takeFrom
actual fun getSystemHttpEngine(): LyngHttpEngine = JsKtorLyngHttpEngine
private object JsKtorLyngHttpEngine : LyngHttpEngine {
private val clientResult by lazy {
runCatching {
HttpClient(Js) {
expectSuccess = false
}
}
}
override val isSupported: Boolean
get() = clientResult.isSuccess
override suspend fun request(request: LyngHttpRequest): LyngHttpResponse {
val httpClient = clientResult.getOrElse {
throw UnsupportedOperationException(it.message ?: "HTTP client is not supported")
}
val response = httpClient.request {
applyRequest(request)
}
return LyngHttpResponse(
status = response.status.value,
statusText = response.status.description,
headers = response.headers.entries().associate { it.key to it.value.toList() },
bodyBytes = response.body<ByteArray>(),
)
}
private fun HttpRequestBuilder.applyRequest(request: LyngHttpRequest) {
method = HttpMethod.parse(request.method.uppercase())
url.takeFrom(request.url)
headers {
request.headers.forEach { (name, value) -> append(name, value) }
}
request.timeoutMillis?.let { timeout { requestTimeoutMillis = it } }
when {
request.bodyBytes != null && request.bodyText != null ->
throw IllegalArgumentException("Only one of bodyText or bodyBytes may be set")
request.bodyBytes != null -> setBody(request.bodyBytes)
request.bodyText != null -> setBody(request.bodyText)
}
}
}
actual fun getSystemHttpEngine(): LyngHttpEngine = createKtorHttpEngine(Js)
@@ -1,91 +1,5 @@
package net.sergeych.lyngio.ws
import io.ktor.client.HttpClient
import io.ktor.client.engine.js.Js
import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.client.plugins.websocket.webSocketSession
import io.ktor.client.request.header
import io.ktor.client.request.url
import io.ktor.websocket.CloseReason
import io.ktor.websocket.DefaultWebSocketSession
import io.ktor.websocket.Frame
import io.ktor.websocket.close
import io.ktor.websocket.readText
import io.ktor.websocket.send
import kotlinx.coroutines.channels.ClosedReceiveChannelException
actual fun getSystemWsEngine(): LyngWsEngine = JsKtorWsEngine
private object JsKtorWsEngine : LyngWsEngine {
private val clientResult by lazy {
runCatching {
HttpClient(Js) {
install(WebSockets)
}
}
}
override val isSupported: Boolean
get() = clientResult.isSuccess
override suspend fun connect(url: String, headers: Map<String, String>): LyngWsSession {
val client = clientResult.getOrElse {
throw UnsupportedOperationException(it.message ?: "WebSocket client is not supported")
}
val session = client.webSocketSession {
url(url)
headers.forEach { (name, value) -> header(name, value) }
}
return JsLyngWsSession(url, session)
}
}
private class JsLyngWsSession(
private val targetUrl: String,
private val session: DefaultWebSocketSession,
) : LyngWsSession {
private var closed = false
override fun isOpen(): Boolean = !closed
override fun url(): String = targetUrl
override suspend fun sendText(text: String) {
ensureOpen()
session.send(text)
}
override suspend fun sendBytes(data: ByteArray) {
ensureOpen()
session.send(data)
}
override suspend fun receive(): LyngWsMessage? {
if (closed) return null
val frame = try {
session.incoming.receive()
} catch (_: ClosedReceiveChannelException) {
closed = true
return null
}
return when (frame) {
is Frame.Text -> LyngWsMessage(isText = true, text = frame.readText())
is Frame.Binary -> LyngWsMessage(isText = false, data = frame.data.copyOf())
is Frame.Close -> {
closed = true
null
}
else -> receive()
}
}
override suspend fun close(code: Int, reason: String) {
if (closed) return
closed = true
session.close(CloseReason(code.toShort(), reason))
}
private fun ensureOpen() {
if (closed) throw IllegalStateException("websocket session is closed")
}
}
actual fun getSystemWsEngine(): LyngWsEngine = createKtorWsEngine(Js)
@@ -1,58 +1,5 @@
package net.sergeych.lyngio.http
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.timeout
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.request
import io.ktor.client.request.setBody
import io.ktor.http.HttpMethod
import io.ktor.http.headers
import io.ktor.http.takeFrom
actual fun getSystemHttpEngine(): LyngHttpEngine = JvmKtorLyngHttpEngine
private object JvmKtorLyngHttpEngine : LyngHttpEngine {
private val clientResult by lazy {
runCatching {
HttpClient(CIO) {
expectSuccess = false
}
}
}
override val isSupported: Boolean
get() = clientResult.isSuccess
override suspend fun request(request: LyngHttpRequest): LyngHttpResponse {
val httpClient = clientResult.getOrElse {
throw UnsupportedOperationException(it.message ?: "HTTP client is not supported")
}
val response = httpClient.request {
applyRequest(request)
}
return LyngHttpResponse(
status = response.status.value,
statusText = response.status.description,
headers = response.headers.entries().associate { it.key to it.value.toList() },
bodyBytes = response.body<ByteArray>(),
)
}
private fun HttpRequestBuilder.applyRequest(request: LyngHttpRequest) {
method = HttpMethod.parse(request.method.uppercase())
url.takeFrom(request.url)
headers {
request.headers.forEach { (name, value) -> append(name, value) }
}
request.timeoutMillis?.let { timeout { requestTimeoutMillis = it } }
when {
request.bodyBytes != null && request.bodyText != null ->
throw IllegalArgumentException("Only one of bodyText or bodyBytes may be set")
request.bodyBytes != null -> setBody(request.bodyBytes)
request.bodyText != null -> setBody(request.bodyText)
}
}
}
actual fun getSystemHttpEngine(): LyngHttpEngine = createKtorHttpEngine(CIO)
@@ -17,94 +17,6 @@
package net.sergeych.lyngio.ws
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.client.plugins.websocket.webSocketSession
import io.ktor.client.request.header
import io.ktor.client.request.url
import io.ktor.websocket.CloseReason
import io.ktor.websocket.DefaultWebSocketSession
import io.ktor.websocket.Frame
import io.ktor.websocket.close
import io.ktor.websocket.readText
import io.ktor.websocket.send
import kotlinx.coroutines.channels.ClosedReceiveChannelException
actual fun getSystemWsEngine(): LyngWsEngine = JvmKtorWsEngine
private object JvmKtorWsEngine : LyngWsEngine {
private val clientResult by lazy {
runCatching {
HttpClient(CIO) {
install(WebSockets)
}
}
}
override val isSupported: Boolean
get() = clientResult.isSuccess
override suspend fun connect(url: String, headers: Map<String, String>): LyngWsSession {
val client = clientResult.getOrElse {
throw UnsupportedOperationException(it.message ?: "WebSocket client is not supported")
}
val session = client.webSocketSession {
url(url)
headers.forEach { (name, value) -> header(name, value) }
}
return JvmLyngWsSession(url, session)
}
}
private class JvmLyngWsSession(
private val targetUrl: String,
private val session: DefaultWebSocketSession,
) : LyngWsSession {
@Volatile
private var closed = false
override fun isOpen(): Boolean = !closed
override fun url(): String = targetUrl
override suspend fun sendText(text: String) {
ensureOpen()
session.send(text)
}
override suspend fun sendBytes(data: ByteArray) {
ensureOpen()
session.send(data)
}
override suspend fun receive(): LyngWsMessage? {
if (closed) return null
val frame = try {
session.incoming.receive()
} catch (_: ClosedReceiveChannelException) {
closed = true
return null
}
return when (frame) {
is Frame.Text -> LyngWsMessage(isText = true, text = frame.readText())
is Frame.Binary -> LyngWsMessage(isText = false, data = frame.data.copyOf())
is Frame.Close -> {
closed = true
null
}
else -> receive()
}
}
override suspend fun close(code: Int, reason: String) {
if (closed) return
closed = true
val safeCode = code.toShort()
session.close(CloseReason(safeCode, reason))
}
private fun ensureOpen() {
if (closed) throw IllegalStateException("websocket session is closed")
}
}
actual fun getSystemWsEngine(): LyngWsEngine = createKtorWsEngine(CIO)
@@ -1,58 +1,5 @@
package net.sergeych.lyngio.http
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.curl.Curl
import io.ktor.client.plugins.timeout
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.request
import io.ktor.client.request.setBody
import io.ktor.http.HttpMethod
import io.ktor.http.headers
import io.ktor.http.takeFrom
actual fun getSystemHttpEngine(): LyngHttpEngine = LinuxLyngHttpEngine
private object LinuxLyngHttpEngine : LyngHttpEngine {
private val clientResult by lazy {
runCatching {
HttpClient(Curl) {
expectSuccess = false
}
}
}
override val isSupported: Boolean
get() = clientResult.isSuccess
override suspend fun request(request: LyngHttpRequest): LyngHttpResponse {
val httpClient = clientResult.getOrElse {
throw UnsupportedOperationException(it.message ?: "HTTP client is not supported")
}
val response = httpClient.request {
applyRequest(request)
}
return LyngHttpResponse(
status = response.status.value,
statusText = response.status.description,
headers = response.headers.entries().associate { it.key to it.value.toList() },
bodyBytes = response.body<ByteArray>(),
)
}
private fun HttpRequestBuilder.applyRequest(request: LyngHttpRequest) {
method = HttpMethod.parse(request.method.uppercase())
url.takeFrom(request.url)
headers {
request.headers.forEach { (name, value) -> append(name, value) }
}
request.timeoutMillis?.let { timeout { requestTimeoutMillis = it } }
when {
request.bodyBytes != null && request.bodyText != null ->
throw IllegalArgumentException("Only one of bodyText or bodyBytes may be set")
request.bodyBytes != null -> setBody(request.bodyBytes)
request.bodyText != null -> setBody(request.bodyText)
}
}
}
actual fun getSystemHttpEngine(): LyngHttpEngine = createKtorHttpEngine(Curl)
@@ -0,0 +1,8 @@
package net.sergeych.lyngio.net
actual fun getSystemNetEngine(): LyngNetEngine = createNativeKtorNetEngine(
isSupported = true,
isTcpAvailable = true,
isTcpServerAvailable = true,
isUdpAvailable = true,
)
@@ -1,91 +1,5 @@
package net.sergeych.lyngio.ws
import io.ktor.client.HttpClient
import io.ktor.client.engine.curl.Curl
import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.client.plugins.websocket.webSocketSession
import io.ktor.client.request.header
import io.ktor.client.request.url
import io.ktor.websocket.CloseReason
import io.ktor.websocket.DefaultWebSocketSession
import io.ktor.websocket.Frame
import io.ktor.websocket.close
import io.ktor.websocket.readText
import io.ktor.websocket.send
import kotlinx.coroutines.channels.ClosedReceiveChannelException
actual fun getSystemWsEngine(): LyngWsEngine = LinuxKtorWsEngine
private object LinuxKtorWsEngine : LyngWsEngine {
private val clientResult by lazy {
runCatching {
HttpClient(Curl) {
install(WebSockets)
}
}
}
override val isSupported: Boolean
get() = clientResult.isSuccess
override suspend fun connect(url: String, headers: Map<String, String>): LyngWsSession {
val client = clientResult.getOrElse {
throw UnsupportedOperationException(it.message ?: "WebSocket client is not supported")
}
val session = client.webSocketSession {
url(url)
headers.forEach { (name, value) -> header(name, value) }
}
return LinuxLyngWsSession(url, session)
}
}
private class LinuxLyngWsSession(
private val targetUrl: String,
private val session: DefaultWebSocketSession,
) : LyngWsSession {
private var closed = false
override fun isOpen(): Boolean = !closed
override fun url(): String = targetUrl
override suspend fun sendText(text: String) {
ensureOpen()
session.send(text)
}
override suspend fun sendBytes(data: ByteArray) {
ensureOpen()
session.send(data)
}
override suspend fun receive(): LyngWsMessage? {
if (closed) return null
val frame = try {
session.incoming.receive()
} catch (_: ClosedReceiveChannelException) {
closed = true
return null
}
return when (frame) {
is Frame.Text -> LyngWsMessage(isText = true, text = frame.readText())
is Frame.Binary -> LyngWsMessage(isText = false, data = frame.data.copyOf())
is Frame.Close -> {
closed = true
null
}
else -> receive()
}
}
override suspend fun close(code: Int, reason: String) {
if (closed) return
closed = true
session.close(CloseReason(code.toShort(), reason))
}
private fun ensureOpen() {
if (closed) throw IllegalStateException("websocket session is closed")
}
}
actual fun getSystemWsEngine(): LyngWsEngine = createKtorWsEngine(Curl)
@@ -0,0 +1,90 @@
package net.sergeych.lyngio.net
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import net.sergeych.lyng.Compiler
import net.sergeych.lyng.Script
import net.sergeych.lyng.io.net.createNetModule
import net.sergeych.lyngio.net.security.PermitAllNetAccessPolicy
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class NetLinuxNativeTest {
@Test
fun testLinuxNativeCapabilitiesAndResolve() = runBlocking {
val engine = getSystemNetEngine()
assertTrue(engine.isSupported)
assertTrue(engine.isTcpAvailable)
assertTrue(engine.isTcpServerAvailable)
assertTrue(engine.isUdpAvailable)
val resolved = engine.resolve("127.0.0.1", 4040)
assertEquals(1, resolved.size)
assertEquals("127.0.0.1", resolved.single().host)
assertEquals(4040, resolved.single().port)
assertEquals(LyngIpVersion.IPV4, resolved.single().ipVersion)
assertTrue(resolved.single().resolved)
}
@Test
fun testLinuxNativeLyngModuleCapabilities() = runBlocking {
val scope = Script.newScope()
createNetModule(PermitAllNetAccessPolicy, scope)
val code = """
import lyng.io.net
val a: SocketAddress = Net.resolve("127.0.0.1", 4040)[0]
[Net.isSupported(), Net.isTcpAvailable(), Net.isTcpServerAvailable(), Net.isUdpAvailable(), a.toString(), a.resolved]
""".trimIndent()
val result = Compiler.compile(code).execute(scope).inspect(scope)
assertTrue(result.contains("true,true,true,true"), result)
assertTrue(result.contains("127.0.0.1:4040"), result)
}
@Test
fun testLinuxNativeTcpAndUdpLoopback() = runBlocking {
val engine = getSystemNetEngine()
withTimeout(5_000) {
val server = engine.tcpListen(host = "127.0.0.1", port = 0, backlog = 16, reuseAddress = true)
val accepted = async {
val client = server.accept()
val text = client.read(4)?.decodeToString()
client.writeUtf8("echo:$text")
client.flush()
client.close()
server.close()
text
}
val socket = engine.tcpConnect("127.0.0.1", server.localAddress().port, timeoutMillis = 2_000, noDelay = true)
socket.writeUtf8("ping")
socket.flush()
val reply = socket.read(32)?.decodeToString()
socket.close()
assertEquals("ping", accepted.await())
assertEquals("echo:ping", reply)
}
withTimeout(5_000) {
val receiver = engine.udpBind(host = "127.0.0.1", port = 0, reuseAddress = true)
val sender = engine.udpBind(host = "127.0.0.1", port = 0, reuseAddress = true)
sender.send("ping".encodeToByteArray(), "127.0.0.1", receiver.localAddress().port)
val datagram = receiver.receive(32)
sender.close()
receiver.close()
assertEquals("ping", datagram?.data?.decodeToString())
assertTrue((datagram?.address?.port ?: 0) > 0)
}
}
}
@@ -1,58 +1,5 @@
package net.sergeych.lyngio.http
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.winhttp.WinHttp
import io.ktor.client.plugins.timeout
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.request
import io.ktor.client.request.setBody
import io.ktor.http.HttpMethod
import io.ktor.http.headers
import io.ktor.http.takeFrom
actual fun getSystemHttpEngine(): LyngHttpEngine = MingwLyngHttpEngine
private object MingwLyngHttpEngine : LyngHttpEngine {
private val clientResult by lazy {
runCatching {
HttpClient(WinHttp) {
expectSuccess = false
}
}
}
override val isSupported: Boolean
get() = clientResult.isSuccess
override suspend fun request(request: LyngHttpRequest): LyngHttpResponse {
val httpClient = clientResult.getOrElse {
throw UnsupportedOperationException(it.message ?: "HTTP client is not supported")
}
val response = httpClient.request {
applyRequest(request)
}
return LyngHttpResponse(
status = response.status.value,
statusText = response.status.description,
headers = response.headers.entries().associate { it.key to it.value.toList() },
bodyBytes = response.body<ByteArray>(),
)
}
private fun HttpRequestBuilder.applyRequest(request: LyngHttpRequest) {
method = HttpMethod.parse(request.method.uppercase())
url.takeFrom(request.url)
headers {
request.headers.forEach { (name, value) -> append(name, value) }
}
request.timeoutMillis?.let { timeout { requestTimeoutMillis = it } }
when {
request.bodyBytes != null && request.bodyText != null ->
throw IllegalArgumentException("Only one of bodyText or bodyBytes may be set")
request.bodyBytes != null -> setBody(request.bodyBytes)
request.bodyText != null -> setBody(request.bodyText)
}
}
}
actual fun getSystemHttpEngine(): LyngHttpEngine = createKtorHttpEngine(WinHttp)
@@ -1,91 +1,5 @@
package net.sergeych.lyngio.ws
import io.ktor.client.HttpClient
import io.ktor.client.engine.winhttp.WinHttp
import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.client.plugins.websocket.webSocketSession
import io.ktor.client.request.header
import io.ktor.client.request.url
import io.ktor.websocket.CloseReason
import io.ktor.websocket.DefaultWebSocketSession
import io.ktor.websocket.Frame
import io.ktor.websocket.close
import io.ktor.websocket.readText
import io.ktor.websocket.send
import kotlinx.coroutines.channels.ClosedReceiveChannelException
actual fun getSystemWsEngine(): LyngWsEngine = MingwKtorWsEngine
private object MingwKtorWsEngine : LyngWsEngine {
private val clientResult by lazy {
runCatching {
HttpClient(WinHttp) {
install(WebSockets)
}
}
}
override val isSupported: Boolean
get() = clientResult.isSuccess
override suspend fun connect(url: String, headers: Map<String, String>): LyngWsSession {
val client = clientResult.getOrElse {
throw UnsupportedOperationException(it.message ?: "WebSocket client is not supported")
}
val session = client.webSocketSession {
url(url)
headers.forEach { (name, value) -> header(name, value) }
}
return MingwLyngWsSession(url, session)
}
}
private class MingwLyngWsSession(
private val targetUrl: String,
private val session: DefaultWebSocketSession,
) : LyngWsSession {
private var closed = false
override fun isOpen(): Boolean = !closed
override fun url(): String = targetUrl
override suspend fun sendText(text: String) {
ensureOpen()
session.send(text)
}
override suspend fun sendBytes(data: ByteArray) {
ensureOpen()
session.send(data)
}
override suspend fun receive(): LyngWsMessage? {
if (closed) return null
val frame = try {
session.incoming.receive()
} catch (_: ClosedReceiveChannelException) {
closed = true
return null
}
return when (frame) {
is Frame.Text -> LyngWsMessage(isText = true, text = frame.readText())
is Frame.Binary -> LyngWsMessage(isText = false, data = frame.data.copyOf())
is Frame.Close -> {
closed = true
null
}
else -> receive()
}
}
override suspend fun close(code: Int, reason: String) {
if (closed) return
closed = true
session.close(CloseReason(code.toShort(), reason))
}
private fun ensureOpen() {
if (closed) throw IllegalStateException("websocket session is closed")
}
}
actual fun getSystemWsEngine(): LyngWsEngine = createKtorWsEngine(WinHttp)
@@ -0,0 +1,216 @@
package net.sergeych.lyngio.net
import io.ktor.network.selector.SelectorManager
import io.ktor.network.sockets.BoundDatagramSocket
import io.ktor.network.sockets.Datagram
import io.ktor.network.sockets.InetSocketAddress
import io.ktor.network.sockets.ServerSocket
import io.ktor.network.sockets.Socket
import io.ktor.network.sockets.SocketAddress
import io.ktor.network.sockets.aSocket
import io.ktor.network.sockets.isClosed
import io.ktor.network.sockets.openReadChannel
import io.ktor.network.sockets.openWriteChannel
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.ByteWriteChannel
import io.ktor.utils.io.readAvailable
import io.ktor.utils.io.readUTF8Line
import io.ktor.utils.io.writeFully
import io.ktor.utils.io.writeStringUtf8
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withTimeout
import kotlinx.io.Buffer
import kotlinx.io.readByteArray
internal fun createNativeKtorNetEngine(
isSupported: Boolean,
isTcpAvailable: Boolean,
isTcpServerAvailable: Boolean,
isUdpAvailable: Boolean,
): LyngNetEngine = NativeKtorNetEngine(
isSupported = isSupported,
isTcpAvailable = isTcpAvailable,
isTcpServerAvailable = isTcpServerAvailable,
isUdpAvailable = isUdpAvailable,
)
private class NativeKtorNetEngine(
override val isSupported: Boolean,
override val isTcpAvailable: Boolean,
override val isTcpServerAvailable: Boolean,
override val isUdpAvailable: Boolean,
) : LyngNetEngine {
private val selectorManager: SelectorManager by lazy { SelectorManager(Dispatchers.Default) }
override suspend fun resolve(host: String, port: Int): List<LyngSocketAddress> {
val rawAddress = InetSocketAddress(host, port).resolveAddress()
?: throw IllegalStateException("Failed to resolve address for $host")
return listOf(
LyngSocketAddress(
host = rawAddress.toIpHostString(),
port = port,
ipVersion = rawAddress.toLyngIpVersion(),
resolved = true,
)
)
}
override suspend fun tcpConnect(
host: String,
port: Int,
timeoutMillis: Long?,
noDelay: Boolean,
): LyngTcpSocket {
val connectBlock: suspend () -> Socket = {
aSocket(selectorManager).tcp().connect(host, port) {
this.noDelay = noDelay
}
}
val socket = if (timeoutMillis != null) withTimeout(timeoutMillis) { connectBlock() } else connectBlock()
return NativeLyngTcpSocket(socket)
}
override suspend fun tcpListen(
host: String?,
port: Int,
backlog: Int,
reuseAddress: Boolean,
): LyngTcpServer {
val bindHost = host ?: "0.0.0.0"
val server = aSocket(selectorManager).tcp().bind(bindHost, port) {
backlogSize = backlog
this.reuseAddress = reuseAddress
}
return NativeLyngTcpServer(server)
}
override suspend fun udpBind(host: String?, port: Int, reuseAddress: Boolean): LyngUdpSocket {
val bindHost = host ?: "0.0.0.0"
val socket = aSocket(selectorManager).udp().bind(bindHost, port) {
this.reuseAddress = reuseAddress
}
return NativeLyngUdpSocket(socket)
}
}
private class NativeLyngTcpSocket(
private val socket: Socket,
) : LyngTcpSocket {
private val input: ByteReadChannel by lazy { socket.openReadChannel() }
private val output: ByteWriteChannel by lazy { socket.openWriteChannel(autoFlush = true) }
override fun isOpen(): Boolean = !socket.isClosed
override fun localAddress(): LyngSocketAddress = socket.localAddress.toLyngSocketAddress(resolved = true)
override fun remoteAddress(): LyngSocketAddress = socket.remoteAddress.toLyngSocketAddress(resolved = true)
override suspend fun read(maxBytes: Int): ByteArray? {
if (!input.awaitContent(1)) return null
val buffer = ByteArray(maxBytes)
val count = input.readAvailable(buffer, 0, maxBytes)
return when {
count <= 0 -> null
count == maxBytes -> buffer
else -> buffer.copyOf(count)
}
}
override suspend fun readLine(): String? = input.readUTF8Line()
override suspend fun write(data: ByteArray) {
output.writeFully(data, 0, data.size)
}
override suspend fun writeUtf8(text: String) {
output.writeStringUtf8(text)
}
override suspend fun flush() {
output.flush()
}
override fun close() {
socket.close()
}
}
private class NativeLyngTcpServer(
private val server: ServerSocket,
) : LyngTcpServer {
override fun isOpen(): Boolean = !server.isClosed
override fun localAddress(): LyngSocketAddress = server.localAddress.toLyngSocketAddress(resolved = true)
override suspend fun accept(): LyngTcpSocket = NativeLyngTcpSocket(server.accept())
override fun close() {
server.close()
}
}
private class NativeLyngUdpSocket(
private val socket: BoundDatagramSocket,
) : LyngUdpSocket {
override fun isOpen(): Boolean = !socket.isClosed
override fun localAddress(): LyngSocketAddress = socket.localAddress.toLyngSocketAddress(resolved = true)
override suspend fun receive(maxBytes: Int): LyngDatagram? {
val datagram = try {
socket.receive()
} catch (e: Throwable) {
if (!isOpen()) return null
throw e
}
val bytes = datagram.packet.readByteArray().let {
if (it.size <= maxBytes) it else it.copyOf(maxBytes)
}
return LyngDatagram(bytes, datagram.address.toLyngSocketAddress(resolved = true))
}
override suspend fun send(data: ByteArray, host: String, port: Int) {
val packet = Buffer()
packet.write(data)
socket.send(Datagram(packet, InetSocketAddress(host, port)))
}
override fun close() {
socket.close()
}
}
private fun SocketAddress.toLyngSocketAddress(resolved: Boolean): LyngSocketAddress {
val inetAddress = this as? InetSocketAddress
if (inetAddress != null) {
val rawAddress = inetAddress.resolveAddress()
val host = rawAddress?.toIpHostString() ?: inetAddress.hostname
return LyngSocketAddress(
host = host,
port = inetAddress.port,
ipVersion = rawAddress?.toLyngIpVersion()
?: if (host.contains(':')) LyngIpVersion.IPV6 else LyngIpVersion.IPV4,
resolved = resolved,
)
}
val rendered = toString()
return LyngSocketAddress(
host = rendered,
port = 0,
ipVersion = if (rendered.contains(':')) LyngIpVersion.IPV6 else LyngIpVersion.IPV4,
resolved = resolved,
)
}
private fun ByteArray.toLyngIpVersion(): LyngIpVersion = if (size == 16) LyngIpVersion.IPV6 else LyngIpVersion.IPV4
private fun ByteArray.toIpHostString(): String = when (size) {
4 -> joinToString(".") { (it.toInt() and 0xff).toString() }
16 -> (0 until 8).joinToString(":") { index ->
val hi = this[index * 2].toInt() and 0xff
val lo = this[index * 2 + 1].toInt() and 0xff
((hi shl 8) or lo).toString(16)
}
else -> error("Unsupported IP address length: $size")
}