init projects

This commit is contained in:
2026-06-17 18:58:41 +03:00
parent 8543c4e241
commit 21a768edd5
505 changed files with 19998 additions and 93 deletions
+2
View File
@@ -0,0 +1,2 @@
/build
/bin
+28
View File
@@ -0,0 +1,28 @@
plugins {
alias(libs.plugins.convention.kmp)
alias(libs.plugins.convention.android.lib)
alias(libs.plugins.convention.serialization)
alias(libs.plugins.convention.koin)
alias(libs.plugins.convention.ktor.client)
}
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation(project(":libs:base"))
implementation(libs.ktor.client.cio)
implementation(libs.kotlinx.rpc.krpc.client)
// Server API
implementation(libs.kotlinx.rpc.krpc.server)
// Serialization module. Also, protobuf and cbor are provided
implementation(libs.kotlinx.rpc.krpc.serialization.json)
// Transport implementation for Ktor
implementation(libs.kotlinx.rpc.krpc.ktor.client)
implementation(libs.kotlinx.rpc.krpc.ktor.server)
}
}
}
android { namespace = "ru.shadowsparky.http" }
}
@@ -0,0 +1,26 @@
package ru.shadowsparky.http.data
import io.ktor.client.HttpClient
import io.ktor.client.engine.okhttp.OkHttp
import okhttp3.OkHttpClient
import java.util.concurrent.TimeUnit
actual class HttpClientFactory actual constructor(private val settings: HttpClientSettings) {
actual fun create(): HttpClient {
return HttpClient(OkHttp) {
settings.setup(this)
engine {
config {
retryOnConnectionFailure(true)
connectTimeout(15, TimeUnit.SECONDS)
readTimeout(15, TimeUnit.SECONDS)
writeTimeout(15, TimeUnit.SECONDS)
}
preconfigured = OkHttpClient.Builder()
.pingInterval(30, TimeUnit.SECONDS)
.build()
}
}
}
}
@@ -0,0 +1,69 @@
package ru.shadowsparky.http
import io.ktor.client.HttpClient
import io.ktor.client.plugins.auth.providers.BearerAuthProvider
import io.ktor.client.plugins.auth.providers.BearerTokens
import io.ktor.client.plugins.auth.providers.RefreshTokensParams
import kotlinx.coroutines.flow.firstOrNull
import org.koin.core.annotation.ComponentScan
import org.koin.core.annotation.Configuration
import org.koin.core.annotation.Module
import org.koin.core.annotation.Single
import ru.shadowsparky.http.data.HttpClientFactory
import ru.shadowsparky.http.data.HttpClientSettings
import ru.shadowsparky.http.data.RefreshTokenRunner
import ru.shadowsparky.http.domain.HttpException
import ru.shadowsparky.http.domain.LogoutAction
import ru.shadowsparky.http.domain.RefreshTokenAction
import ru.shadowsparky.http.domain.TokenInfo
import ru.shadowsparky.http.domain.TokenStorage
import ru.shadowsparky.koin
@Module
@Configuration
@ComponentScan
class HttpModule {
@Single
fun createHttpClient(settings: HttpClientSettings): HttpClient {
return HttpClientFactory(settings).create()
}
@Single
fun createBearerAuthProvider(
refreshTokenRunner: RefreshTokenRunner
): BearerAuthProvider {
val tokenStorage = koin.getOrNull<TokenStorage>()
return BearerAuthProvider(
refreshTokens = {
refreshTokenRunner.runOrJoin {
refreshTokenInternal(tokenStorage)
}
},
loadTokens = {
tokenStorage?.token?.firstOrNull()?.toKtor()
},
realm = tokenStorage?.realm
)
}
private suspend fun RefreshTokensParams.refreshTokenInternal(tokenStorage: TokenStorage?): BearerTokens? {
val token = tokenStorage?.token?.firstOrNull()?.refresh ?: return null
return try {
val newToken = koin.getOrNull<RefreshTokenAction>()
?.refresh(this.client, token)
?: return null
tokenStorage.updateToken(newToken)
newToken.toKtor()
} catch (e: Throwable) {
if (e is HttpException) {
koin.getOrNull<LogoutAction>()?.logout()
}
null
}
}
}
private fun TokenInfo.toKtor(): BearerTokens {
return BearerTokens(token, refresh)
}
@@ -0,0 +1,7 @@
package ru.shadowsparky.http.data
import io.ktor.client.HttpClient
expect class HttpClientFactory(settings: HttpClientSettings) {
fun create(): HttpClient
}
@@ -0,0 +1,111 @@
package ru.shadowsparky.http.data
import io.ktor.client.HttpClientConfig
import io.ktor.client.call.body
import io.ktor.client.plugins.HttpResponseValidator
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.HttpTimeoutConfig
import io.ktor.client.plugins.auth.Auth
import io.ktor.client.plugins.auth.providers.BearerAuthProvider
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.client.request.header
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonPrimitive
import org.koin.core.annotation.Factory
import ru.shadowsparky.domain.AppConfig
import ru.shadowsparky.domain.Log
import ru.shadowsparky.http.domain.ClientKtorExtension
import ru.shadowsparky.http.domain.HttpException
import ru.shadowsparky.http.domain.UserAgentProvider
import ru.shadowsparky.koin
const val USE_HTTP_CUSTOM_CERTS = "use_http_custom_certs"
private const val MESSAGE_LONG = "message"
private const val MESSAGE = "msg"
@Factory
class HttpClientSettings(
private val json: Json,
private val userAgentProvider: UserAgentProvider?,
private val bearerAuthProvider: BearerAuthProvider,
private val appenderList: List<ClientKtorExtension>,
private val log: Log
) {
fun setup(
clientConfig: HttpClientConfig<*>
) = with(clientConfig) {
install(Auth) {
providers.add(bearerAuthProvider)
}
expectSuccess = false
HttpResponseValidator {
validateResponse {
val code = it.status.value
if (code in 400..599) {
val body = it.body<JsonElement>()
if (body is JsonObject) {
val msg = when {
body.containsKey(MESSAGE_LONG) -> body[MESSAGE_LONG]?.jsonPrimitive?.content
body.containsKey(MESSAGE) -> body[MESSAGE]?.jsonPrimitive?.content
else -> body.toString()
}
throw HttpException(code, msg)
} else {
throw HttpException(code, body.toString())
}
}
}
}
install(Logging) {
val isDebug = koin.getOrNull<AppConfig>()?.isDebug() ?: true
if (isDebug) {
this.level = LogLevel.ALL
this.logger = object : Logger {
override fun log(message: String) {
log.d("Http", message)
}
}
}
}
install(ContentNegotiation) {
json(json)
}
install(WebSockets) {
// как часто слать ping кадры
pingIntervalMillis = 30_000 // или: pingInterval = 20.seconds (в новых API)
}
defaultRequest {
header(HttpHeaders.Accept, ContentType.Application.Json)
header(HttpHeaders.ContentType, ContentType.Application.Json)
userAgentProvider?.provide()?.let {
header(HttpHeaders.UserAgent, it)
}
}
// Таймауты на уровне Ktor
install(HttpTimeout) {
requestTimeoutMillis = HttpTimeoutConfig.INFINITE_TIMEOUT_MS
socketTimeoutMillis = HttpTimeoutConfig.INFINITE_TIMEOUT_MS
connectTimeoutMillis = 20_000
}
appenderList.forEach { it.append(this) }
}
}
@@ -0,0 +1,39 @@
package ru.shadowsparky.http.data
import io.ktor.client.plugins.auth.providers.BearerTokens
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.koin.core.annotation.Factory
import ru.shadowsparky.domain.DispatcherProvider
@Factory
class RefreshTokenRunner(private val dispatcherProvider: DispatcherProvider) {
private val scope = CoroutineScope(SupervisorJob() + dispatcherProvider.io)
private val lock = Mutex()
private var inFlight: Deferred<BearerTokens?>? = null
suspend fun runOrJoin(block: suspend () -> BearerTokens?): BearerTokens? {
val deferred = lock.withLock {
inFlight?.takeIf { it.isActive } ?: startNew(block).also { inFlight = it }
}
return deferred.await()
}
private fun startNew(block: suspend () -> BearerTokens?): Deferred<BearerTokens?> {
val d = scope.async(dispatcherProvider.io) { block() }
d.invokeOnCompletion {
scope.launch {
lock.withLock {
if (inFlight === d) inFlight = null
}
}
}
return d
}
}
@@ -0,0 +1,7 @@
package ru.shadowsparky.http.domain
import io.ktor.client.HttpClientConfig
interface ClientKtorExtension {
fun append(clientConfig: HttpClientConfig<*>)
}
@@ -0,0 +1,8 @@
package ru.shadowsparky.http.domain
import io.ktor.http.HttpStatusCode
open class HttpException(val httpCode: Int, override val message: String?) : RuntimeException()
class BadRequestException(msg: String) : HttpException(HttpStatusCode.BadRequest.value, msg)
open class NotFoundException(msg: String) : HttpException(HttpStatusCode.NotFound.value, msg)
@@ -0,0 +1,5 @@
package ru.shadowsparky.http.domain
interface LogoutAction {
suspend fun logout()
}
@@ -0,0 +1,7 @@
package ru.shadowsparky.http.domain
import io.ktor.client.HttpClient
interface RefreshTokenAction {
suspend fun refresh(client: HttpClient, refreshToken: String): TokenInfo
}
@@ -0,0 +1,9 @@
package ru.shadowsparky.http.domain
import kotlinx.serialization.Serializable
@Serializable
data class TokenInfo(
val token: String,
val refresh: String? = null
)
@@ -0,0 +1,16 @@
package ru.shadowsparky.http.domain
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
interface TokenStorage {
val realm: String
val token: Flow<TokenInfo?>
suspend fun updateToken(newToken: TokenInfo)
suspend fun clear()
}
suspend fun TokenStorage.hasAccount(): Boolean {
return token.firstOrNull()?.token != null
}
@@ -0,0 +1,5 @@
package ru.shadowsparky.http.domain
interface UserAgentProvider {
fun provide(): String?
}
@@ -0,0 +1,23 @@
package ru.shadowsparky.http.data
import io.ktor.client.HttpClient
import io.ktor.client.engine.okhttp.OkHttp
import okhttp3.OkHttpClient
import java.util.concurrent.TimeUnit
actual class HttpClientFactory actual constructor(private val settings: HttpClientSettings) {
actual fun create(): HttpClient {
return HttpClient(OkHttp) {
settings.setup(this)
engine {
config {
retryOnConnectionFailure(true)
connectTimeout(15, TimeUnit.SECONDS)
readTimeout(15, TimeUnit.SECONDS)
writeTimeout(15, TimeUnit.SECONDS)
}
preconfigured = OkHttpClient.Builder().pingInterval(30, TimeUnit.SECONDS).build()
}
}
}
}
@@ -0,0 +1,12 @@
package ru.shadowsparky.http.data
import io.ktor.client.HttpClient
import io.ktor.client.engine.js.Js
actual class HttpClientFactory actual constructor(private val settings: HttpClientSettings) {
actual fun create(): HttpClient {
return HttpClient(Js) {
settings.setup(this)
}
}
}