Skip to main content Skip to docs navigation

TCP sockets, HTTP and WebSocket client and server, URL, QueryString, MimeType utils...

Document not reviewed yet, might be outdated. Please, let us know if you find something invalid here.
On this page

KorIO has utilities for handling network.

MimeType

fun VfsFile.mimeType(): MimeType

class MimeType(val mime: String, val exts: List<String>) : Vfs.Attribute {
	companion object {
		val APPLICATION_OCTET_STREAM = MimeType("application/octet-stream", listOf("bin"))
		val APPLICATION_JSON = MimeType("application/json", listOf("json"))
		val IMAGE_PNG = MimeType("image/png", listOf("png"))
		val IMAGE_JPEG = MimeType("image/jpeg", listOf("jpg", "jpeg"))
		val IMAGE_GIF = MimeType("image/gif", listOf("gif"))
		val TEXT_HTML = MimeType("text/html", listOf("htm", "html"))
		val TEXT_PLAIN = MimeType("text/plain", listOf("txt", "text"))
		val TEXT_CSS = MimeType("text/css", listOf("css"))
		val TEXT_JS = MimeType("application/javascript", listOf("js"))

		fun register(mimeType: MimeType)
		fun register(vararg mimeTypes: MimeType)
		fun register(mime: String, vararg exsts: String)

		fun getByExtension(ext: String, default: MimeType
	}
}

QueryString

object QueryString {
	fun decode(str: CharSequence): Map<String, List<String>>
	fun encode(map: Map<String, List<String>>): String
	fun encode(vararg items: Pair<String, String>): String
}

HostWithPort

data class HostWithPort(val host: String, val port: Int) {
	companion object {
		fun parse(str: String, defaultPort: Int): HostWithPort
	}
}

URL

fun createBase64URLForData(data: ByteArray, contentType: String): String

fun URL(url: String): URL
fun URL(
    scheme: String?,
    userInfo: String?,
    host: String?,
    path: String,
    query: String?,
    fragment: String?,
    opaque: Boolean = false,
    port: Int = DEFAULT_PORT
): URL

data class URL {
    val isOpaque: Boolean,
    val scheme: String?,
    val userInfo: String?,
    val host: String?,
    val path: String,
    val query: String?,
    val fragment: String?,
    val defaultPort: Int

    val user: String?
    val password: String?
    val isHierarchical: Boolean

    val port: Int
    val fullUrl: String

    val fullUrlWithoutScheme: String
    val pathWithQuery: String

    fun toUrlString(includeScheme: Boolean = true, out: StringBuilder = StringBuilder()): StringBuilder

    val isAbsolute: Boolean

    override fun toString(): String
    fun toComponentString(): String
    fun resolve(path: URL): URL

    companion object {
        val DEFAULT_PORT = 0
        fun isAbsolute(url: String): Boolean
        fun resolve(base: String, access: String): String
        fun decodeComponent(s: String, charset: Charset = UTF8, formUrlEncoded: Boolean = false): String
        fun encodeComponent(s: String, charset: Charset = UTF8, formUrlEncoded: Boolean = false): String
    }
}

TCP Client and Server

suspend fun createTcpClient(secure: Boolean = false): AsyncClient
suspend fun createTcpServer(port: Int = AsyncServer.ANY_PORT, host: String = "127.0.0.1", backlog: Int = 511, secure: Boolean = false): AsyncServer
suspend fun createTcpClient(host: String, port: Int, secure: Boolean = false): AsyncClient

interface AsyncClient : AsyncInputStream, AsyncOutputStream, AsyncCloseable {
    val connected: Boolean
    suspend fun connect(host: String, port: Int)
    override suspend fun read(buffer: ByteArray, offset: Int, len: Int): Int
    override suspend fun write(buffer: ByteArray, offset: Int, len: Int)
    override suspend fun close()

    object Stats {
        val writeCountStart: AtomicLong
        val writeCountEnd: AtomicLong
        val writeCountError: AtomicLong
    }

    companion object {
        suspend operator fun invoke(host: String, port: Int, secure: Boolean = false): AsyncClient
        suspend fun create(secure: Boolean = false): AsyncClient
        suspend fun createAndConnect(host: String, port: Int, secure: Boolean = false): AsyncClient
    }
}

interface AsyncServer {
    val requestPort: Int
    val host: String
    val backlog: Int
    val port: Int

    companion object {
        val ANY_PORT = 0
        suspend operator fun invoke(port: Int, host: String = "127.0.0.1", backlog: Int = -1): AsyncServer
    }

    suspend fun accept(): AsyncClient
    suspend fun listen(handler: suspend (AsyncClient) -> Unit): Closeable
    suspend fun listen(): ReceiveChannel<AsyncClient>
}

Http Client and Server

Common

interface Http {
    companion object {
        val Date = DateFormat("EEE, dd MMM yyyy HH:mm:ss z")
        fun TemporalRedirect(uri: String): RedirectException
        fun PermanentRedirect(uri: String): RedirectException
    }

    enum class Methods : Method {
        ALL, OPTIONS, GET, HEAD,
        POST, PUT, DELETE,
        TRACE, CONNECT, PATCH,
    }

    interface Method {
        val name: String

        companion object {
            val OPTIONS = Methods.OPTIONS
            val GET = Methods.GET
            val HEAD = Methods.HEAD
            val POST = Methods.POST
            val PUT = Methods.PUT
            val DELETE = Methods.DELETE
            val TRACE = Methods.TRACE
            val CONNECT = Methods.CONNECT
            val PATCH = Methods.PATCH

            fun values(): List<Method>
            val valuesMap: Map<String, Method>

            operator fun get(name: String): Method
            operator fun invoke(name: String): Method = this[name]
        }
    }

    data class CustomMethod(val _name: String) : Method {
        val nameUC: String
        override val name: String
        override fun toString(): String
    }

    open class HttpException(
        val statusCode: Int,
        val msg: String = "Error$statusCode",
        val statusText: String = HttpStatusMessage.CODES[statusCode] ?: "Error$statusCode",
        val headers: Http.Headers = Http.Headers()
    ) : IOException("$statusCode $statusText - $msg") {
        companion object {
            fun unauthorizedBasic(realm: String = "Realm", msg: String = "Unauthorized"): Nothing
        }
    }

    data class Auth(
        val user: String,
        val pass: String,
        val digest: String
    ) {
        companion object {
            fun parse(auth: String): Auth
        }

        fun validate(expectedUser: String, expectedPass: String, realm: String
        suspend fun checkBasic(realm: String = "Realm", check: suspend Auth.() -> Boolean)
    }

    class Request(val uri: String, val headers: Http.Headers) {
        val path: String
        val queryString: String
        val getParams: QueryString
        val absoluteURI: String
    }

    class Response {
        val headers = arrayListOf<Pair<String, String>>()
        fun header(key: String, value: String)
    }

    data class Headers(val items: List<Pair<String, String>>) : Iterable<Pair<String, String>> {
        constructor(vararg items: Pair<String, String>)
        constructor(map: Map<String, String>)
        constructor(str: String?)

        override fun iterator(): Iterator<Pair<String, String>>

        operator fun get(key: String): String?
        fun getAll(key: String): List<String>
        fun getFirst(key: String): String?

        fun toListGrouped(): List<Pair<String, List<String>>>

        fun withAppendedHeaders(newHeaders: List<Pair<String, String>>): Headers

        fun withReplaceHeaders(newHeaders: List<Pair<String, String>>): Headers
        fun withAppendedHeaders(vararg newHeaders: Pair<String, String>): Headers
        fun withReplaceHeaders(vararg newHeaders: Pair<String, String>): Headers
        fun containsAll(other: Http.Headers): Boolean

        operator fun plus(that: Headers): Headers

        companion object {
            fun fromListMap(map: Map<String?, List<String>>): Headers
            fun parse(str: String?): Headers
            val ContentLength = "Content-Length"
            val ContentType = "Content-Type"
        }
    }

    data class RedirectException(val code: Int = 307, val redirectUri: String) : Http.HttpException(code, HttpStatusMessage(code))
}

Client

fun createHttpClient() = defaultHttpFactory.createClient()

abstract class HttpClient protected constructor() {
    var ignoreSslCertificates: Boolean = false

    data class Response(
        val status: Int,
        val statusText: String,
        val headers: Http.Headers,
        val content: AsyncInputStream
    ) {
        val success: Boolean = status < 400
        suspend fun readAllBytes(): ByteArray
        val responseCharset: Charset
        suspend fun readAllString(charset: Charset = responseCharset): String
        suspend fun checkErrors(): Response

        fun withStringResponse(str: String, charset: Charset = UTF8): Response
        fun <T> toCompletedResponse(content: T): CompletedResponse
    }

    data class CompletedResponse<T>(
        val status: Int,
        val statusText: String,
        val headers: Http.Headers,
        val content: T
    ) {
        val success = status < 400
    }

    data class RequestConfig(
        val followRedirects: Boolean = true,
        val throwErrors: Boolean = false,
        val maxRedirects: Int = 10,
        val referer: String? = null,
        val simulateBrowser: Boolean = false
    )

    suspend fun request(
        method: Http.Method,
        url: String,
        headers: Http.Headers = Http.Headers(),
        content: AsyncStream? = null,
        config: RequestConfig = RequestConfig()
    ): Response

    suspend fun requestAsString(
        method: Http.Method,
        url: String,
        headers: Http.Headers = Http.Headers(),
        content: AsyncStream? = null,
        config: RequestConfig = RequestConfig()
    ): CompletedResponse<String>

    suspend fun requestAsBytes(
        method: Http.Method,
        url: String,
        headers: Http.Headers = Http.Headers(),
        content: AsyncStream? = null,
        config: RequestConfig = RequestConfig()
    ): CompletedResponse<ByteArray>

    suspend fun readBytes(url: String, config: RequestConfig = RequestConfig()): ByteArray
    suspend fun readString(url: String, config: RequestConfig = RequestConfig()): String
    suspend fun readJson(url: String, config: RequestConfig = RequestConfig()): Any?
}

// Delayed
fun HttpClient.delayed(ms: Long) = DelayedHttpClient(ms, this)
open class DelayedHttpClient(val delayMs: Long, val parent: HttpClient) : HttpClient()

class FakeHttpClient(val redirect: HttpClient? = null) : HttpClient() {
    val log = arrayListOf<String>()
    fun getAndClearLog(): List<String>

    var defaultResponse =
        HttpClient.Response(200, "OK", Http.Headers(), "LogHttpClient.response".toByteArray(UTF8).openAsync())

    class ResponseBuilder {
        private var responseCode = 200
        private var responseContent = "LogHttpClient.response".toByteArray(UTF8)
        private var responseHeaders = Http.Headers()

        fun response(content: String, code: Int = 200, charset: Charset = UTF8)
        fun response(content: ByteArray, code: Int = 200)
        fun redirect(url: String, code: Int = 302): Unit

        fun ok(content: String)
        fun notFound(content: String = "404 - Not Found")
        fun internalServerError(content: String = "500 - Internal Server Error")
    }

    data class Rule(
        val method: Http.Method?,
        val url: String? = null,
        val headers: Http.Headers? = null
    ) {
        fun matches(method: Http.Method, url: String, headers: Http.Headers, content: ByteArray?): Boolean
    }

    fun onRequest(
        method: Http.Method? = null,
        url: String? = null,
        headers: Http.Headers? = null
    ): ResponseBuilder
}

fun LogHttpClient() = FakeHttpClient()

object HttpStatusMessage {
    val CODES: Map<Int, String>
    operator fun invoke(code: Int): String
}

object HttpStats {
    val connections: AtomicLong
    val disconnections: AtomicLong
    override fun toString(): String
}

interface HttpFactory {
    fun createClient(): HttpClient
    fun createServer(): HttpServer
}

class ProxiedHttpFactory(var parent: HttpFactory) : HttpFactory by parent

fun setDefaultHttpFactory(factory: HttpFactory)
fun httpError(code: Int, msg: String): Nothing

Endpoint and Rest

fun HttpFactory.createClientEndpoint(endpoint: String)
fun createHttpClientEndpoint(endpoint: String) = createHttpClient().endpoint(endpoint)

interface HttpClientEndpoint {
    suspend fun request(
        method: Http.Method,
        path: String,
        headers: Http.Headers = Http.Headers(),
        content: AsyncStream? = null,
        config: HttpClient.RequestConfig = HttpClient.RequestConfig()
    ): HttpClient.Response
}

internal data class Request(
    val method: Http.Method,
    val path: String,
    val headers: Http.Headers,
    val content: AsyncStream?
) {
    suspend fun format(format: String = "{METHOD}:{PATH}:{CONTENT}"): String
}

class FakeHttpClientEndpoint(val defaultMessage: String = "{}") : HttpClientEndpoint {
	private val log: ArrayList<Request>
	private var responsePointer = 0
	private val responses: ArrayList<HttpClient.Response>()

	fun addResponse(code: Int, content: String)
	fun addOkResponse(content: String)
	fun addNotFoundResponse(content: String)
	override suspend fun request(
		method: Http.Method,
		path: String,
		headers: Http.Headers,
		content: AsyncStream?,
		config: HttpClient.RequestConfig
	): HttpClient.Response

	suspend fun capture(format: String = "{METHOD}:{PATH}:{CONTENT}", callback: suspend () -> Unit): List<String>
}

fun HttpClient.endpoint(endpoint: String): HttpClientEndpoint
fun HttpClientEndpoint.rest(): HttpRestClient
fun HttpClient.rest(endpoint: String): HttpRestClient
fun HttpFactory.createRestClient(endpoint: String, mapper: ObjectMapper): HttpRestClient

class HttpRestClient(val endpoint: HttpClientEndpoint) {
    suspend fun request(method: Http.Method, path: String, request: Any?, mapper: ObjectMapper = Mapper): Any

    suspend fun head(path: String): Any
    suspend fun delete(path: String): Any
    suspend fun get(path: String): Any
    suspend fun put(path: String, request: Any): Any
    suspend fun post(path: String, request: Any): Any
}

Server (HTTP and WebSockets)

fun createHttpServer() = defaultHttpFactory.createServer()

open class HttpServer protected constructor() : AsyncCloseable {
    companion object {
        operator fun invoke() = defaultHttpFactory.createServer()
    }

    abstract class BaseRequest(
        val uri: String,
        val headers: Http.Headers
    ) : Extra by Extra.Mixin() {
        private val parts by lazy { uri.split('?', limit = 2) }
        val path: String by lazy { parts[0] }
        val queryString: String by lazy { parts.getOrElse(1) { "" } }
        val getParams by lazy { QueryString.decode(queryString) }
        val absoluteURI: String by lazy { uri }
    }

    abstract class WsRequest(
        uri: String,
        headers: Http.Headers,
        val scope: CoroutineScope
    ) : BaseRequest(uri, headers) {
        abstract fun reject()

        abstract fun close()
        abstract fun onStringMessage(handler: suspend (String) -> Unit)
        abstract fun onBinaryMessage(handler: suspend (ByteArray) -> Unit)
        abstract fun onClose(handler: suspend () -> Unit)
        abstract fun send(msg: String)
        abstract fun send(msg: ByteArray)

        fun sendSafe(msg: String)
        fun sendSafe(msg: ByteArray)

        fun stringMessageStream(): ReceiveChannel<String>
        fun binaryMessageStream(): ReceiveChannel<ByteArray>
        fun anyMessageStream(): ReceiveChannel<Any>
    }

    val requestConfig = RequestConfig()

    data class RequestConfig(
        val beforeSendHeadersInterceptors: MutableMap<String, suspend (Request) -> Unit> = LinkedHashMap()
    ) : Extra by Extra.Mixin() {
        // TODO:
        fun registerComponent(component: Any, dependsOn: List<Any>): Unit = TODO()
    }

    abstract class Request constructor(
        val method: Http.Method,
        uri: String,
        headers: Http.Headers,
        val requestConfig: RequestConfig = RequestConfig()
    ) : BaseRequest(uri, headers), AsyncOutputStream {
        val finalizers = arrayListOf<suspend () -> Unit>()

        fun getHeader(key: String): String?
        fun getHeaderList(key: String): List<String>
        fun removeHeader(key: String)
        fun addHeader(key: String, value: String)
        fun replaceHeader(key: String, value: String)

        protected abstract suspend fun _handler(handler: (ByteArray) -> Unit)
        protected abstract suspend fun _endHandler(handler: () -> Unit)
        protected abstract suspend fun _sendHeader(code: Int, message: String, headers: Http.Headers)
        protected abstract suspend fun _write(data: ByteArray, offset: Int = 0, size: Int = data.size - offset)
        protected abstract suspend fun _end()

        suspend fun handler(handler: (ByteArray) -> Unit)
        suspend fun endHandler(handler: () -> Unit)

        suspend fun readRawBody(maxSize: Int = 0x1000): ByteArray
        fun setStatus(code: Int, message: String = HttpStatusMessage(code))

        override suspend fun write(buffer: ByteArray, offset: Int, len: Int)
        suspend fun end()
        suspend fun end(data: ByteArray)
        suspend fun write(data: String, charset: Charset = UTF8)
        suspend fun end(data: String, charset: Charset = UTF8)
        override suspend fun close()
    }

    suspend fun allHandler(handler: suspend (BaseRequest) -> Unit)
    open val actualPort: Int = 0

    suspend fun websocketHandler(handler: suspend (WsRequest) -> Unit): HttpServer
    suspend fun httpHandler(handler: suspend (Request) -> Unit): HttpServer
    suspend fun listen(port: Int = 0, host: String = "127.0.0.1"): HttpServer
    suspend fun listen(port: Int = 0, host: String = "127.0.0.1", handler: suspend (Request) -> Unit): HttpServer
    final override suspend fun close()
}

class FakeRequest(
    method: Http.Method,
    uri: String,
    headers: Http.Headers = Http.Headers(),
    val body: ByteArray = EMPTY_BYTE_ARRAY,
    requestConfig: HttpServer.RequestConfig
) : HttpServer.Request(method, uri, headers, requestConfig) {
    var outputHeaders: Http.Headers = Http.Headers()
    var outputStatusCode: Int = 0
    var outputStatusMessage: String = ""
    var output: String = ""
    val log = arrayListOf<String>()
}

WebSocket Client

suspend fun WebSocketClient(
    url: String,
    protocols: List<String>? = null,
    origin: String? = null,
    wskey: String? = "wskey",
    debug: Boolean = false
): WebSocketClient

abstract class WebSocketClient {
    val url: String
    val protocols: List<String>?

    val onOpen: Signal<Unit>
    val onError: Signal<Throwable>
    val onClose: Signal<Unit>

    val onBinaryMessage: Signal<ByteArray>
    val onStringMessage: Signal<String>
    val onAnyMessage: Signal<Any>

    open fun close(code: Int = 0, reason: String = ""): Unit
    open suspend fun send(message: String): Unit
    open suspend fun send(message: ByteArray): Unit
}

suspend fun WebSocketClient.readString(): String
suspend fun WebSocketClient.readBinary(): ByteArray

class WebSocketException(message: String) : IOException(message)
Was this article useful?