twonly-app/lib/src/providers/api_provider.dart
2025-01-23 20:46:52 +01:00

349 lines
11 KiB
Dart

import 'dart:collection';
import 'dart:math';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/material.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:logging/logging.dart';
import 'package:twonly/src/proto/api/client_to_server.pb.dart' as client;
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/signal/signal_helper.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:twonly/src/utils.dart';
import 'package:web_socket_channel/io.dart';
import 'package:libsignal_protocol_dart/src/ecc/ed25519.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
class Result<T, E> {
final T? value;
final E? error;
bool get isSuccess => value != null;
bool get isError => error != null;
Result.success(this.value) : error = null;
Result.error(this.error) : value = null;
}
class ApiProvider {
ApiProvider({required this.apiUrl, required this.backupApiUrl});
final String apiUrl;
final String? backupApiUrl;
int _reconnectionDelay = 5;
final log = Logger("api_provider");
Function(bool)? _connectionStateCallback;
final HashMap<Int64, server.ServerToClient?> messagesV0 = HashMap();
IOWebSocketChannel? _channel;
Future<bool> _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;
}
}
Future<bool> connect() async {
if (_channel != null && _channel!.closeCode != null) {
return true;
}
log.info("Trying to connect to the backend $apiUrl!");
if (await _connectTo(apiUrl)) {
await authenticate();
if (_connectionStateCallback != null) _connectionStateCallback!(true);
_reconnectionDelay = 5;
return true;
}
if (backupApiUrl != null) {
log.info("Trying to connect to the backup backend $backupApiUrl!");
if (await _connectTo(backupApiUrl!)) {
await authenticate();
if (_connectionStateCallback != null) _connectionStateCallback!(true);
_reconnectionDelay = 5;
return true;
}
}
return false;
}
bool get isConnected => _channel != null && _channel!.closeCode != null;
void _onDone() {
if (_connectionStateCallback != null) {
_connectionStateCallback!(false);
}
_channel = null;
tryToReconnect();
}
void _onError(dynamic e) {
if (_connectionStateCallback != null) {
_connectionStateCallback!(false);
}
_channel = null;
tryToReconnect();
}
void setConnectionStateCallback(Function(bool) callBack) {
_connectionStateCallback = callBack;
}
void tryToReconnect() {
Future.delayed(Duration(seconds: _reconnectionDelay)).then(
(value) async {
_reconnectionDelay = _reconnectionDelay + 2;
if (_reconnectionDelay > 20) {
_reconnectionDelay = 20;
}
await connect();
},
);
}
void _onData(dynamic msgBuffer) {
try {
final msg = server.ServerToClient.fromBuffer(msgBuffer);
if (msg.v0.hasResponse()) {
messagesV0[msg.v0.seq] = msg;
} else {
_handleServerMessage(msg);
log.shout("Got a new message from the server: $msg");
}
} catch (e) {
log.shout("Error parsing the servers message: $e");
}
}
Future _handleServerMessage(server.ServerToClient msg) async {
client.Response? response;
if (msg.v0.requestNewPreKeys) {
List<PreKeyRecord> localPreKeys = await SignalHelper.getPreKeys();
List<client.Response_PreKey> prekeysList = [];
for (int i = 0; i < localPreKeys.length; i++) {
prekeysList.add(client.Response_PreKey()
..id = Int64(localPreKeys[i].id)
..prekey = localPreKeys[i].getKeyPair().publicKey.serialize());
}
var prekeys = client.Response_Prekeys(prekeys: prekeysList);
var ok = client.Response_Ok()..prekeys = prekeys;
response = client.Response()..ok = ok;
}
if (response == null) return;
var v0 = client.V0()
..seq = msg.v0.seq
..response = response;
var res = ClientToServer()..v0 = v0;
final resBytes = res.writeToBuffer();
_channel!.sink.add(resBytes);
}
Future<server.ServerToClient?> _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<server.ServerToClient?> _sendRequestV0(ClientToServer request) async {
if (_channel == null) {
return null;
}
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);
return await _waitForResponse(seq);
}
ClientToServer createClientToServerFromHandshake(Handshake handshake) {
var v0 = client.V0()
..seq = Int64(0)
..handshake = handshake;
return ClientToServer()..v0 = v0;
}
ClientToServer createClientToServerFromApplicationData(
ApplicationData applicationData) {
var v0 = client.V0()
..seq = Int64(0)
..applicationdata = applicationData;
return ClientToServer()..v0 = v0;
}
static String getLocalizedString(BuildContext context, ErrorCode code) {
switch (code.toString()) {
case "Unknown":
return AppLocalizations.of(context)!.errorUnknown;
case "BadRequest":
return AppLocalizations.of(context)!.errorBadRequest;
case "TooManyRequests":
return AppLocalizations.of(context)!.errorTooManyRequests;
case "InternalError":
return AppLocalizations.of(context)!.errorInternalError;
case "InvalidInvitationCode":
return AppLocalizations.of(context)!.errorInvalidInvitationCode;
case "UsernameAlreadyTaken":
return AppLocalizations.of(context)!.errorUsernameAlreadyTaken;
case "SignatureNotValid":
return AppLocalizations.of(context)!.errorSignatureNotValid;
case "UsernameNotFound":
return AppLocalizations.of(context)!.errorUsernameNotFound;
case "UsernameNotValid":
return AppLocalizations.of(context)!.errorUsernameNotValid;
case "InvalidPublicKey":
return AppLocalizations.of(context)!.errorInvalidPublicKey;
case "SessionAlreadyAuthenticated":
return AppLocalizations.of(context)!.errorSessionAlreadyAuthenticated;
case "SessionNotAuthenticated":
return AppLocalizations.of(context)!.errorSessionNotAuthenticated;
case "OnlyOneSessionAllowed":
return AppLocalizations.of(context)!.errorOnlyOneSessionAllowed;
default:
return code.toString(); // Fallback for unrecognized keys
}
}
Result _asResult(server.ServerToClient msg) {
if (msg.v0.response.hasOk()) {
return Result.success(msg.v0.response.ok);
} else {
return Result.error(msg.v0.response.error);
}
}
Future authenticate() async {
if (await SignalHelper.getSignalIdentity() == null) {
return;
}
var handshake = Handshake()..getchallenge = Handshake_GetChallenge();
var req = createClientToServerFromHandshake(handshake);
final resp = await _sendRequestV0(req);
if (resp == null) {
log.shout("Server is not reachable!");
return;
}
final result = _asResult(resp);
if (result.isError) {
log.shout(result);
return;
}
final challenge = result.value.challenge;
final privKey = await SignalHelper.getPrivateKey();
if (privKey == null) return;
final random = getRandomUint8List(32);
final signature = sign(privKey.serialize(), challenge, random);
final userData = await getUser();
if (userData == null) return;
var open = Handshake_OpenSession()
..response = signature
..userId = userData.userId;
var opensession = Handshake()..opensession = open;
var req2 = createClientToServerFromHandshake(opensession);
final resp2 = await _sendRequestV0(req2);
if (resp2 == null) {
log.shout("Server is not reachable!");
return;
}
final result2 = _asResult(resp2);
if (result2.isError) {
log.shout(result2);
return;
}
log.info("Authenticated!");
}
Future<Result> register(String username, String? inviteCode) async {
final signalIdentity = await SignalHelper.getSignalIdentity();
if (signalIdentity == null) {
return Result.error(
"There was an fatal error. Try reinstalling the app.");
}
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);
final resp = await _sendRequestV0(req);
if (resp == null) {
return Result.error("Server is not reachable!");
}
return _asResult(resp);
}
Future<Result> getUserData(String username) async {
var get = ApplicationData_GetUserByUsername()..username = username;
var appData = ApplicationData()..getuserbyusername = get;
var req = createClientToServerFromApplicationData(appData);
final resp = await _sendRequestV0(req);
if (resp == null) {
return Result.error("Server is not reachable!");
}
return _asResult(resp);
}
}