102 lines
4.0 KiB
Kotlin

package net.sergeych.kiloparsec
import kotlinx.coroutines.*
import net.sergeych.crypto2.SafeKeyExchange
import net.sergeych.crypto2.SigningKey
import net.sergeych.mp_logger.LogTag
import net.sergeych.mp_logger.Loggable
import net.sergeych.mp_logger.debug
import net.sergeych.mp_logger.info
import net.sergeych.utools.pack
private var clientIds = 0
class KiloClientConnection<S>(
private val clientInterface: KiloInterface<S>,
private val device: Transport.Device,
private val session: S,
private val secretIdKey: SigningKey.Secret? = null,
) : RemoteInterface, Loggable by LogTag("KPC:${++clientIds}") {
constructor(localInterface: KiloInterface<S>, connection: KiloConnectionData<S>, secretIdKey: SigningKey.Secret? = null)
: this(localInterface, connection.device, connection.session, secretIdKey)
private val kiloRemoteInterface = CompletableDeferred<KiloRemoteInterface<S>>()
private val deferredParams = CompletableDeferred<KiloParams<S>>()
suspend fun remoteId(): SigningKey.Public? = deferredParams.await().remoteIdentity
/**
* Run the client, blocking until the device is closed, or some critical exception
* will stop the transport, or the calling scope will be canceled.
* Cancelling the scope where server is running is a preferred way to stop the client.
*/
suspend fun run(onConnectedStateChanged: ((Boolean) -> Unit)? = null) {
coroutineScope {
var job: Job? = null
try {
// in parallel: keys and connection
val deferredKeyPair = async { SafeKeyExchange() }
debug { "opening device" }
debug { "got a transport device $device" }
// client transport has no dedicated commands (unlike the server's),
// it is a calling party:
val l0Interface = KiloL0Interface(clientInterface, deferredParams)
val transport = Transport(device, l0Interface, Unit)
job = launch { transport.run() }
debug { "transport started" }
val pair = deferredKeyPair.await()
debug { "keypair ready" }
val serverHe = transport.call(L0Request, Handshake(1u, pair.publicKey))
val sk = pair.clientSessionKey(serverHe.publicKey)
var params = KiloParams(false, transport, sk, session, null, this@KiloClientConnection)
// Check ID if any
serverHe.signature?.let { s ->
if (!s.isValid(params.token))
throw RemoteInterface.SecurityException("wrong signature")
params = params.copy(remoteIdentity = s.publicKey)
}
transport.call(
L0ClientId, params.encrypt(
pack(
ClientIdentity(
secretIdKey?.publicKey,
secretIdKey?.sign(params.token)
)
)
)
)
deferredParams.complete(params)
kiloRemoteInterface.complete(
KiloRemoteInterface(deferredParams, clientInterface)
)
clientInterface.onConnectHandler?.invoke(params.scope)
onConnectedStateChanged?.invoke(true)
job.join()
} catch (x: CancellationException) {
info { "client is cancelled" }
} catch (x: RemoteInterface.ClosedException) {
x.printStackTrace()
info { "connection closed by remote" }
} finally {
onConnectedStateChanged?.invoke(false)
job?.cancel()
device.apply { runCatching { close() } }
}
}
}
suspend fun token() = deferredParams.await().token
override suspend fun <A, R> call(cmd: Command<A, R>, args: A): R =
kiloRemoteInterface.await().call(cmd, args)
}