feat: cache .well-known data

- BREAKING: create `DatabaseApi.storeWellKnown` method
- BREAKING: create `DatabaseApi.getWellKnown` method
- add new getter `Client.wellKnown` containing cached `DiscoveryInformation`
- override `Client.homeserver` to invalidate `Client.wellKnown` in case the domain changed
- override `Client.getWellknown` to cache the resolved `DiscoveryInformation`
- add tests for well-known cache

Fixes: #1865

Signed-off-by: The one with the braid <info@braid.business>
This commit is contained in:
The one with the braid 2024-08-12 21:53:03 +02:00 committed by Nicolas Werner
parent 11b8160f21
commit 6e48c308ad
No known key found for this signature in database
GPG Key ID: B38119FF80087618
6 changed files with 123 additions and 6 deletions

View File

@ -25,7 +25,6 @@ import 'dart:typed_data';
import 'package:async/async.dart'; import 'package:async/async.dart';
import 'package:collection/collection.dart' show IterableExtension; import 'package:collection/collection.dart' show IterableExtension;
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:http/http.dart';
import 'package:mime/mime.dart'; import 'package:mime/mime.dart';
import 'package:olm/olm.dart' as olm; import 'package:olm/olm.dart' as olm;
import 'package:random_string/random_string.dart'; import 'package:random_string/random_string.dart';
@ -124,6 +123,24 @@ class Client extends MatrixApi {
/// The timeout until a typing indicator gets removed automatically. /// The timeout until a typing indicator gets removed automatically.
final Duration typingIndicatorTimeout; final Duration typingIndicatorTimeout;
DiscoveryInformation? _wellKnown;
/// the cached .well-known file updated using [getWellknown]
DiscoveryInformation? get wellKnown => _wellKnown;
/// The homeserver this client is communicating with.
///
/// In case the [homeserver]'s host differs from the previous value, the
/// [wellKnown] cache will be invalidated.
@override
set homeserver(Uri? homeserver) {
if (homeserver?.host != this.homeserver?.host) {
_wellKnown = null;
unawaited(database?.storeWellKnown(null));
}
super.homeserver = homeserver;
}
Future<MatrixImageFileResizedResponse?> Function( Future<MatrixImageFileResizedResponse?> Function(
MatrixImageFileResizeArguments)? customImageResizer; MatrixImageFileResizeArguments)? customImageResizer;
@ -531,6 +548,27 @@ class Client extends MatrixApi {
} }
} }
/// Gets discovery information about the domain. The file may include
/// additional keys, which MUST follow the Java package naming convention,
/// e.g. `com.example.myapp.property`. This ensures property names are
/// suitably namespaced for each application and reduces the risk of
/// clashes.
///
/// Note that this endpoint is not necessarily handled by the homeserver,
/// but by another webserver, to be used for discovering the homeserver URL.
///
/// The result of this call is stored in [wellKnown] for later use at runtime.
@override
Future<DiscoveryInformation> getWellknown() async {
final wellKnown = await super.getWellknown();
// do not reset the well known here, so super call
super.homeserver = wellKnown.mHomeserver.baseUrl.stripTrailingSlash();
_wellKnown = wellKnown;
await database?.storeWellKnown(wellKnown);
return wellKnown;
}
/// Checks to see if a username is available, and valid, for the server. /// Checks to see if a username is available, and valid, for the server.
/// Returns the fully-qualified Matrix user ID (MXID) that has been registered. /// Returns the fully-qualified Matrix user ID (MXID) that has been registered.
/// You have to call [checkHomeserver] first to set a homeserver. /// You have to call [checkHomeserver] first to set a homeserver.
@ -1196,7 +1234,7 @@ class Client extends MatrixApi {
path = '_matrix/media/v3/config'; path = '_matrix/media/v3/config';
} }
final requestUri = Uri(path: path); final requestUri = Uri(path: path);
final request = Request('GET', baseUri!.resolveUri(requestUri)); final request = http.Request('GET', baseUri!.resolveUri(requestUri));
request.headers['authorization'] = 'Bearer ${bearerToken!}'; request.headers['authorization'] = 'Bearer ${bearerToken!}';
final response = await httpClient.send(request); final response = await httpClient.send(request);
final responseBody = await response.stream.toBytes(); final responseBody = await response.stream.toBytes();
@ -1236,7 +1274,7 @@ class Client extends MatrixApi {
// removed with msc3916, so just to be explicit // removed with msc3916, so just to be explicit
'allow_remote': allowRemote.toString(), 'allow_remote': allowRemote.toString(),
}); });
final request = Request('GET', baseUri!.resolveUri(requestUri)); final request = http.Request('GET', baseUri!.resolveUri(requestUri));
request.headers['authorization'] = 'Bearer ${bearerToken!}'; request.headers['authorization'] = 'Bearer ${bearerToken!}';
final response = await httpClient.send(request); final response = await httpClient.send(request);
final responseBody = await response.stream.toBytes(); final responseBody = await response.stream.toBytes();
@ -1279,7 +1317,7 @@ class Client extends MatrixApi {
// removed with msc3916, so just to be explicit // removed with msc3916, so just to be explicit
'allow_remote': allowRemote.toString(), 'allow_remote': allowRemote.toString(),
}); });
final request = Request('GET', baseUri!.resolveUri(requestUri)); final request = http.Request('GET', baseUri!.resolveUri(requestUri));
request.headers['authorization'] = 'Bearer ${bearerToken!}'; request.headers['authorization'] = 'Bearer ${bearerToken!}';
final response = await httpClient.send(request); final response = await httpClient.send(request);
final responseBody = await response.stream.toBytes(); final responseBody = await response.stream.toBytes();
@ -1315,7 +1353,7 @@ class Client extends MatrixApi {
'url': url.toString(), 'url': url.toString(),
if (ts != null) 'ts': ts.toString(), if (ts != null) 'ts': ts.toString(),
}); });
final request = Request('GET', baseUri!.resolveUri(requestUri)); final request = http.Request('GET', baseUri!.resolveUri(requestUri));
request.headers['authorization'] = 'Bearer ${bearerToken!}'; request.headers['authorization'] = 'Bearer ${bearerToken!}';
final response = await httpClient.send(request); final response = await httpClient.send(request);
final responseBody = await response.stream.toBytes(); final responseBody = await response.stream.toBytes();
@ -1369,7 +1407,7 @@ class Client extends MatrixApi {
'allow_remote': allowRemote.toString(), 'allow_remote': allowRemote.toString(),
}); });
final request = Request('GET', baseUri!.resolveUri(requestUri)); final request = http.Request('GET', baseUri!.resolveUri(requestUri));
request.headers['authorization'] = 'Bearer ${bearerToken!}'; request.headers['authorization'] = 'Bearer ${bearerToken!}';
final response = await httpClient.send(request); final response = await httpClient.send(request);
final responseBody = await response.stream.toBytes(); final responseBody = await response.stream.toBytes();
@ -1956,12 +1994,16 @@ class Client extends MatrixApi {
_accountData = data; _accountData = data;
_updatePushrules(); _updatePushrules();
}); });
_discoveryDataLoading = database.getWellKnown().then((data) {
_wellKnown = data;
});
// ignore: deprecated_member_use_from_same_package // ignore: deprecated_member_use_from_same_package
presences.clear(); presences.clear();
if (waitUntilLoadCompletedLoaded) { if (waitUntilLoadCompletedLoaded) {
await userDeviceKeysLoading; await userDeviceKeysLoading;
await roomsLoading; await roomsLoading;
await _accountDataLoading; await _accountDataLoading;
await _discoveryDataLoading;
} }
} }
_initLock = false; _initLock = false;
@ -2784,10 +2826,13 @@ class Client extends MatrixApi {
Future? userDeviceKeysLoading; Future? userDeviceKeysLoading;
Future? roomsLoading; Future? roomsLoading;
Future? _accountDataLoading; Future? _accountDataLoading;
Future? _discoveryDataLoading;
Future? firstSyncReceived; Future? firstSyncReceived;
Future? get accountDataLoading => _accountDataLoading; Future? get accountDataLoading => _accountDataLoading;
Future? get wellKnownLoading => _discoveryDataLoading;
/// A map of known device keys per user. /// A map of known device keys per user.
Map<String, DeviceKeysList> get userDeviceKeys => _userDeviceKeys; Map<String, DeviceKeysList> get userDeviceKeys => _userDeviceKeys;
Map<String, DeviceKeysList> _userDeviceKeys = {}; Map<String, DeviceKeysList> _userDeviceKeys = {};
@ -3593,6 +3638,7 @@ class SdkError {
class SyncConnectionException implements Exception { class SyncConnectionException implements Exception {
final Object originalException; final Object originalException;
SyncConnectionException(this.originalException); SyncConnectionException(this.originalException);
} }

View File

@ -27,7 +27,9 @@ import 'package:matrix/src/utils/queued_to_device_event.dart';
abstract class DatabaseApi { abstract class DatabaseApi {
int get maxFileSize => 1 * 1024 * 1024; int get maxFileSize => 1 * 1024 * 1024;
bool get supportsFileStoring => false; bool get supportsFileStoring => false;
Future<Map<String, dynamic>?> getClient(String name); Future<Map<String, dynamic>?> getClient(String name);
Future updateClient( Future updateClient(
@ -334,6 +336,10 @@ abstract class DatabaseApi {
Future<CachedPresence?> getPresence(String userId); Future<CachedPresence?> getPresence(String userId);
Future<void> storeWellKnown(DiscoveryInformation? discoveryInformation);
Future<DiscoveryInformation?> getWellKnown();
/// Deletes the whole database. The database needs to be created again after /// Deletes the whole database. The database needs to be created again after
/// this. /// this.
Future<void> delete(); Future<void> delete();

View File

@ -1595,6 +1595,25 @@ class HiveCollectionsDatabase extends DatabaseApi {
} }
} }
@override
Future<void> storeWellKnown(DiscoveryInformation? discoveryInformation) {
if (discoveryInformation == null) {
return _clientBox.delete('discovery_information');
}
return _clientBox.put(
'discovery_information',
jsonEncode(discoveryInformation.toJson()),
);
}
@override
Future<DiscoveryInformation?> getWellKnown() async {
final rawDiscoveryInformation =
await _clientBox.get('discovery_information');
if (rawDiscoveryInformation == null) return null;
return DiscoveryInformation.fromJson(jsonDecode(rawDiscoveryInformation));
}
@override @override
Future<void> delete() => _collection.deleteFromDisk(); Future<void> delete() => _collection.deleteFromDisk();

View File

@ -1401,6 +1401,25 @@ class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin {
throw UnimplementedError(); throw UnimplementedError();
} }
@override
Future<void> storeWellKnown(DiscoveryInformation? discoveryInformation) {
if (discoveryInformation == null) {
return _clientBox.delete('discovery_information');
}
return _clientBox.put(
'discovery_information',
jsonEncode(discoveryInformation.toJson()),
);
}
@override
Future<DiscoveryInformation?> getWellKnown() async {
final rawDiscoveryInformation =
await _clientBox.get('discovery_information');
if (rawDiscoveryInformation == null) return null;
return DiscoveryInformation.fromJson(jsonDecode(rawDiscoveryInformation));
}
@override @override
Future<void> delete() => Hive.deleteFromDisk(); Future<void> delete() => Hive.deleteFromDisk();

View File

@ -1623,6 +1623,25 @@ class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
return CachedPresence.fromJson(copyMap(rawPresence)); return CachedPresence.fromJson(copyMap(rawPresence));
} }
@override
Future<void> storeWellKnown(DiscoveryInformation? discoveryInformation) {
if (discoveryInformation == null) {
return _clientBox.delete('discovery_information');
}
return _clientBox.put(
'discovery_information',
jsonEncode(discoveryInformation.toJson()),
);
}
@override
Future<DiscoveryInformation?> getWellKnown() async {
final rawDiscoveryInformation =
await _clientBox.get('discovery_information');
if (rawDiscoveryInformation == null) return null;
return DiscoveryInformation.fromJson(jsonDecode(rawDiscoveryInformation));
}
@override @override
Future<void> delete() async { Future<void> delete() async {
// database?.path is null on web // database?.path is null on web

View File

@ -175,6 +175,7 @@ void main() {
matrix.userDeviceKeys['@alice:example.com']?.deviceKeys['JLAFKJWSCS'] matrix.userDeviceKeys['@alice:example.com']?.deviceKeys['JLAFKJWSCS']
?.verified, ?.verified,
false); false);
expect(matrix.wellKnown, isNull);
await matrix.handleSync(SyncUpdate.fromJson({ await matrix.handleSync(SyncUpdate.fromJson({
'next_batch': 'fakesync', 'next_batch': 'fakesync',
@ -1168,6 +1169,13 @@ void main() {
await client.dispose(closeDatabase: true); await client.dispose(closeDatabase: true);
}); });
test('wellKnown cache', () async {
final client = await getClient();
expect(client.wellKnown, null);
await client.getWellknown();
expect(client.wellKnown?.mHomeserver.baseUrl.host, 'matrix.example.com');
});
test('refreshAccessToken', () async { test('refreshAccessToken', () async {
final client = await getClient(); final client = await getClient();
expect(client.accessToken, 'abcd'); expect(client.accessToken, 'abcd');