All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.exactpro.th2.httpserver.server.Th2HttpServer.kt Maven / Gradle / Ivy

/*
 * Copyright 2020-2021 Exactpro (Exactpro Systems Limited)
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package com.exactpro.th2.httpserver.server

import com.exactpro.th2.common.event.Event
import com.exactpro.th2.httpserver.server.options.ServerOptions
import com.exactpro.th2.httpserver.server.responses.Th2Response
import mu.KotlinLogging
import rawhttp.core.HttpVersion
import rawhttp.core.RawHttp
import rawhttp.core.RawHttpRequest
import rawhttp.core.RawHttpResponse
import rawhttp.core.body.BodyReader
import rawhttp.core.errors.InvalidHttpRequest
import java.io.IOException
import java.net.InetSocketAddress
import java.net.ServerSocket
import java.net.Socket
import java.net.SocketException
import java.util.UUID
import java.util.Optional
import java.util.concurrent.ExecutorService
import java.util.concurrent.TimeUnit
import kotlin.Exception
import kotlin.NoSuchElementException


private val LOGGER = KotlinLogging.logger { }

internal class Th2HttpServer(
    private val eventStore: ((String, String, Throwable?) -> Event),
    private val options: ServerOptions,
    private val terminationTime: Long,
    socketDelayCheck: Long
) : HttpServer {

    @Volatile
    private var listen: Boolean = true
    private val dialogManager: DialogueManager = DialogueManager(socketDelayCheck)

    private var socket: ServerSocket = options.createSocket()
    private val executorService: ExecutorService = options.createExecutorService()
    private val additionalExecutors: ExecutorService = options.createExecutorService()
    private val http: RawHttp = options.getRawHttp()



    override fun start() {
        Thread({
            while (listen) {
                try {
                    val client = socket.accept()

                    // thread waiting to accept socket before continue
                    executorService.submit { handle(client) }
                } catch (e: SocketException) {
                    serverError("Broken or closed socket!", e)
                    recreateSocket()
                } catch (e: IOException) {
                    serverError("Failed to accept client socket", e)
                    if (socket.isClosed) recreateSocket()
                }
            }
        }, "th2-conn-http-server").start()
        dialogManager.startCleaner()
    }

    private fun recreateSocket() {
        if (!listen) return
        options.runCatching { socket = createSocket() }.onFailure { e -> LOGGER.error(e) { "Can't recreate socket!" } }
    }

    private fun handle(client: Socket) {
        var request: RawHttpRequest
        var serverWillCloseConnection = false
        while (!serverWillCloseConnection) {
            try {
                request = http.parseRequest(
                    client.getInputStream(),
                    (client.remoteSocketAddress as InetSocketAddress).address
                )

                val requestEagerly = request.eagerly().apply { LOGGER.debug("Received request: \n$this\n") }

                val uuid = UUID.randomUUID().toString()
                additionalExecutors.submit { options.onRequest(requestEagerly, uuid) }
                val connectionOption = request.headers.getFirst("Connection")
                serverWillCloseConnection =
                    connectionOption.map { string: String? -> "close".equals(string, ignoreCase = true) }.orElse(false)
                if (!serverWillCloseConnection) serverWillCloseConnection =
                    !keepAlive(request.startLine.httpVersion, connectionOption)


                if (!serverWillCloseConnection) {
                    dialogManager.dialogues[uuid] = Dialogue(requestEagerly, client)
                    LOGGER.debug("Stored dialog: $uuid")
                }

            } catch (e: Exception) {
                if (e !is SocketException && e is InvalidHttpRequest && e.lineNumber == 0) {
                    LOGGER.debug { "Client closed connection" }
                } else {
                    serverError("Failed to handle request, broken socket", e)
                }
                serverWillCloseConnection = true // cannot keep listening anymore
            } finally {
                if (serverWillCloseConnection) client.runCatching(Socket::close)
            }
        }
    }

    fun handleResponse(response: RawHttpResponse) {
        try {
            val uuid: String = response.libResponse.get().uuid
            LOGGER.debug { "Message processing for $uuid has been started " }
            dialogManager.dialogues.remove(uuid)?.let {
                options.prepareResponse(it.request, response).writeTo(it.socket.getOutputStream())
                LOGGER.debug("Response: \n$response\nwas send to client")
            } ?: run {
                serverError(
                    "Failed to handle response, no matching client found. Response: /n$response",
                    null
                )
            }
        } catch (e: SocketException) {
            serverError(
                "Failed to handle response, socket is broken. Response: /n$response",
                e
            )
        } catch (e: NoSuchElementException) {
            serverError(
                "Response is broken, please check api realization. Need to provide Th2Response object inside response",
                e
            )
        } finally {
            closeBodyOf(response)
        }
    }

    override fun stop() {
        LOGGER.debug("Server is shutting down")
        listen = false
        dialogManager.close()
        try {
            socket.close()
        } catch (e: IOException) {
            LOGGER.warn(e) { "Failed to close Server socket" }
        } finally {
            executorService.shutdown()
            additionalExecutors.shutdown()
            if (!executorService.awaitTermination(terminationTime, TimeUnit.SECONDS)) {
                LOGGER.warn { "Socket Executors didn't turn off on specified time" }
                executorService.shutdownNow()
            }
            if (!additionalExecutors.isTerminated) {
                LOGGER.warn { "Additional Executors didn't turn off on specified time" }
                additionalExecutors.shutdown()
            }
        }
    }

    private fun closeBodyOf(response: RawHttpResponse<*>) {
        response.body.ifPresent { b: BodyReader ->
            try {
                b.close()
            } catch (e: IOException) {
                LOGGER.warn(e) { "Body of message may be already closed" }
            }
        }
    }

    private fun keepAlive(httpVersion: HttpVersion, connectionOption: Optional): Boolean {
        // https://tools.ietf.org/html/rfc7230#section-6.3
        // If the received protocol is HTTP/1.1 (or later)
        // OR
        // If the received protocol is HTTP/1.0, the "keep-alive" connection
        // option is present
        // THEN the connection will persist
        // OTHERWISE close the connection
        return !httpVersion.isOlderThan(HttpVersion.HTTP_1_1) || httpVersion == HttpVersion.HTTP_1_0
                && connectionOption.map { option: String? -> "keep-alive".equals(option, ignoreCase = true) }
            .orElse(false)
    }

    private fun serverError(msg: String, throwable: Throwable?) {
        if (!listen) {
            LOGGER.warn(throwable) { "Closed server: $msg" }
        } else {
            eventStore(msg, "Error", throwable)
            LOGGER.error(throwable) { msg }
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy