KorIO Network

KorIO has utilities for handling network.

Table of contents:

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)