102 lines
4.0 KiB
Kotlin
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)
|
|
} |