diff --git a/build.gradle.kts b/build.gradle.kts index 215a78d..18986f0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,9 +1,13 @@ plugins { - kotlin("multiplatform") version "1.7.20" - kotlin("plugin.serialization") version "1.7.20" + kotlin("multiplatform") version "1.7.10" + kotlin("plugin.serialization") version "1.7.10" `maven-publish` } +val ktor_version="2.1.1" +val logback_version="1.2.10" + + group = "net.sergeych" version = "0.0.1-SNAPSHOT" @@ -51,7 +55,13 @@ kotlin { } } val jvmMain by getting - val jvmTest by getting + val jvmTest by getting { + dependencies { + implementation("io.ktor:ktor-server-core:$ktor_version") + implementation("io.ktor:ktor-server-netty:$ktor_version") + implementation("ch.qos.logback:logback-classic:$logback_version") + } + } val jsMain by getting val jsTest by getting } diff --git a/src/commonMain/kotlin/net.sergeych.superlogin/BackgroundKeyGenerator.kt b/src/commonMain/kotlin/net.sergeych.superlogin/BackgroundKeyGenerator.kt index f91383a..11dc088 100644 --- a/src/commonMain/kotlin/net.sergeych.superlogin/BackgroundKeyGenerator.kt +++ b/src/commonMain/kotlin/net.sergeych.superlogin/BackgroundKeyGenerator.kt @@ -12,7 +12,7 @@ object BackgroundKeyGenerator { const val DefaultStrength = 4096 - class EntropyLowException : Exception("entropy level is below requested") + class EntropyLowException(current: Int,requested: Int) : Exception("entropy level is below requested ($current/$requested)") private var keyStrength = DefaultStrength private var nextKey: Deferred? = null @@ -73,14 +73,17 @@ object BackgroundKeyGenerator { } fun randomBytes(length: Int,minEntropy: Int): ByteArray { + addEntropyTimestamp() entropyHash?.let { if (minEntropy <= entropy) { entropy -= minEntropy - addEntropyTimestamp() return randomBytes(length, it) } } - throw EntropyLowException() + if( minEntropy == 0 ) { + return randomBytes(length, null) + } + throw EntropyLowException(minEntropy, entropy) } fun randomBytes(length: Int, IV: ByteArray? = null): ByteArray { diff --git a/src/commonMain/kotlin/net.sergeych.superlogin/PasswordDerivationParams.kt b/src/commonMain/kotlin/net.sergeych.superlogin/PasswordDerivationParams.kt index 537c27a..6dbe9d4 100644 --- a/src/commonMain/kotlin/net.sergeych.superlogin/PasswordDerivationParams.kt +++ b/src/commonMain/kotlin/net.sergeych.superlogin/PasswordDerivationParams.kt @@ -11,7 +11,7 @@ import kotlin.random.Random * and a method to derive password accordingly. */ @Serializable -class PasswordDerivationParams( +data class PasswordDerivationParams( val rounds: Int = 15000, val algorithm: HashAlgorithm = HashAlgorithm.SHA3_256, val salt: ByteArray = Random.nextBytes(32), diff --git a/src/commonMain/kotlin/net.sergeych.superlogin/SuperloginClient.kt b/src/commonMain/kotlin/net.sergeych.superlogin/SuperloginClient.kt deleted file mode 100644 index cbdea3a..0000000 --- a/src/commonMain/kotlin/net.sergeych.superlogin/SuperloginClient.kt +++ /dev/null @@ -1,17 +0,0 @@ -package net.sergeych.superlogin - -import net.sergeych.parsec3.Adapter - - -class SuperloginClient(adapter: Adapter<*>) { - -// init { -// adapter.invokeCommand() -// } - - fun register() { - - } - - -} \ No newline at end of file diff --git a/src/commonMain/kotlin/net.sergeych.superlogin/api.kt b/src/commonMain/kotlin/net.sergeych.superlogin/api.kt index 55eca68..3fc3263 100644 --- a/src/commonMain/kotlin/net.sergeych.superlogin/api.kt +++ b/src/commonMain/kotlin/net.sergeych.superlogin/api.kt @@ -13,7 +13,8 @@ data class RegistrationArgs( val loginPublicKey: PublicKey, val derivationParams: PasswordDerivationParams, val restoreId: ByteArray, - val restoreData: ByteArray + val restoreData: ByteArray, + val extraData: ByteArray? = null ) @Serializable @@ -21,11 +22,15 @@ sealed class AuthenticationResult { @Serializable data class Success( val loginToken: ByteArray, + val applicationData: ByteArray? ): AuthenticationResult() @Serializable object LoginUnavailable: AuthenticationResult() + @Serializable + object LoginIdUnavailable: AuthenticationResult() + @Serializable object RestoreIdUnavailable: AuthenticationResult() } @@ -36,23 +41,19 @@ data class LoginArgs( val packedSignedRecord: ByteArray ) -@Serializable -data class LoginData( - val encryptedPrivateKey: ByteArray, - val loginNonce: ByteArray -) class SuperloginServerApi : CommandHost() { - val registerUser by command() - val loginUserByToken by command() + val slRegister by command() + val slLogout by command() + val slLoginByToken by command() - val requestUserLoginParams by command() + val slRequestDerivationParams by command() /** * Get resstoreData by restoreId: password reset procedure start. */ - val requestUserLogin by command() +// val requestUserLogin by command() // val performLogin by command(val loginData: SuperloginData) : LoginState(true) + + /** + * Login state whatever it was now is logged out, and client application should delete + * any saved [SuperloginData] instance it has. + */ + object LoggedOut : LoginState(false) +} \ No newline at end of file diff --git a/src/commonMain/kotlin/net.sergeych.superlogin/Registration.kt b/src/commonMain/kotlin/net.sergeych.superlogin/client/Registration.kt similarity index 51% rename from src/commonMain/kotlin/net.sergeych.superlogin/Registration.kt rename to src/commonMain/kotlin/net.sergeych.superlogin/client/Registration.kt index e5dd074..ca4ea0d 100644 --- a/src/commonMain/kotlin/net.sergeych.superlogin/Registration.kt +++ b/src/commonMain/kotlin/net.sergeych.superlogin/client/Registration.kt @@ -1,13 +1,19 @@ -package net.sergeych.superlogin +package net.sergeych.superlogin.client +import net.sergeych.boss_serialization.BossDecoder +import net.sergeych.boss_serialization_mp.BossEncoder import net.sergeych.mp_logger.LogTag +import net.sergeych.mp_logger.debug import net.sergeych.mp_logger.error import net.sergeych.mp_logger.exception import net.sergeych.parsec3.Adapter import net.sergeych.parsec3.WithAdapter +import net.sergeych.superlogin.* +import net.sergeych.superlogin.server.SuperloginRestoreAccessPayload import net.sergeych.unikrypto.HashAlgorithm import net.sergeych.unikrypto.SymmetricKey import net.sergeych.unikrypto.digest +import kotlin.random.Random /** * Registration instances are used to perform superlogin-compatible registration in a smart way @@ -22,75 +28,106 @@ import net.sergeych.unikrypto.digest * * See [register] for more. */ -class Registration(val adapter: Adapter,loginKeyStrength: Int = 4096): LogTag("SLREG") { +class Registration( + val adapter: Adapter<*>, loginKeyStrength: Int = 4096, + val pbkdfRounds: Int = 15000, +) : LogTag("SLREG") { sealed class Result { /** * Login is already in use or is somehow else invalid */ - object InvalidLogin: Result() + object InvalidLogin : Result() /** * Operation failed for nknown reason, usually it means * network or server downtime */ - class NetworkFailure(val exception: Throwable?=null): Result() + class NetworkFailure(val exception: Throwable? = null) : Result() - class Success(val secret: String,val dataKey: SymmetricKey,loginToken: ByteArray): Result() + class Success( + val secret: String, + val dataKey: SymmetricKey, + val loginToken: ByteArray, + val encodedData: ByteArray?) : Result() { + inline fun data() = encodedData?.let { BossDecoder.decodeFrom(it)} + } } private var lastPasswordHash: ByteArray? = null private var lastDerivationParams: PasswordDerivationParams? = null private var passwordKeys: DerivedKeys? = null - val api = SuperloginServerApi() + val api = SuperloginServerApi() private val deferredLoginKey = BackgroundKeyGenerator.getKeyAsync(loginKeyStrength) private val dataKey = BackgroundKeyGenerator.randomSymmetricKey() + inline suspend fun register( + login: String, + password: String, + derivationParams: PasswordDerivationParams = PasswordDerivationParams(rounds = pbkdfRounds), + extraData: T? = null, + ): Result = registerWithData(login, password, extraData?.let { BossEncoder.encode(it) }) + + /** * Smart attempt to register. It is ok to repeatedly call it if the result is not [Result.Success]: it will * cache internal data and reuse time-consuming precalculated values for caches and derived keys if the * password and derivarion parameters are not changed between calls. */ - suspend fun register( + suspend fun registerWithData( login: String, password: String, - derivationParams: PasswordDerivationParams = PasswordDerivationParams() + extraData: ByteArray? = null, ): Result { val newPasswordHash = HashAlgorithm.SHA3_256.digest(password) - if( lastPasswordHash?.contentEquals(newPasswordHash) != true || - lastDerivationParams?.equals(derivationParams) != true || - passwordKeys == null ) { - passwordKeys = DerivedKeys.derive(password,derivationParams) - lastDerivationParams = derivationParams + if (lastPasswordHash?.contentEquals(newPasswordHash) != true || + passwordKeys == null + ) { + lastDerivationParams = PasswordDerivationParams(pbkdfRounds) + passwordKeys = DerivedKeys.derive(password, lastDerivationParams!! ) lastPasswordHash = newPasswordHash } - val spl = SuperloginPayload(login, deferredLoginKey.await(), dataKey) + val loginPrivateKey = deferredLoginKey.await() + val spl = SuperloginRestoreAccessPayload(login, loginPrivateKey, dataKey) repeat(10) { - val (restoreKey, restoreData) = AccessControlObject.pack(passwordKeys!!.loginAccessKey,spl) + val (restoreKey, restoreData) = AccessControlObject.pack(passwordKeys!!.loginAccessKey, spl) try { - val result = adapter.invokeCommand( - api.registerUser, RegistrationArgs( + val result = adapter.invokeCommand( + api.slRegister, RegistrationArgs( login, passwordKeys!!.loginId, deferredLoginKey.await().publicKey, - derivationParams, - restoreKey.restoreId, restoreData + lastDerivationParams!!, + restoreKey.restoreId, restoreData, + extraData ) ) when (result) { AuthenticationResult.RestoreIdUnavailable -> { // rare situation but still possible: just repack the ACO + debug { "retrying registration on restoreId clash" } + } + + AuthenticationResult.LoginIdUnavailable -> { + // rare situation: loginId is already in use. We have to re-derive password keys + // using new random salt: + debug { "retrying registration on loginId clash"} + lastDerivationParams = lastDerivationParams!!.copy(salt = Random.nextBytes(32)) + passwordKeys = DerivedKeys.derive(password, lastDerivationParams!!) } AuthenticationResult.LoginUnavailable -> return Result.InvalidLogin - is AuthenticationResult.Success -> return Result.Success( - restoreKey.secret, - dataKey, - result.loginToken - ) + + is AuthenticationResult.Success -> { + return Result.Success( + restoreKey.secret, + dataKey, + result.loginToken, + result.applicationData + ) + } } - } - catch(x: Throwable) { + } catch (x: Throwable) { exception { "Failed to register" to x } } } diff --git a/src/commonMain/kotlin/net.sergeych.superlogin/client/SuperloginClient.kt b/src/commonMain/kotlin/net.sergeych.superlogin/client/SuperloginClient.kt new file mode 100644 index 0000000..43537be --- /dev/null +++ b/src/commonMain/kotlin/net.sergeych.superlogin/client/SuperloginClient.kt @@ -0,0 +1,213 @@ +package net.sergeych.superlogin.client + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.serialization.Serializable +import net.sergeych.boss_serialization.BossDecoder +import net.sergeych.boss_serialization_mp.BossEncoder +import net.sergeych.mp_logger.LogTag +import net.sergeych.mp_logger.Loggable +import net.sergeych.mp_logger.exception +import net.sergeych.mp_logger.warning +import net.sergeych.mp_tools.globalLaunch +import net.sergeych.parsec3.Adapter +import net.sergeych.parsec3.CommandDescriptor +import net.sergeych.parsec3.Parsec3Transport +import net.sergeych.parsec3.WithAdapter +import net.sergeych.superlogin.AuthenticationResult +import net.sergeych.superlogin.SuperloginServerApi +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +/** + * Application might want to serialize and store updated instances of it in some + * permanent storage to restore login data also in offline more, and re-login + * automatically where possible withot asking the password again. + * + * Superlogin client _does not store the data itself_ but only notifies client software + * that it is changed and ought to be updated. + */ +@Serializable +data class SuperloginData( + val loginToken: ByteArray? = null, + val data: T? = null, +) + + +class SuperloginClient( + private val transport: Parsec3Transport, + savedData: SuperloginData? = null, + private val dataType: KType, +) : Parsec3Transport, Loggable by LogTag("SLCLI") { + + private val _state = MutableStateFlow( + if (savedData == null) LoginState.LoggedOut + else LoginState.LoggedIn(savedData) + ) + val state: StateFlow = _state + + private val _cflow = MutableStateFlow(false) + override val connectedFlow: StateFlow = _cflow + + private var slData: SuperloginData? = savedData + set(value) { + if (field != value) { + field = value + if (value == null) { + // do actual disconnect work + _cflow.value = false + _state.value = LoginState.LoggedOut + if (!adapterReady.isActive) { + adapterReady.cancel() + adapterReady = CompletableDeferred() + } + globalLaunch { + transport.adapter().invokeCommand(api.slLogout) + adapterReady.complete(Unit) + } + } else { + val v = _state.value + if (v !is LoginState.LoggedIn<*> || v.loginData != value) { + _state.value = LoginState.LoggedIn(value) + if (!adapterReady.isCompleted) adapterReady.complete(Unit) + } + } + } + } + + val applicationData: D? + get() = (state.value as? LoginState.LoggedIn)?.loginData?.data + + private var adapterReady = CompletableDeferred() + + override suspend fun adapter(): Adapter { + do { + try { + adapterReady.await() + return transport.adapter() + } catch (x: Throwable) { + exception { "failed to get adapter" to x } + } + } while (true) + } + + suspend fun call(ca: CommandDescriptor, args: A ): R = adapter().invokeCommand(ca, args) + suspend fun call(ca: CommandDescriptor): R = adapter().invokeCommand(ca) + + private suspend fun invoke(ca: CommandDescriptor, args: A ): R = transport.adapter().invokeCommand(ca, args) + private suspend fun invoke(ca: CommandDescriptor): R = transport.adapter().invokeCommand(ca) + + private var jobs = listOf() + + private val api = SuperloginServerApi() + + private suspend fun tryRestoreLogin() { + slData?.loginToken?.let { token -> + try { + val ar = transport.adapter().invokeCommand(api.slLoginByToken, token) + slData = if (ar is AuthenticationResult.Success) { + val data: D? = ar.applicationData?.let { BossDecoder.decodeFrom(dataType, it) } + SuperloginData(ar.loginToken, data) + } else { + null + } + } catch (t: Throwable) { + exception { "failed to restore login by token, will retry" to t } + delay(1500) + tryRestoreLogin() + } + } ?: warning { "tryRestoreLogin is ignored as slData is now null" } + } + + init { + jobs += globalLaunch { + transport.connectedFlow.collect { on -> + if (on) tryRestoreLogin() + else { + _cflow.value = false + } + } + } + } + + + override fun close() { + transport.close() + } + + override fun reconnect() { + transport.reconnect() + if (!adapterReady.isActive) { + adapterReady.cancel() + adapterReady = CompletableDeferred() + } + } + + private var registration: Registration? = null + + val isLoggedIn get() = state.value.isLoggedIn + + /** + * Perform registration and login attempt and return the result. It automatically caches and reuses intermediate + * long-calculated keys so it runs much fatser when called again with the same password. + * + * _Client should be in logged-out state!_ Use [logout] as needed + * + * If result is [Registration.Result.Success], user state is _logged in_. + * + * @throws IllegalStateException if logged in + */ + suspend fun register( + loginName: String, password: String, data: D? = null, + loginKeyStrength: Int = 2048, pbkdfRounds: Int = 15000, + ): Registration.Result { + mustBeLoggedOut() + val rn = registration ?: Registration(transport.adapter(), loginKeyStrength, pbkdfRounds) + .also { registration = it } + return rn.registerWithData(loginName, password, extraData = BossEncoder.encode(dataType, data)) + .also { rr -> + if (rr is Registration.Result.Success) { + slData = SuperloginData(rr.loginToken, rr.encodedData?.let { BossDecoder.decodeFrom(dataType, it) }) + } + } + } + + private fun mustBeLoggedOut() { + if (isLoggedIn) + throw IllegalStateException("please log out first") + } + + private fun mustBeLoggedIn() { + if (!isLoggedIn) + throw IllegalStateException("please log in first") + } + + fun logout() { + mustBeLoggedIn() + slData = null + } + + suspend fun LoginByToken(token: ByteArray): SuperloginData { + mustBeLoggedOut() + val r = invoke(api.slLoginByToken,token) + when(r) { + AuthenticationResult.LoginIdUnavailable -> TODO() + AuthenticationResult.LoginUnavailable -> TODO() + AuthenticationResult.RestoreIdUnavailable -> TODO() + is AuthenticationResult.Success -> TODO() + } + } + + companion object { + inline operator fun invoke( + t: Parsec3Transport, + savedData: SuperloginData? = null, + ): SuperloginClient { + return SuperloginClient(t, savedData, typeOf()) + } + } + +} \ No newline at end of file diff --git a/src/commonMain/kotlin/net.sergeych.superlogin/server/SLServerTraits.kt b/src/commonMain/kotlin/net.sergeych.superlogin/server/SLServerTraits.kt new file mode 100644 index 0000000..8562aa4 --- /dev/null +++ b/src/commonMain/kotlin/net.sergeych.superlogin/server/SLServerTraits.kt @@ -0,0 +1,27 @@ +package net.sergeych.superlogin.server + +import net.sergeych.superlogin.AuthenticationResult +import net.sergeych.superlogin.RegistrationArgs + +/** + * Set of procedures the server implementing superlogin protocol should provide + */ +interface SLServerTraits { + /** + * Register specified user. The server should check that: + * + * - [RegistrationArgs.loginName] is not used, otherwise return [AuthenticationResult.LoginUnavailable] + * - [RegistrationArgs.loginId] is not used, otherwise return [AuthenticationResult.LoginIdUnavailable] + * - [RegistrationArgs.restoreId] is not used or return [AuthenticationResult.RestoreIdUnavailable] + * + * Then it should save permanently data from `registrationArgs` in a way tha allow fast search (indexed, + * usually) by `loginName`, `loginId` and `restoreId`, and return [AuthenticationResult.Success] with + * newly generated random bytes string `loginToken` that would optionally simplify logging in. + * + * If the implementation does not provide login token, it should still provide random bytes string + * to maintain hight security level of the serivce. + */ + suspend fun register(registrationArgs: RegistrationArgs): AuthenticationResult + + suspend fun logout() {} +} \ No newline at end of file diff --git a/src/commonMain/kotlin/net.sergeych.superlogin/SuperloginPayload.kt b/src/commonMain/kotlin/net.sergeych.superlogin/server/SuperloginRestoreAccessPayload.kt similarity index 86% rename from src/commonMain/kotlin/net.sergeych.superlogin/SuperloginPayload.kt rename to src/commonMain/kotlin/net.sergeych.superlogin/server/SuperloginRestoreAccessPayload.kt index 3eb3d86..cd0cd4e 100644 --- a/src/commonMain/kotlin/net.sergeych.superlogin/SuperloginPayload.kt +++ b/src/commonMain/kotlin/net.sergeych.superlogin/server/SuperloginRestoreAccessPayload.kt @@ -1,4 +1,4 @@ -package net.sergeych.superlogin +package net.sergeych.superlogin.server import kotlinx.serialization.Serializable import net.sergeych.unikrypto.PrivateKey @@ -13,7 +13,7 @@ import net.sergeych.unikrypto.SymmetricKey * - storage key (used to safely keep stored data) */ @Serializable -data class SuperloginPayload( +data class SuperloginRestoreAccessPayload( val login: String, val loginPrivateKey: PrivateKey, val dataStorageKey: SymmetricKey diff --git a/src/commonTest/kotlin/superlogin/assert_throws.kt b/src/commonTest/kotlin/superlogin/assert_throws.kt new file mode 100644 index 0000000..d2d5eee --- /dev/null +++ b/src/commonTest/kotlin/superlogin/assert_throws.kt @@ -0,0 +1,14 @@ +package superlogin + +import kotlin.test.fail + +suspend inline fun assertThrowsAsync(f: suspend () -> Unit) { + try { + f() + fail("Nothing was thrown while ${T::class.simpleName} is expected") + } + catch(x: Throwable) { + if( x !is T ) + fail("${x::class.simpleName} was thrown instead of ${T::class.simpleName}") + } +} diff --git a/src/jvmMain/kotlin/net/sergeych/superlogin/server/SLServerApiBase.kt b/src/jvmMain/kotlin/net/sergeych/superlogin/server/SLServerApiBase.kt new file mode 100644 index 0000000..7e81986 --- /dev/null +++ b/src/jvmMain/kotlin/net/sergeych/superlogin/server/SLServerApiBase.kt @@ -0,0 +1,8 @@ +package net.sergeych.superlogin.server + +import net.sergeych.parsec3.CommandHost + +/** + * Server-side API convenience base. + */ +open class SLServerApiBase: CommandHost>() \ No newline at end of file diff --git a/src/jvmMain/kotlin/net/sergeych/superlogin/server/SLServerSession.kt b/src/jvmMain/kotlin/net/sergeych/superlogin/server/SLServerSession.kt new file mode 100644 index 0000000..b1fd252 --- /dev/null +++ b/src/jvmMain/kotlin/net/sergeych/superlogin/server/SLServerSession.kt @@ -0,0 +1,18 @@ +package net.sergeych.superlogin.server + +import net.sergeych.parsec3.WithAdapter +import net.sergeych.superlogin.client.SuperloginData + +/** + * The superlogin server session. Please use only [loginName] and [loginToken], the rest could be + * a subject to change. + */ +open class SLServerSession: WithAdapter() { + var slData: SuperloginData? = null + + val userData: T? get() = slData?.let { it.data } + + val loginToken get() = slData?.loginToken + + var loginName: String? = null +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/net/sergeych/superlogin/server/SuperloginServer.kt b/src/jvmMain/kotlin/net/sergeych/superlogin/server/SuperloginServer.kt new file mode 100644 index 0000000..b1eda31 --- /dev/null +++ b/src/jvmMain/kotlin/net/sergeych/superlogin/server/SuperloginServer.kt @@ -0,0 +1,28 @@ +package net.sergeych.superlogin.server + +import net.sergeych.boss_serialization_mp.decodeBoss +import net.sergeych.parsec3.AdapterBuilder +import net.sergeych.parsec3.CommandHost +import net.sergeych.parsec3.WithAdapter +import net.sergeych.superlogin.AuthenticationResult +import net.sergeych.superlogin.SuperloginServerApi +import net.sergeych.superlogin.client.SuperloginData + +inline fun , H : CommandHost> AdapterBuilder.superloginServer( + traits: SLServerTraits, +) { + val a2 = SuperloginServerApi() + on(a2.slRegister) { ra -> + traits.register(ra).also { rr -> + if( rr is AuthenticationResult.Success) + slData = SuperloginData(rr.loginToken, rr.applicationData?.let { it.decodeBoss()}) + loginName = ra.loginName + } + } + on(a2.slLogout) { + println("--- logged out ---") + slData = null + loginName = null + traits.logout() + } +} \ No newline at end of file diff --git a/src/jvmTest/kotlin/net/sergeych/WsServerKtTest.kt b/src/jvmTest/kotlin/net/sergeych/WsServerKtTest.kt new file mode 100644 index 0000000..bcf912d --- /dev/null +++ b/src/jvmTest/kotlin/net/sergeych/WsServerKtTest.kt @@ -0,0 +1,119 @@ +package net.sergeych + +import io.ktor.server.engine.* +import io.ktor.server.netty.* +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.Serializable +import net.sergeych.parsec3.Parsec3WSClient +import net.sergeych.parsec3.WithAdapter +import net.sergeych.parsec3.parsec3TransportServer +import net.sergeych.superlogin.AuthenticationResult +import net.sergeych.superlogin.RegistrationArgs +import net.sergeych.superlogin.SuperloginServerApi +import net.sergeych.superlogin.client.LoginState +import net.sergeych.superlogin.client.Registration +import net.sergeych.superlogin.client.SuperloginClient +import net.sergeych.superlogin.server.SLServerApiBase +import net.sergeych.superlogin.server.SLServerSession +import net.sergeych.superlogin.server.SLServerTraits +import net.sergeych.superlogin.server.superloginServer +import superlogin.assertThrowsAsync +import kotlin.random.Random +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +data class TestSession(var buzz: String = "BuZZ") : SLServerSession() + + +object TestApiServer : SLServerApiBase() { + val loginName by command() +} + + +object TestServerTraits : SLServerTraits { + val byLogin = mutableMapOf() + val byLoginId = mutableMapOf, RegistrationArgs>() + val byRestoreId = mutableMapOf, RegistrationArgs>() + val byToken = mutableMapOf, RegistrationArgs>() + + override suspend fun register(ra: RegistrationArgs): AuthenticationResult { + println("ra: ${ra.loginName}") + return when { + ra.loginName in byLogin -> { + AuthenticationResult.LoginUnavailable + } + ra.loginId.toList() in byLoginId -> AuthenticationResult.LoginIdUnavailable + ra.restoreId.toList() in byRestoreId -> AuthenticationResult.RestoreIdUnavailable + else -> { + byLogin[ra.loginName] = ra + byRestoreId[ra.restoreId.toList()] = ra + byLoginId[ra.loginId.toList()] = ra + val token = Random.Default.nextBytes(32) + byToken[token.toList()] = ra + AuthenticationResult.Success(token, ra.extraData) + } + } + } + +} + +@Serializable +data class TestData( + val foo: String, +) + + +internal class WsServerKtTest { + + + @Test + fun testWsServer() { + + embeddedServer(Netty, port = 8080) { + parsec3TransportServer(TestApiServer) { + newSession { TestSession() } + superloginServer(TestServerTraits) + on(api.loginName) { + loginName + } + } + }.start(wait = false) + + val client = Parsec3WSClient("ws://localhost:8080/api/p3", SuperloginServerApi()) { + } + + + runBlocking { + val slc = SuperloginClient(client) + assertEquals(LoginState.LoggedOut, slc.state.value) + var rt = slc.register("foo", "passwd", TestData("bar!")) + assertIs(rt) + val secret = rt.secret + var token = rt.loginToken + println(rt.secret) + assertEquals("bar!", rt.data()?.foo) + + assertEquals("foo", slc.call(TestApiServer.loginName)) + + val s = slc.state.value + assertIs>(s) + assertEquals("bar!", s.loginData.data!!.foo) + + assertThrowsAsync { + slc.register("foo", "passwd", TestData("nobar")) + } + slc.logout() + assertIs(slc.state.value) + assertEquals(null, slc.call(TestApiServer.loginName)) + + rt = slc.register("foo", "passwd", TestData("nobar")) + assertIs(rt) + +// slc.loginByToken() + } + + } + + +} \ No newline at end of file