working variant of server/client kotlin architecture (1)

This commit is contained in:
Sergey Chernov 2022-11-26 12:34:14 +01:00
parent 8b8724b0f1
commit 5df6709983
15 changed files with 538 additions and 62 deletions

View File

@ -1,9 +1,13 @@
plugins { plugins {
kotlin("multiplatform") version "1.7.20" kotlin("multiplatform") version "1.7.10"
kotlin("plugin.serialization") version "1.7.20" kotlin("plugin.serialization") version "1.7.10"
`maven-publish` `maven-publish`
} }
val ktor_version="2.1.1"
val logback_version="1.2.10"
group = "net.sergeych" group = "net.sergeych"
version = "0.0.1-SNAPSHOT" version = "0.0.1-SNAPSHOT"
@ -51,7 +55,13 @@ kotlin {
} }
} }
val jvmMain by getting 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 jsMain by getting
val jsTest by getting val jsTest by getting
} }

View File

@ -12,7 +12,7 @@ object BackgroundKeyGenerator {
const val DefaultStrength = 4096 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 keyStrength = DefaultStrength
private var nextKey: Deferred<PrivateKey>? = null private var nextKey: Deferred<PrivateKey>? = null
@ -73,14 +73,17 @@ object BackgroundKeyGenerator {
} }
fun randomBytes(length: Int,minEntropy: Int): ByteArray { fun randomBytes(length: Int,minEntropy: Int): ByteArray {
addEntropyTimestamp()
entropyHash?.let { entropyHash?.let {
if (minEntropy <= entropy) { if (minEntropy <= entropy) {
entropy -= minEntropy entropy -= minEntropy
addEntropyTimestamp()
return randomBytes(length, it) 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 { fun randomBytes(length: Int, IV: ByteArray? = null): ByteArray {

View File

@ -11,7 +11,7 @@ import kotlin.random.Random
* and a method to derive password accordingly. * and a method to derive password accordingly.
*/ */
@Serializable @Serializable
class PasswordDerivationParams( data class PasswordDerivationParams(
val rounds: Int = 15000, val rounds: Int = 15000,
val algorithm: HashAlgorithm = HashAlgorithm.SHA3_256, val algorithm: HashAlgorithm = HashAlgorithm.SHA3_256,
val salt: ByteArray = Random.nextBytes(32), val salt: ByteArray = Random.nextBytes(32),

View File

@ -1,17 +0,0 @@
package net.sergeych.superlogin
import net.sergeych.parsec3.Adapter
class SuperloginClient(adapter: Adapter<*>) {
// init {
// adapter.invokeCommand()
// }
fun register() {
}
}

View File

@ -13,7 +13,8 @@ data class RegistrationArgs(
val loginPublicKey: PublicKey, val loginPublicKey: PublicKey,
val derivationParams: PasswordDerivationParams, val derivationParams: PasswordDerivationParams,
val restoreId: ByteArray, val restoreId: ByteArray,
val restoreData: ByteArray val restoreData: ByteArray,
val extraData: ByteArray? = null
) )
@Serializable @Serializable
@ -21,11 +22,15 @@ sealed class AuthenticationResult {
@Serializable @Serializable
data class Success( data class Success(
val loginToken: ByteArray, val loginToken: ByteArray,
val applicationData: ByteArray?
): AuthenticationResult() ): AuthenticationResult()
@Serializable @Serializable
object LoginUnavailable: AuthenticationResult() object LoginUnavailable: AuthenticationResult()
@Serializable
object LoginIdUnavailable: AuthenticationResult()
@Serializable @Serializable
object RestoreIdUnavailable: AuthenticationResult() object RestoreIdUnavailable: AuthenticationResult()
} }
@ -36,23 +41,19 @@ data class LoginArgs(
val packedSignedRecord: ByteArray val packedSignedRecord: ByteArray
) )
@Serializable
data class LoginData(
val encryptedPrivateKey: ByteArray,
val loginNonce: ByteArray
)
class SuperloginServerApi<T: WithAdapter> : CommandHost<T>() { class SuperloginServerApi<T: WithAdapter> : CommandHost<T>() {
val registerUser by command<RegistrationArgs,AuthenticationResult>() val slRegister by command<RegistrationArgs,AuthenticationResult>()
val loginUserByToken by command<ByteArray,AuthenticationResult>() val slLogout by command<Unit,Unit>()
val slLoginByToken by command<ByteArray,AuthenticationResult>()
val requestUserLoginParams by command<String,PasswordDerivationParams>() val slRequestDerivationParams by command<String,PasswordDerivationParams>()
/** /**
* Get resstoreData by restoreId: password reset procedure start. * Get resstoreData by restoreId: password reset procedure start.
*/ */
val requestUserLogin by command<ByteArray,ByteArray>() // val requestUserLogin by command<ByteArray,ByteArray>()
// val performLogin by command<LoginArgs // val performLogin by command<LoginArgs
} }

View File

@ -0,0 +1,15 @@
package net.sergeych.superlogin.client
sealed class LoginState(val isLoggedIn: Boolean) {
/**
* User is logged in (either connected or yet not). Client application should save
* updated [loginData] at this point
*/
class LoggedIn<D>(val loginData: SuperloginData<D>) : 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)
}

View File

@ -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.LogTag
import net.sergeych.mp_logger.debug
import net.sergeych.mp_logger.error import net.sergeych.mp_logger.error
import net.sergeych.mp_logger.exception import net.sergeych.mp_logger.exception
import net.sergeych.parsec3.Adapter import net.sergeych.parsec3.Adapter
import net.sergeych.parsec3.WithAdapter import net.sergeych.parsec3.WithAdapter
import net.sergeych.superlogin.*
import net.sergeych.superlogin.server.SuperloginRestoreAccessPayload
import net.sergeych.unikrypto.HashAlgorithm import net.sergeych.unikrypto.HashAlgorithm
import net.sergeych.unikrypto.SymmetricKey import net.sergeych.unikrypto.SymmetricKey
import net.sergeych.unikrypto.digest import net.sergeych.unikrypto.digest
import kotlin.random.Random
/** /**
* Registration instances are used to perform superlogin-compatible registration in a smart way * 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. * See [register] for more.
*/ */
class Registration<T: WithAdapter>(val adapter: Adapter<T>,loginKeyStrength: Int = 4096): LogTag("SLREG") { class Registration(
val adapter: Adapter<*>, loginKeyStrength: Int = 4096,
val pbkdfRounds: Int = 15000,
) : LogTag("SLREG") {
sealed class Result { sealed class Result {
/** /**
* Login is already in use or is somehow else invalid * Login is already in use or is somehow else invalid
*/ */
object InvalidLogin: Result() object InvalidLogin : Result()
/** /**
* Operation failed for nknown reason, usually it means * Operation failed for nknown reason, usually it means
* network or server downtime * 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 <reified D>data() = encodedData?.let { BossDecoder.decodeFrom<D>(it)}
}
} }
private var lastPasswordHash: ByteArray? = null private var lastPasswordHash: ByteArray? = null
private var lastDerivationParams: PasswordDerivationParams? = null private var lastDerivationParams: PasswordDerivationParams? = null
private var passwordKeys: DerivedKeys? = null private var passwordKeys: DerivedKeys? = null
val api = SuperloginServerApi<T>() val api = SuperloginServerApi<WithAdapter>()
private val deferredLoginKey = BackgroundKeyGenerator.getKeyAsync(loginKeyStrength) private val deferredLoginKey = BackgroundKeyGenerator.getKeyAsync(loginKeyStrength)
private val dataKey = BackgroundKeyGenerator.randomSymmetricKey() private val dataKey = BackgroundKeyGenerator.randomSymmetricKey()
inline suspend fun <reified T> 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 * 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 * 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. * password and derivarion parameters are not changed between calls.
*/ */
suspend fun register( suspend fun registerWithData(
login: String, login: String,
password: String, password: String,
derivationParams: PasswordDerivationParams = PasswordDerivationParams() extraData: ByteArray? = null,
): Result { ): Result {
val newPasswordHash = HashAlgorithm.SHA3_256.digest(password) val newPasswordHash = HashAlgorithm.SHA3_256.digest(password)
if( lastPasswordHash?.contentEquals(newPasswordHash) != true || if (lastPasswordHash?.contentEquals(newPasswordHash) != true ||
lastDerivationParams?.equals(derivationParams) != true || passwordKeys == null
passwordKeys == null ) { ) {
passwordKeys = DerivedKeys.derive(password,derivationParams) lastDerivationParams = PasswordDerivationParams(pbkdfRounds)
lastDerivationParams = derivationParams passwordKeys = DerivedKeys.derive(password, lastDerivationParams!! )
lastPasswordHash = newPasswordHash lastPasswordHash = newPasswordHash
} }
val spl = SuperloginPayload(login, deferredLoginKey.await(), dataKey) val loginPrivateKey = deferredLoginKey.await()
val spl = SuperloginRestoreAccessPayload(login, loginPrivateKey, dataKey)
repeat(10) { repeat(10) {
val (restoreKey, restoreData) = AccessControlObject.pack(passwordKeys!!.loginAccessKey,spl) val (restoreKey, restoreData) = AccessControlObject.pack(passwordKeys!!.loginAccessKey, spl)
try { try {
val result = adapter.invokeCommand( val result = adapter.invokeCommand(
api.registerUser, RegistrationArgs( api.slRegister, RegistrationArgs(
login, login,
passwordKeys!!.loginId, passwordKeys!!.loginId,
deferredLoginKey.await().publicKey, deferredLoginKey.await().publicKey,
derivationParams, lastDerivationParams!!,
restoreKey.restoreId, restoreData restoreKey.restoreId, restoreData,
extraData
) )
) )
when (result) { when (result) {
AuthenticationResult.RestoreIdUnavailable -> { AuthenticationResult.RestoreIdUnavailable -> {
// rare situation but still possible: just repack the ACO // 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 AuthenticationResult.LoginUnavailable -> return Result.InvalidLogin
is AuthenticationResult.Success -> return Result.Success(
restoreKey.secret, is AuthenticationResult.Success -> {
dataKey, return Result.Success(
result.loginToken restoreKey.secret,
) dataKey,
result.loginToken,
result.applicationData
)
}
} }
} } catch (x: Throwable) {
catch(x: Throwable) {
exception { "Failed to register" to x } exception { "Failed to register" to x }
} }
} }

View File

@ -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<T>(
val loginToken: ByteArray? = null,
val data: T? = null,
)
class SuperloginClient<D, S : WithAdapter>(
private val transport: Parsec3Transport<S>,
savedData: SuperloginData<D>? = null,
private val dataType: KType,
) : Parsec3Transport<S>, Loggable by LogTag("SLCLI") {
private val _state = MutableStateFlow<LoginState>(
if (savedData == null) LoginState.LoggedOut
else LoginState.LoggedIn(savedData)
)
val state: StateFlow<LoginState> = _state
private val _cflow = MutableStateFlow<Boolean>(false)
override val connectedFlow: StateFlow<Boolean> = _cflow
private var slData: SuperloginData<D>? = 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<D>)?.loginData?.data
private var adapterReady = CompletableDeferred<Unit>()
override suspend fun adapter(): Adapter<S> {
do {
try {
adapterReady.await()
return transport.adapter()
} catch (x: Throwable) {
exception { "failed to get adapter" to x }
}
} while (true)
}
suspend fun <A, R> call(ca: CommandDescriptor<A, R>, args: A ): R = adapter().invokeCommand(ca, args)
suspend fun <R> call(ca: CommandDescriptor<Unit, R>): R = adapter().invokeCommand(ca)
private suspend fun <A, R> invoke(ca: CommandDescriptor<A, R>, args: A ): R = transport.adapter().invokeCommand(ca, args)
private suspend fun <R> invoke(ca: CommandDescriptor<Unit, R>): R = transport.adapter().invokeCommand(ca)
private var jobs = listOf<Job>()
private val api = SuperloginServerApi<WithAdapter>()
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<D> {
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 <reified D, S : WithAdapter> invoke(
t: Parsec3Transport<S>,
savedData: SuperloginData<D>? = null,
): SuperloginClient<D, S> {
return SuperloginClient(t, savedData, typeOf<D>())
}
}
}

View File

@ -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() {}
}

View File

@ -1,4 +1,4 @@
package net.sergeych.superlogin package net.sergeych.superlogin.server
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import net.sergeych.unikrypto.PrivateKey import net.sergeych.unikrypto.PrivateKey
@ -13,7 +13,7 @@ import net.sergeych.unikrypto.SymmetricKey
* - storage key (used to safely keep stored data) * - storage key (used to safely keep stored data)
*/ */
@Serializable @Serializable
data class SuperloginPayload( data class SuperloginRestoreAccessPayload(
val login: String, val login: String,
val loginPrivateKey: PrivateKey, val loginPrivateKey: PrivateKey,
val dataStorageKey: SymmetricKey val dataStorageKey: SymmetricKey

View File

@ -0,0 +1,14 @@
package superlogin
import kotlin.test.fail
suspend inline fun <reified T: Throwable> 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}")
}
}

View File

@ -0,0 +1,8 @@
package net.sergeych.superlogin.server
import net.sergeych.parsec3.CommandHost
/**
* Server-side API convenience base.
*/
open class SLServerApiBase<D>: CommandHost<SLServerSession<D>>()

View File

@ -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<T>: WithAdapter() {
var slData: SuperloginData<T>? = null
val userData: T? get() = slData?.let { it.data }
val loginToken get() = slData?.loginToken
var loginName: String? = null
}

View File

@ -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 <reified D, T : SLServerSession<D>, H : CommandHost<T>> AdapterBuilder<T, H>.superloginServer(
traits: SLServerTraits,
) {
val a2 = SuperloginServerApi<WithAdapter>()
on(a2.slRegister) { ra ->
traits.register(ra).also { rr ->
if( rr is AuthenticationResult.Success)
slData = SuperloginData<D>(rr.loginToken, rr.applicationData?.let { it.decodeBoss<D>()})
loginName = ra.loginName
}
}
on(a2.slLogout) {
println("--- logged out ---")
slData = null
loginName = null
traits.logout()
}
}

View File

@ -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<TestData>()
object TestApiServer : SLServerApiBase<TestData>() {
val loginName by command<Unit, String?>()
}
object TestServerTraits : SLServerTraits {
val byLogin = mutableMapOf<String, RegistrationArgs>()
val byLoginId = mutableMapOf<List<Byte>, RegistrationArgs>()
val byRestoreId = mutableMapOf<List<Byte>, RegistrationArgs>()
val byToken = mutableMapOf<List<Byte>, 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<WithAdapter>()) {
}
runBlocking {
val slc = SuperloginClient<TestData, WithAdapter>(client)
assertEquals(LoginState.LoggedOut, slc.state.value)
var rt = slc.register("foo", "passwd", TestData("bar!"))
assertIs<Registration.Result.Success>(rt)
val secret = rt.secret
var token = rt.loginToken
println(rt.secret)
assertEquals("bar!", rt.data<TestData>()?.foo)
assertEquals("foo", slc.call(TestApiServer.loginName))
val s = slc.state.value
assertIs<LoginState.LoggedIn<TestData>>(s)
assertEquals("bar!", s.loginData.data!!.foo)
assertThrowsAsync<IllegalStateException> {
slc.register("foo", "passwd", TestData("nobar"))
}
slc.logout()
assertIs<LoginState.LoggedOut>(slc.state.value)
assertEquals(null, slc.call(TestApiServer.loginName))
rt = slc.register("foo", "passwd", TestData("nobar"))
assertIs<Registration.Result.InvalidLogin>(rt)
// slc.loginByToken()
}
}
}