import 'dart:async'; import 'dart:convert'; import 'package:arbiter/features/connection/server_info_storage.dart'; import 'package:arbiter/features/identity/pk_manager.dart'; import 'package:arbiter/proto/arbiter.pbgrpc.dart'; import 'package:arbiter/proto/user_agent.pb.dart'; import 'package:grpc/grpc.dart'; import 'package:mtcore/markettakers.dart'; class Connection { final ClientChannel channel; final StreamController _tx; final StreamIterator _rx; Connection({ required this.channel, required StreamController tx, required ResponseStream rx, }) : _tx = tx, _rx = StreamIterator(rx); Future send(UserAgentRequest request) async { _tx.add(request); } Future receive() async { await _rx.moveNext(); return _rx.current; } Future close() async { await _tx.close(); await channel.shutdown(); } } Future _connect(StoredServerInfo serverInfo) async { final channel = ClientChannel( serverInfo.address, port: serverInfo.port, options: ChannelOptions( connectTimeout: const Duration(seconds: 10), credentials: ChannelCredentials.secure( onBadCertificate: (cert, host) { return true; }, ), ), ); final client = ArbiterServiceClient(channel); final tx = StreamController(); final rx = client.userAgent(tx.stream); return Connection(channel: channel, tx: tx, rx: rx); } List formatChallenge(AuthChallenge challenge, List pubkey) { final encodedPubkey = base64Encode(pubkey); final payload = "${challenge.nonce}:$encodedPubkey"; return utf8.encode(payload); } Future connectAndAuthorize( StoredServerInfo serverInfo, KeyHandle key, { String? bootstrapToken, }) async { try { final connection = await _connect(serverInfo); talker.info( 'Connected to server at ${serverInfo.address}:${serverInfo.port}', ); final pubkey = await key.getPublicKey(); final req = AuthChallengeRequest( pubkey: pubkey, bootstrapToken: bootstrapToken, keyType: switch (key.alg) { KeyAlgorithm.rsa => KeyType.KEY_TYPE_RSA, KeyAlgorithm.ecdsa => KeyType.KEY_TYPE_ECDSA_SECP256K1, KeyAlgorithm.ed25519 => KeyType.KEY_TYPE_ED25519, }, ); await connection.send(UserAgentRequest(authChallengeRequest: req)); talker.info( "Sent auth challenge request with pubkey ${base64Encode(pubkey)}", ); final response = await connection.receive(); talker.info( 'Received response from server, checking for auth challenge...', ); if (!response.hasAuthChallenge()) { throw Exception( 'Expected AuthChallengeResponse, got ${response.whichPayload()}', ); } final challenge = formatChallenge(response.authChallenge, pubkey); talker.info( 'Received auth challenge, signing with key ${base64Encode(pubkey)}', ); final signature = await key.sign(challenge); final solutionReq = AuthChallengeSolution(signature: signature); await connection.send(UserAgentRequest(authChallengeSolution: solutionReq)); talker.info('Sent auth challenge solution, waiting for server response...'); final solutionResponse = await connection.receive(); if (!solutionResponse.hasAuthOk()) { throw Exception( 'Expected AuthChallengeSolutionResponse, got ${solutionResponse.whichPayload()}', ); } talker.info('Authentication successful, connection established'); return connection; } catch (e) { throw Exception('Failed to connect to server: $e'); } }