import 'dart:async'; import 'dart:collection'; import 'dart:convert'; import 'dart:math'; import 'package:fixnum/fixnum.dart'; import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; import 'package:twonly/src/app.dart'; import 'package:twonly/src/proto/api/client_to_server.pbserver.dart'; import 'package:twonly/src/proto/api/error.pb.dart'; import 'package:twonly/src/proto/api/server_to_client.pb.dart' as server; import 'package:twonly/src/providers/api/api.dart'; import 'package:twonly/src/providers/api/api_utils.dart'; import 'package:twonly/src/providers/api/media.dart'; import 'package:twonly/src/providers/api/server_messages.dart'; import 'package:twonly/src/services/fcm_service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; // ignore: library_prefixes import 'package:twonly/src/utils/signal.dart' as SignalHelper; import 'package:web_socket_channel/io.dart'; // ignore: implementation_imports import 'package:libsignal_protocol_dart/src/ecc/ed25519.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; /// The ApiProvider is responsible for communicating with the server. /// It handles errors and does automatically tries to reconnect on /// errors or network changes. class ApiProvider { final String apiUrl = (kDebugMode) ? "ws://10.99.0.140:3030/api/client" : "wss://api.twonly.eu/api/client"; // ws://api.twonly.eu/api/client final String? backupApiUrl = (kDebugMode) ? "ws://10.99.0.140:3030/api/client" : "wss://api2.twonly.eu/api/client"; bool isAuthenticated = false; ApiProvider(); final log = Logger("ApiProvider"); // reconnection params Timer? reconnectionTimer; // int _reconnectionDelay = 5; final HashMap messagesV0 = HashMap(); IOWebSocketChannel? _channel; Future _connectTo(String apiUrl) async { try { var channel = IOWebSocketChannel.connect( Uri.parse(apiUrl), ); _channel = channel; _channel!.stream.listen(_onData, onDone: _onDone, onError: _onError); await _channel!.ready; log.info("Websocket is connected!"); return true; } on WebSocketChannelException catch (e) { log.shout("Error: $e"); return false; } } // Function is called after the user is authenticated at the server Future onAuthenticated() async { isAuthenticated = true; initFCMAfterAuthenticated(); } Future onConnected() async { await authenticate(); globalCallbackConnectionState(true); if (!globalIsAppInBackground) { tryTransmitMessages(); tryDownloadAllMediaFiles(); notifyContactsAboutAvatarChange(); } } Future close(Function callback) async { log.info("Closing the websocket connection!"); if (_channel != null) { await _channel!.sink.close(); callback(); return; } callback(); } Future connect() async { if (_channel != null) { return true; } // ensure that the connect function is not called again by the timer. if (reconnectionTimer != null) { reconnectionTimer!.cancel(); } isAuthenticated = false; log.fine("Trying to connect to the backend $apiUrl!"); if (await _connectTo(apiUrl)) { await onConnected(); return true; } if (backupApiUrl != null) { log.fine("Trying to connect to the backup backend $backupApiUrl!"); if (await _connectTo(backupApiUrl!)) { await onConnected(); return true; } } return false; } bool get isConnected => _channel != null && _channel!.closeCode != null; void _onDone() { log.info("WebSocket Closed"); globalCallbackConnectionState(false); _channel = null; isAuthenticated = false; } void _onError(dynamic e) { log.info("WebSocket Error: $e"); globalCallbackConnectionState(false); _channel = null; isAuthenticated = false; } void _onData(dynamic msgBuffer) { try { final msg = server.ServerToClient.fromBuffer(msgBuffer); if (msg.v0.hasResponse()) { messagesV0[msg.v0.seq] = msg; } else { handleServerMessage(msg); } } catch (e) { log.shout("Error parsing the servers message: $e"); } } Future _waitForResponse(Int64 seq) async { final startTime = DateTime.now(); final timeout = Duration(seconds: 5); while (true) { if (messagesV0[seq] != null) { final tmp = messagesV0[seq]; messagesV0.remove(seq); return tmp; } if (DateTime.now().difference(startTime) > timeout) { log.shout("Timeout for message $seq"); return null; } await Future.delayed(Duration(milliseconds: 10)); } } Future sendResponse(ClientToServer response) async { if (_channel != null) { _channel!.sink.add(response.writeToBuffer()); } } Future sendRequestSync(ClientToServer request, {bool authenticated = true}) async { if (_channel == null) { log.shout("sending request, but api is not connected."); if (!await connect()) { return Result.error(ErrorCode.InternalError); } } if (_channel == null) { return Result.error(ErrorCode.InternalError); } var seq = Int64(Random().nextInt(4294967296)); while (messagesV0.containsKey(seq)) { seq = Int64(Random().nextInt(4294967296)); } request.v0.seq = seq; final requestBytes = request.writeToBuffer(); _channel!.sink.add(requestBytes); Result res = asResult(await _waitForResponse(seq)); if (res.isError) { if (res.error == ErrorCode.SessionNotAuthenticated) { isAuthenticated = false; if (authenticated) { await authenticate(); if (isAuthenticated) { // this will send the request one more time. return sendRequestSync(request, authenticated: false); } else { log.shout("Session is not authenticated."); return Result.error(ErrorCode.InternalError); } } } } return res; } Future tryAuthenticateWithToken(int userId) async { final storage = getSecureStorage(); String? apiAuthToken = await storage.read(key: "api_auth_token"); if (apiAuthToken != null) { final authenticate = Handshake_Authenticate() ..userId = Int64(userId) ..authToken = base64Decode(apiAuthToken); final handshake = Handshake()..authenticate = authenticate; final req = createClientToServerFromHandshake(handshake); final result = await sendRequestSync(req, authenticated: false); if (result.isSuccess) { log.info("Authenticated using api_auth_token"); onAuthenticated(); return true; } if (result.isError) { if (result.error != ErrorCode.AuthTokenNotValid) { log.shout("Error while authenticating using token", result); return false; } } } return false; } Future authenticate() async { if (isAuthenticated) return; if (await SignalHelper.getSignalIdentity() == null) { return; } final userData = await getUser(); if (userData == null) return; if (await tryAuthenticateWithToken(userData.userId)) { return; } var handshake = Handshake() ..getauthchallenge = Handshake_GetAuthChallenge(); var req = createClientToServerFromHandshake(handshake); final result = await sendRequestSync(req, authenticated: false); if (result.isError) { log.shout("Error requesting auth challenge", result); return; } final challenge = result.value.authchallenge; final privKey = await SignalHelper.getPrivateKey(); if (privKey == null) return; final random = getRandomUint8List(32); final signature = sign(privKey.serialize(), challenge, random); final getAuthToken = Handshake_GetAuthToken() ..response = signature ..userId = Int64(userData.userId); final getauthtoken = Handshake()..getauthtoken = getAuthToken; var req2 = createClientToServerFromHandshake(getauthtoken); final result2 = await sendRequestSync(req2, authenticated: false); if (result2.isError) { log.shout("Error while sending auth challenge: ${result2.error}"); return; } Uint8List apiAuthToken = result2.value.authtoken; String apiAuthTokenB64 = base64Encode(apiAuthToken); final storage = getSecureStorage(); await storage.write(key: "api_auth_token", value: apiAuthTokenB64); await tryAuthenticateWithToken(userData.userId); } Future register(String username, String? inviteCode) async { final signalIdentity = await SignalHelper.getSignalIdentity(); if (signalIdentity == null) { return Result.error(ErrorCode.InternalError); } final signalStore = await SignalHelper.getSignalStoreFromIdentity(signalIdentity); final signedPreKey = (await signalStore.loadSignedPreKeys())[0]; log.shout("handle registrationId", signalIdentity.registrationId); var register = Handshake_Register() ..username = username ..publicIdentityKey = (await signalStore.getIdentityKeyPair()).getPublicKey().serialize() ..registrationId = Int64(signalIdentity.registrationId) ..signedPrekey = signedPreKey.getKeyPair().publicKey.serialize() ..signedPrekeySignature = signedPreKey.signature ..signedPrekeyId = Int64(signedPreKey.id); if (inviteCode != null && inviteCode != "") { register.inviteCode = inviteCode; } // Create the Handshake message var handshake = Handshake()..register = register; var req = createClientToServerFromHandshake(handshake); return await sendRequestSync(req); } Future getUsername(int userId) async { var get = ApplicationData_GetUserById()..userId = Int64(userId); var appData = ApplicationData()..getuserbyid = get; var req = createClientToServerFromApplicationData(appData); return await sendRequestSync(req); } Future getUploadToken(int recipientsCount) async { var get = ApplicationData_GetUploadToken() ..recipientsCount = recipientsCount; var appData = ApplicationData()..getuploadtoken = get; var req = createClientToServerFromApplicationData(appData); return await sendRequestSync(req); } Future triggerDownload(List token, int offset) async { var get = ApplicationData_DownloadData() ..downloadToken = token ..offset = offset; var appData = ApplicationData()..downloaddata = get; var req = createClientToServerFromApplicationData(appData); return await sendRequestSync(req); } Future uploadData(List uploadToken, Uint8List data, int offset, List? checksum) async { var get = ApplicationData_UploadData() ..uploadToken = uploadToken ..data = data ..offset = offset; if (checksum != null) { get.checksum = checksum; } var appData = ApplicationData()..uploaddata = get; var req = createClientToServerFromApplicationData(appData); final result = await sendRequestSync(req); return result; } Future getUserData(String username) async { var get = ApplicationData_GetUserByUsername()..username = username; var appData = ApplicationData()..getuserbyusername = get; var req = createClientToServerFromApplicationData(appData); return await sendRequestSync(req); } Future updateFCMToken(String googleFcm) async { var get = ApplicationData_UpdateGoogleFcmToken()..googleFcm = googleFcm; var appData = ApplicationData()..updategooglefcmtoken = get; var req = createClientToServerFromApplicationData(appData); return await sendRequestSync(req); } Future sendTextMessage(int target, Uint8List msg) async { var testMessage = ApplicationData_TextMessage() ..userId = Int64(target) ..body = msg; var appData = ApplicationData()..textmessage = testMessage; var req = createClientToServerFromApplicationData(appData); return await sendRequestSync(req); } }