working variant of server/client kotlin architecture (1)
This commit is contained in:
parent
8b8724b0f1
commit
5df6709983
@ -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
|
||||
}
|
||||
|
@ -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<PrivateKey>? = 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 {
|
||||
|
@ -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),
|
||||
|
@ -1,17 +0,0 @@
|
||||
package net.sergeych.superlogin
|
||||
|
||||
import net.sergeych.parsec3.Adapter
|
||||
|
||||
|
||||
class SuperloginClient(adapter: Adapter<*>) {
|
||||
|
||||
// init {
|
||||
// adapter.invokeCommand()
|
||||
// }
|
||||
|
||||
fun register() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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<T: WithAdapter> : CommandHost<T>() {
|
||||
|
||||
val registerUser by command<RegistrationArgs,AuthenticationResult>()
|
||||
val loginUserByToken by command<ByteArray,AuthenticationResult>()
|
||||
val slRegister by command<RegistrationArgs,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.
|
||||
*/
|
||||
val requestUserLogin by command<ByteArray,ByteArray>()
|
||||
// val requestUserLogin by command<ByteArray,ByteArray>()
|
||||
// val performLogin by command<LoginArgs
|
||||
}
|
@ -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)
|
||||
}
|
@ -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<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 {
|
||||
/**
|
||||
* 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 <reified D>data() = encodedData?.let { BossDecoder.decodeFrom<D>(it)}
|
||||
}
|
||||
}
|
||||
|
||||
private var lastPasswordHash: ByteArray? = null
|
||||
private var lastDerivationParams: PasswordDerivationParams? = null
|
||||
private var passwordKeys: DerivedKeys? = null
|
||||
val api = SuperloginServerApi<T>()
|
||||
val api = SuperloginServerApi<WithAdapter>()
|
||||
private val deferredLoginKey = BackgroundKeyGenerator.getKeyAsync(loginKeyStrength)
|
||||
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
|
||||
* 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 }
|
||||
}
|
||||
}
|
@ -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>())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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() {}
|
||||
}
|
@ -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
|
14
src/commonTest/kotlin/superlogin/assert_throws.kt
Normal file
14
src/commonTest/kotlin/superlogin/assert_throws.kt
Normal 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}")
|
||||
}
|
||||
}
|
@ -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>>()
|
@ -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
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
119
src/jvmTest/kotlin/net/sergeych/WsServerKtTest.kt
Normal file
119
src/jvmTest/kotlin/net/sergeych/WsServerKtTest.kt
Normal 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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user