This commit is contained in:
2025-05-27 19:14:58 +03:00
parent 71491f8793
commit 0f7167210c
17 changed files with 969 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
import com.bmuschko.gradle.docker.tasks.AbstractDockerRemoteApiTask
import com.bmuschko.gradle.docker.tasks.image.DockerBuildImage
import com.bmuschko.gradle.docker.tasks.image.DockerPushImage
plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.kotlinx.serialization)
alias(libs.plugins.johnrengelman.shadow)
alias(libs.plugins.bmuschko.docker)
}
kotlin {
jvm()
linuxX64 {
binaries {
executable {
entryPoint = "pw.binom.main"
}
}
}
mingwX64 {
binaries {
executable {
entryPoint = "pw.binom.main"
}
}
}
sourceSets {
commonMain.dependencies {
implementation(libs.binom.io.strong.properties.ini)
implementation(libs.binom.io.strong.properties.yaml)
implementation(libs.binom.io.strong.application)
implementation(libs.binom.io.sqlite)
implementation(libs.binom.io.http.server.core)
implementation(libs.binom.io.http.client)
}
commonTest.dependencies {
api(kotlin("test-common"))
api(kotlin("test-annotations-common"))
}
jvmTest.dependencies {
api(kotlin("test-junit"))
}
}
}
val dockerImage = System.getenv("DOCKER_IMAGE_NAME")
val dockerLogin = System.getenv("DOCKER_REGISTRY_USERNAME")
val dockerPassword = System.getenv("DOCKER_REGISTRY_PASSWORD")
val dockerHost = System.getenv("DOCKER_REGISTRY_HOST")
docker {
registryCredentials {
url.set(dockerHost)
if (dockerLogin != null) {
username.set(dockerLogin)
}
if (dockerPassword != null) {
password.set(dockerPassword)
}
}
}
val jvmJar by tasks.getting(Jar::class)
val shadowJar by tasks.register("shadowJar", com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar::class) {
from(jvmJar.archiveFile)
group = "build"
configurations = listOf(project.configurations["jvmRuntimeClasspath"])
exclude(
"META-INF/*.SF",
"META-INF/*.DSA",
"META-INF/*.RSA",
"META-INF/*.txt",
"META-INF/NOTICE",
"LICENSE",
)
manifest {
attributes("Main-Class" to "pw.binom.JvmMain")
}
archiveFileName.set("full-application.jar")
}
val buildImage by tasks.register("buildDockerImage", DockerBuildImage::class) {
group = "docker"
dependsOn(shadowJar)
inputDir.set(projectDir)
if (dockerImage != null) {
images.add(dockerImage)
buildArgs.put("--output", "type=oci,name=$dockerImage")
}
applyUrl()
doFirst {
dockerImage ?: throw IllegalArgumentException("DOCKER_IMAGE_NAME is not set")
println("Image name: \"$dockerImage\"")
}
}
tasks.register("pushDockerImage", DockerPushImage::class) {
group = "docker"
applyUrl()
dependsOn(buildImage)
images.addAll(buildImage.images)
// buildImage.images
}
fun AbstractDockerRemoteApiTask.applyUrl() {
val host = System.getenv("DOCKER_HOST")
if (host != null) {
url.set(host)
}
}
@@ -0,0 +1,15 @@
package pw.binom
import pw.binom.io.http.Headers
interface DataLogger {
suspend fun save(
method: String,
request: String,
incomeHeader: Headers,
incomeData: ByteArray,
responseCode: Int,
outcomeHeader: Headers,
outcomeData: ByteArray,
)
}
@@ -0,0 +1,13 @@
@file:JvmName("JvmMain")
package pw.binom
import pw.binom.config.DefaultConfig
import pw.binom.strong.StrongApplication
import kotlin.jvm.JvmName
fun main(args: Array<String>) {
StrongApplication.run(args) {
+DefaultConfig(properties, networkManager)
}
}
@@ -0,0 +1,28 @@
package pw.binom.config
import pw.binom.http.client.HttpClientRunnable
import pw.binom.http.client.factory.Https11ConnectionFactory
import pw.binom.http.client.factory.NativeNetChannelFactory
import pw.binom.network.NetworkManager
import pw.binom.services.ConsoleDataLogger
import pw.binom.services.HttpServer
import pw.binom.services.SQLiteDataLogger
import pw.binom.services.SpyHandler
import pw.binom.strong.Strong
import pw.binom.strong.bean
import pw.binom.strong.beanAsyncCloseable
import pw.binom.strong.properties.StrongProperties
fun DefaultConfig(config: StrongProperties, networkManager: NetworkManager) = Strong.config {
it.beanAsyncCloseable {
HttpClientRunnable(
idleCoroutineContext = networkManager,
factory = Https11ConnectionFactory(),
source = NativeNetChannelFactory(networkManager)
)
}
it.bean { HttpServer() }
it.bean { SpyHandler() }
it.bean { ConsoleDataLogger() }
it.bean { SQLiteDataLogger() }
}
@@ -0,0 +1,12 @@
package pw.binom.properties
import kotlinx.serialization.Serializable
import pw.binom.properties.serialization.annotations.PropertiesPrefix
@PropertiesPrefix("app")
@Serializable
data class ApplicationProperties(
val port: Int,
val forward: String,
val database: String,
)
@@ -0,0 +1,35 @@
package pw.binom.services
import pw.binom.DataLogger
import pw.binom.io.http.Headers
import pw.binom.io.http.forEachHeader
import pw.binom.logger.Logger
import pw.binom.logger.info
class ConsoleDataLogger : DataLogger {
private val logger by Logger.ofThisOrGlobal
override suspend fun save(
method: String,
request: String,
incomeHeader: Headers,
incomeData: ByteArray,
responseCode: Int,
outcomeHeader: Headers,
outcomeData: ByteArray,
) {
logger.info("Income: $method $request")
logger.info("Request headers:")
incomeHeader.forEachHeader { key, value ->
logger.info(" $key: $value")
}
logger.info("Request data [${incomeData.size} bytes]:")
logger.info(incomeData.decodeToString())
logger.info("Response code: $responseCode")
logger.info("Response headers:")
outcomeHeader.forEachHeader { key, value ->
logger.info(" $key: $value")
}
logger.info("Request data [${outcomeData.size} bytes]:")
logger.info(outcomeData.decodeToString())
}
}
@@ -0,0 +1,37 @@
package pw.binom.services
import kotlinx.coroutines.cancelAndJoin
import pw.binom.DEFAULT_BUFFER_SIZE
import pw.binom.io.ByteBufferFactory
import pw.binom.io.httpServer.HttpHandler
import pw.binom.io.httpServer.HttpServer2
import pw.binom.io.httpServer.ListenJob
import pw.binom.io.socket.InetSocketAddress
import pw.binom.network.NetworkManager
import pw.binom.pool.GenericObjectPool
import pw.binom.properties.ApplicationProperties
import pw.binom.strong.BeanLifeCycle
import pw.binom.strong.inject
import pw.binom.strong.properties.injectProperty
class HttpServer {
private val httpHandler: HttpHandler by inject()
private val networkManager: NetworkManager by inject()
private val applicationProperties: ApplicationProperties by injectProperty()
private val bufferPool by lazy { GenericObjectPool(ByteBufferFactory(DEFAULT_BUFFER_SIZE)) }
private var server: HttpServer2? = null
private var listenJob: ListenJob? = null
init {
BeanLifeCycle.postConstruct {
val server = HttpServer2(handler = httpHandler, dispatcher = networkManager, byteBufferPool = bufferPool)
this.server = server
listenJob = server.listen(InetSocketAddress.resolve("0.0.0.0", applicationProperties.port))
}
BeanLifeCycle.preDestroy {
listenJob?.cancelAndJoin()
server?.asyncCloseAnyway()
}
}
}
@@ -0,0 +1,75 @@
package pw.binom.services
import pw.binom.DataLogger
import pw.binom.db.async.AsyncPreparedStatement
import pw.binom.db.sqlite.AsyncConnectionAdapter
import pw.binom.db.sqlite.AsyncSQLiteConnector
import pw.binom.io.file.File
import pw.binom.io.http.Headers
import pw.binom.properties.ApplicationProperties
import pw.binom.strong.BeanLifeCycle
import pw.binom.strong.properties.injectProperty
class SQLiteDataLogger : DataLogger {
private val applicationProperties: ApplicationProperties by injectProperty()
private var connection: AsyncConnectionAdapter? = null
private var statement: AsyncPreparedStatement? = null
private fun Headers.makeString() =
asSequence().map { (key, list) ->
list.asSequence().map { "$key: $it" }
}.flatten().joinToString("\n")
override suspend fun save(
method: String,
request: String,
incomeHeader: Headers,
incomeData: ByteArray,
responseCode: Int,
outcomeHeader: Headers,
outcomeData: ByteArray,
) {
val statement = statement!!
statement.set(0, method)
statement.set(1, request)
statement.set(2, incomeHeader.makeString())
statement.set(3, incomeData)
statement.set(4, responseCode)
statement.set(5, outcomeHeader.makeString())
statement.set(6, outcomeData)
statement.executeUpdate()
}
init {
BeanLifeCycle.postConstruct {
val connection = AsyncSQLiteConnector.openFile(File(applicationProperties.database))
this.connection = connection
connection.executeUpdate(
"""
create table if not exists requests(
id integer primary key autoincrement,
method text not null,
request text not null,
income_header text not null,
income_data blob not null,
response_code int not null,
outcome_header text not null,
outcome_data blob not null
);
""".trimIndent()
)
statement = connection.prepareStatement(
"""
insert into requests (
method,request,income_header,income_data,
response_code,outcome_header,outcome_data
) values(?,?,?,?,?,?,?)
""".trimIndent()
)
}
BeanLifeCycle.preDestroy {
statement?.asyncCloseAnyway()
connection?.asyncCloseAnyway()
}
}
}
@@ -0,0 +1,74 @@
package pw.binom.services
import pw.binom.DataLogger
import pw.binom.asyncInput
import pw.binom.copyTo
import pw.binom.http.client.Http11ClientExchange
import pw.binom.http.client.HttpClientRunnable
import pw.binom.io.ByteArrayOutput
import pw.binom.io.http.headersOf
import pw.binom.io.httpServer.HttpHandler
import pw.binom.io.httpServer.HttpServerExchange
import pw.binom.io.useAsync
import pw.binom.logger.Logger
import pw.binom.logger.info
import pw.binom.properties.ApplicationProperties
import pw.binom.strong.inject
import pw.binom.strong.injectServiceList
import pw.binom.strong.properties.injectProperty
import pw.binom.url.toURL
class SpyHandler : HttpHandler {
private val applicationProperties: ApplicationProperties by injectProperty()
private val client: HttpClientRunnable by inject()
private val loggers by injectServiceList<DataLogger>()
override suspend fun handle(exchange: HttpServerExchange) {
val result = "${applicationProperties.forward}${exchange.requestURI}"
val request = client.request(
method = exchange.requestMethod,
url = result.toURL()
)
request.headers.clear()
request.headers.addAll(exchange.requestHeaders)
val income = ByteArrayOutput()
val outcome = ByteArrayOutput()
var responseCode = 0
var outcomeHeader = headersOf()
var outcomeData = byteArrayOf()
println("exchange.input.available->${exchange.input.available}")
exchange.input.useAsync { input ->
input.copyTo(income)
}
val incomeData = income.toByteArray()
val r = request.connect() as Http11ClientExchange
r.useAsync { response ->
response.getOutput().useAsync { output ->
outcome.writeFully(incomeData)
}
outcomeHeader = response.getResponseHeaders()
responseCode = response.getResponseCode()
exchange.startResponse(response.getResponseCode(), outcomeHeader)
response.getInput().useAsync { input ->
input.copyTo(outcome)
}
outcomeData = outcome.toByteArray()
exchange.output.useAsync { output ->
output.writeFully(outcomeData)
}
}
loggers.forEach { l ->
l.save(
method = exchange.requestMethod,
request = exchange.requestURI.toString(),
incomeHeader = exchange.requestHeaders,
incomeData = incomeData,
responseCode = responseCode,
outcomeHeader = outcomeHeader,
outcomeData = outcomeData,
)
}
}
}