feat(xep): Separate XEP-0030 and XEP-0115 support
Also, we now validate whatever we get in terms of disco#info and capability hashes before caching.
This commit is contained in:
parent
da591a552d
commit
1d87c0ce95
@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
- **BREAKING**: Remove `lastResource` from `XmppConnection`'s `connect` method. Instead, set the `StreamManagementNegotiator`'s `resource` attribute instead. Since the resource can only really be restored by stream management, this is no issue.
|
- **BREAKING**: Remove `lastResource` from `XmppConnection`'s `connect` method. Instead, set the `StreamManagementNegotiator`'s `resource` attribute instead. Since the resource can only really be restored by stream management, this is no issue.
|
||||||
- **BREAKING**: Changed order of parameters of `CryptographicHashManager.hashFromData`
|
- **BREAKING**: Changed order of parameters of `CryptographicHashManager.hashFromData`
|
||||||
|
- **BREAKING**: Removed support for XEP-0414, as the (supported) hash computations are already implemented by `CryptographicHashManager.hashFromData`.
|
||||||
|
- The `DiscoManager` now only handled entity capabilities if a `EntityCapabilityManager` is registered.
|
||||||
|
- The `EntityCapabilityManager` now verifies and validates its data before caching.
|
||||||
|
|
||||||
## 0.3.1
|
## 0.3.1
|
||||||
|
|
||||||
|
@ -85,7 +85,6 @@ export 'package:moxxmpp/src/xeps/xep_0388/errors.dart';
|
|||||||
export 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
|
export 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
|
||||||
export 'package:moxxmpp/src/xeps/xep_0388/user_agent.dart';
|
export 'package:moxxmpp/src/xeps/xep_0388/user_agent.dart';
|
||||||
export 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
|
export 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
|
||||||
export 'package:moxxmpp/src/xeps/xep_0414.dart';
|
|
||||||
export 'package:moxxmpp/src/xeps/xep_0424.dart';
|
export 'package:moxxmpp/src/xeps/xep_0424.dart';
|
||||||
export 'package:moxxmpp/src/xeps/xep_0444.dart';
|
export 'package:moxxmpp/src/xeps/xep_0444.dart';
|
||||||
export 'package:moxxmpp/src/xeps/xep_0446.dart';
|
export 'package:moxxmpp/src/xeps/xep_0446.dart';
|
||||||
|
@ -578,7 +578,12 @@ class XmppConnection {
|
|||||||
|
|
||||||
// Tell consumers of the event stream that we're done with stream feature
|
// Tell consumers of the event stream that we're done with stream feature
|
||||||
// negotiations
|
// negotiations
|
||||||
await _sendEvent(StreamNegotiationsDoneEvent());
|
await _sendEvent(
|
||||||
|
StreamNegotiationsDoneEvent(
|
||||||
|
getManagerById<StreamManagementManager>(smManager)?.streamResumed ??
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the connection state to [state] and triggers an event of type
|
/// Sets the connection state to [state] and triggers an event of type
|
||||||
|
@ -257,4 +257,10 @@ class NonRecoverableErrorEvent extends XmppEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Triggered when the stream negotiations are done.
|
/// Triggered when the stream negotiations are done.
|
||||||
class StreamNegotiationsDoneEvent extends XmppEvent {}
|
class StreamNegotiationsDoneEvent extends XmppEvent {
|
||||||
|
StreamNegotiationsDoneEvent(this.resumed);
|
||||||
|
|
||||||
|
/// Flag indicating whether we resumed a previous stream (true) or are in a completely
|
||||||
|
/// new stream (false).
|
||||||
|
final bool resumed;
|
||||||
|
}
|
||||||
|
5
packages/moxxmpp/lib/src/util/list.dart
Normal file
5
packages/moxxmpp/lib/src/util/list.dart
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
extension ListItemCountExtension<T> on List<T> {
|
||||||
|
int count(bool Function(T) matches) {
|
||||||
|
return where(matches).length;
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,7 @@ class DiscoCacheKey {
|
|||||||
const DiscoCacheKey(this.jid, this.node);
|
const DiscoCacheKey(this.jid, this.node);
|
||||||
|
|
||||||
/// The JID we're requesting disco data from.
|
/// The JID we're requesting disco data from.
|
||||||
|
// TODO(Unknown): Replace with JID
|
||||||
final String jid;
|
final String jid;
|
||||||
|
|
||||||
/// Optionally the node we are requesting from.
|
/// Optionally the node we are requesting from.
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
import 'package:moxxmpp/src/connection.dart';
|
|
||||||
import 'package:moxxmpp/src/events.dart';
|
import 'package:moxxmpp/src/events.dart';
|
||||||
import 'package:moxxmpp/src/jid.dart';
|
import 'package:moxxmpp/src/jid.dart';
|
||||||
import 'package:moxxmpp/src/managers/base.dart';
|
import 'package:moxxmpp/src/managers/base.dart';
|
||||||
@ -41,12 +40,6 @@ class DiscoManager extends XmppManagerBase {
|
|||||||
/// Disco identities that we advertise
|
/// Disco identities that we advertise
|
||||||
final List<Identity> _identities;
|
final List<Identity> _identities;
|
||||||
|
|
||||||
/// Map full JID to Capability hashes
|
|
||||||
final Map<String, CapabilityHashInfo> _capHashCache = {};
|
|
||||||
|
|
||||||
/// Map capability hash to the disco info
|
|
||||||
final Map<String, DiscoInfo> _capHashInfoCache = {};
|
|
||||||
|
|
||||||
/// Map full JID to Disco Info
|
/// Map full JID to Disco Info
|
||||||
final Map<DiscoCacheKey, DiscoInfo> _discoInfoCache = {};
|
final Map<DiscoCacheKey, DiscoInfo> _discoInfoCache = {};
|
||||||
|
|
||||||
@ -101,13 +94,7 @@ class DiscoManager extends XmppManagerBase {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onXmppEvent(XmppEvent event) async {
|
Future<void> onXmppEvent(XmppEvent event) async {
|
||||||
if (event is PresenceReceivedEvent) {
|
if (event is StreamNegotiationsDoneEvent) {
|
||||||
await _onPresence(event.jid, event.presence);
|
|
||||||
} else if (event is ConnectionStateChangedEvent) {
|
|
||||||
// TODO(Unknown): This handling is stupid. We should have an event that is
|
|
||||||
// triggered when we cannot guarantee that everything is as
|
|
||||||
// it was before.
|
|
||||||
if (event.state != XmppConnectionState.connected) return;
|
|
||||||
if (event.resumed) return;
|
if (event.resumed) return;
|
||||||
|
|
||||||
// Cancel all waiting requests
|
// Cancel all waiting requests
|
||||||
@ -135,6 +122,15 @@ class DiscoManager extends XmppManagerBase {
|
|||||||
_discoItemsCallbacks[node] = callback;
|
_discoItemsCallbacks[node] = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add a [DiscoCacheKey]-[DiscoInfo] pair [discoInfoEntry] to the internal cache.
|
||||||
|
Future<void> addCachedDiscoInfo(
|
||||||
|
MapEntry<DiscoCacheKey, DiscoInfo> discoInfoEntry,
|
||||||
|
) async {
|
||||||
|
await _cacheLock.synchronized(() {
|
||||||
|
_discoInfoCache[discoInfoEntry.key] = discoInfoEntry.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Adds a list of features to the possible disco info response.
|
/// Adds a list of features to the possible disco info response.
|
||||||
/// This function only adds features that are not already present in the disco features.
|
/// This function only adds features that are not already present in the disco features.
|
||||||
void addFeatures(List<String> features) {
|
void addFeatures(List<String> features) {
|
||||||
@ -155,39 +151,6 @@ class DiscoManager extends XmppManagerBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onPresence(JID from, Stanza presence) async {
|
|
||||||
final c = presence.firstTag('c', xmlns: capsXmlns);
|
|
||||||
if (c == null) return;
|
|
||||||
|
|
||||||
final info = CapabilityHashInfo(
|
|
||||||
c.attributes['ver']! as String,
|
|
||||||
c.attributes['node']! as String,
|
|
||||||
c.attributes['hash']! as String,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if we already know of that cache
|
|
||||||
var cached = false;
|
|
||||||
await _cacheLock.synchronized(() async {
|
|
||||||
if (!_capHashCache.containsKey(info.ver)) {
|
|
||||||
cached = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (cached) return;
|
|
||||||
|
|
||||||
// Request the cap hash
|
|
||||||
logger.finest(
|
|
||||||
"Received capability hash we don't know about. Requesting it...",
|
|
||||||
);
|
|
||||||
final result =
|
|
||||||
await discoInfoQuery(from.toString(), node: '${info.node}#${info.ver}');
|
|
||||||
if (result.isType<DiscoError>()) return;
|
|
||||||
|
|
||||||
await _cacheLock.synchronized(() async {
|
|
||||||
_capHashCache[from.toString()] = info;
|
|
||||||
_capHashInfoCache[info.ver] = result.get<DiscoInfo>();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the [DiscoInfo] object that would be used as the response to a disco#info
|
/// Returns the [DiscoInfo] object that would be used as the response to a disco#info
|
||||||
/// query against our bare JID with no node. The results node attribute is set
|
/// query against our bare JID with no node. The results node attribute is set
|
||||||
/// to [node].
|
/// to [node].
|
||||||
@ -269,10 +232,11 @@ class DiscoManager extends XmppManagerBase {
|
|||||||
Future<void> _exitDiscoInfoCriticalSection(
|
Future<void> _exitDiscoInfoCriticalSection(
|
||||||
DiscoCacheKey key,
|
DiscoCacheKey key,
|
||||||
Result<DiscoError, DiscoInfo> result,
|
Result<DiscoError, DiscoInfo> result,
|
||||||
|
bool shouldCache,
|
||||||
) async {
|
) async {
|
||||||
await _cacheLock.synchronized(() async {
|
await _cacheLock.synchronized(() async {
|
||||||
// Add to cache if it is a result
|
// Add to cache if it is a result
|
||||||
if (result.isType<DiscoInfo>()) {
|
if (result.isType<DiscoInfo>() && shouldCache) {
|
||||||
_discoInfoCache[key] = result.get<DiscoInfo>();
|
_discoInfoCache[key] = result.get<DiscoInfo>();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -280,14 +244,24 @@ class DiscoManager extends XmppManagerBase {
|
|||||||
await _discoInfoTracker.resolve(key, result);
|
await _discoInfoTracker.resolve(key, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends a disco info query to the (full) jid [entity], optionally with node=[node].
|
/// Send a disco#info query to [entity]. If [node] is specified, then the disco#info
|
||||||
|
/// request will be directed against that one node of [entity].
|
||||||
|
///
|
||||||
|
/// [shouldEncrypt] indicates to possible end-to-end encryption implementations whether
|
||||||
|
/// the request should be encrypted (true) or not (false).
|
||||||
|
///
|
||||||
|
/// [shouldCache] indicates whether the successful result of the disco#info query
|
||||||
|
/// should be cached (true) or not(false).
|
||||||
Future<Result<DiscoError, DiscoInfo>> discoInfoQuery(
|
Future<Result<DiscoError, DiscoInfo>> discoInfoQuery(
|
||||||
String entity, {
|
String entity, {
|
||||||
String? node,
|
String? node,
|
||||||
bool shouldEncrypt = true,
|
bool shouldEncrypt = true,
|
||||||
|
bool shouldCache = true,
|
||||||
}) async {
|
}) async {
|
||||||
final cacheKey = DiscoCacheKey(entity, node);
|
|
||||||
DiscoInfo? info;
|
DiscoInfo? info;
|
||||||
|
final cacheKey = DiscoCacheKey(entity, node);
|
||||||
|
final ecm = getAttributes()
|
||||||
|
.getManagerById<EntityCapabilitiesManager>(entityCapabilitiesManager);
|
||||||
final ffuture = await _cacheLock
|
final ffuture = await _cacheLock
|
||||||
.synchronized<Future<Future<Result<DiscoError, DiscoInfo>>?>?>(
|
.synchronized<Future<Future<Result<DiscoError, DiscoInfo>>?>?>(
|
||||||
() async {
|
() async {
|
||||||
@ -296,6 +270,14 @@ class DiscoManager extends XmppManagerBase {
|
|||||||
info = _discoInfoCache[cacheKey];
|
info = _discoInfoCache[cacheKey];
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
|
// Check if we know entity capabilities
|
||||||
|
if (ecm != null && node == null) {
|
||||||
|
info = await ecm.getCachedDiscoInfoFromJid(JID.fromString(entity));
|
||||||
|
if (info != null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return _discoInfoTracker.waitFor(cacheKey);
|
return _discoInfoTracker.waitFor(cacheKey);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -316,14 +298,14 @@ class DiscoManager extends XmppManagerBase {
|
|||||||
final query = stanza.firstTag('query');
|
final query = stanza.firstTag('query');
|
||||||
if (query == null) {
|
if (query == null) {
|
||||||
final result = Result<DiscoError, DiscoInfo>(InvalidResponseDiscoError());
|
final result = Result<DiscoError, DiscoInfo>(InvalidResponseDiscoError());
|
||||||
await _exitDiscoInfoCriticalSection(cacheKey, result);
|
await _exitDiscoInfoCriticalSection(cacheKey, result, shouldCache);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stanza.attributes['type'] == 'error') {
|
if (stanza.attributes['type'] == 'error') {
|
||||||
//final error = stanza.firstTag('error');
|
//final error = stanza.firstTag('error');
|
||||||
final result = Result<DiscoError, DiscoInfo>(ErrorResponseDiscoError());
|
final result = Result<DiscoError, DiscoInfo>(ErrorResponseDiscoError());
|
||||||
await _exitDiscoInfoCriticalSection(cacheKey, result);
|
await _exitDiscoInfoCriticalSection(cacheKey, result, shouldCache);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -333,7 +315,7 @@ class DiscoManager extends XmppManagerBase {
|
|||||||
JID.fromString(entity),
|
JID.fromString(entity),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await _exitDiscoInfoCriticalSection(cacheKey, result);
|
await _exitDiscoInfoCriticalSection(cacheKey, result, shouldCache);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,37 +1,35 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:cryptography/cryptography.dart';
|
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
import 'package:moxxmpp/src/events.dart';
|
||||||
|
import 'package:moxxmpp/src/jid.dart';
|
||||||
import 'package:moxxmpp/src/managers/base.dart';
|
import 'package:moxxmpp/src/managers/base.dart';
|
||||||
import 'package:moxxmpp/src/managers/namespaces.dart';
|
import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||||
import 'package:moxxmpp/src/namespaces.dart';
|
import 'package:moxxmpp/src/namespaces.dart';
|
||||||
import 'package:moxxmpp/src/presence.dart';
|
import 'package:moxxmpp/src/presence.dart';
|
||||||
import 'package:moxxmpp/src/rfcs/rfc_4790.dart';
|
import 'package:moxxmpp/src/rfcs/rfc_4790.dart';
|
||||||
import 'package:moxxmpp/src/stringxml.dart';
|
import 'package:moxxmpp/src/stringxml.dart';
|
||||||
|
import 'package:moxxmpp/src/util/list.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0004.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0030/cache.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
|
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
|
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0414.dart';
|
import 'package:moxxmpp/src/xeps/xep_0300.dart';
|
||||||
|
import 'package:synchronized/synchronized.dart';
|
||||||
|
|
||||||
@immutable
|
/// Given an identity [i], compute the string according to XEP-0115 § 5.1 step 2.
|
||||||
class CapabilityHashInfo {
|
String _identityString(Identity i) =>
|
||||||
const CapabilityHashInfo(this.ver, this.node, this.hash);
|
'${i.category}/${i.type}/${i.lang ?? ""}/${i.name ?? ""}';
|
||||||
final String ver;
|
|
||||||
final String node;
|
|
||||||
final String hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculates the Entitiy Capability hash according to XEP-0115 based on the
|
/// Calculates the Entitiy Capability hash according to XEP-0115 based on the
|
||||||
/// disco information.
|
/// disco information.
|
||||||
Future<String> calculateCapabilityHash(
|
Future<String> calculateCapabilityHash(
|
||||||
|
HashFunction algorithm,
|
||||||
DiscoInfo info,
|
DiscoInfo info,
|
||||||
HashAlgorithm algorithm,
|
|
||||||
) async {
|
) async {
|
||||||
final buffer = StringBuffer();
|
final buffer = StringBuffer();
|
||||||
final identitiesSorted = info.identities
|
final identitiesSorted = info.identities.map(_identityString).toList();
|
||||||
.map(
|
|
||||||
(Identity i) =>
|
|
||||||
'${i.category}/${i.type}/${i.lang ?? ""}/${i.name ?? ""}',
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
// ignore: cascade_invocations
|
// ignore: cascade_invocations
|
||||||
identitiesSorted.sort(ioctetSortComparator);
|
identitiesSorted.sort(ioctetSortComparator);
|
||||||
buffer.write('${identitiesSorted.join("<")}<');
|
buffer.write('${identitiesSorted.join("<")}<');
|
||||||
@ -72,8 +70,11 @@ Future<String> calculateCapabilityHash(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return base64
|
final rawHash = await CryptographicHashManager.hashFromData(
|
||||||
.encode((await algorithm.hash(utf8.encode(buffer.toString()))).bytes);
|
algorithm,
|
||||||
|
utf8.encode(buffer.toString()),
|
||||||
|
);
|
||||||
|
return base64.encode(rawHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A manager implementing the advertising of XEP-0115. It responds to the
|
/// A manager implementing the advertising of XEP-0115. It responds to the
|
||||||
@ -91,6 +92,15 @@ class EntityCapabilitiesManager extends XmppManagerBase {
|
|||||||
/// The cached capability hash.
|
/// The cached capability hash.
|
||||||
String? _capabilityHash;
|
String? _capabilityHash;
|
||||||
|
|
||||||
|
/// Cache the mapping between the full JID and the capability hash string.
|
||||||
|
final Map<String, String> _jidToCapHashCache = {};
|
||||||
|
|
||||||
|
/// Cache the mapping between capability hash string and the resulting disco#info.
|
||||||
|
final Map<String, DiscoInfo> _capHashCache = {};
|
||||||
|
|
||||||
|
/// A lock guarding access to the capability hash cache.
|
||||||
|
final Lock _cacheLock = Lock();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> isSupported() async => true;
|
Future<bool> isSupported() async => true;
|
||||||
|
|
||||||
@ -101,10 +111,10 @@ class EntityCapabilitiesManager extends XmppManagerBase {
|
|||||||
/// the DiscoManager.
|
/// the DiscoManager.
|
||||||
Future<String> getCapabilityHash() async {
|
Future<String> getCapabilityHash() async {
|
||||||
_capabilityHash ??= await calculateCapabilityHash(
|
_capabilityHash ??= await calculateCapabilityHash(
|
||||||
|
HashFunction.sha1,
|
||||||
getAttributes()
|
getAttributes()
|
||||||
.getManagerById<DiscoManager>(discoManager)!
|
.getManagerById<DiscoManager>(discoManager)!
|
||||||
.getDiscoInfo(null),
|
.getDiscoInfo(null),
|
||||||
getHashByName('sha-1')!,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return _capabilityHash!;
|
return _capabilityHash!;
|
||||||
@ -135,20 +145,198 @@ class EntityCapabilitiesManager extends XmppManagerBase {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// If we know of [jid]'s capability hash, look up the [DiscoInfo] associated with
|
||||||
|
/// that capability hash. If we don't know of [jid]'s capability hash, return null.
|
||||||
|
Future<DiscoInfo?> getCachedDiscoInfoFromJid(JID jid) async {
|
||||||
|
return _cacheLock.synchronized(() {
|
||||||
|
final capHash = _jidToCapHashCache[jid.toString()];
|
||||||
|
if (capHash == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _capHashCache[capHash];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
Future<void> onPresence(PresenceReceivedEvent event) async {
|
||||||
|
final c = event.presence.firstTag('c', xmlns: capsXmlns);
|
||||||
|
if (c == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final hashFunctionName = c.attributes['hash'] as String?;
|
||||||
|
final capabilityNode = c.attributes['node'] as String?;
|
||||||
|
final ver = c.attributes['ver'] as String?;
|
||||||
|
if (hashFunctionName == null || capabilityNode == null || ver == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we know of the hash
|
||||||
|
final isCached =
|
||||||
|
await _cacheLock.synchronized(() => _capHashCache.containsKey(ver));
|
||||||
|
if (isCached) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final dm = getAttributes().getManagerById<DiscoManager>(discoManager)!;
|
||||||
|
final discoRequest = await dm.discoInfoQuery(
|
||||||
|
event.jid.toString(),
|
||||||
|
node: capabilityNode,
|
||||||
|
);
|
||||||
|
if (discoRequest.isType<DiscoError>()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final discoInfo = discoRequest.get<DiscoInfo>();
|
||||||
|
|
||||||
|
final hashFunction = HashFunction.maybeFromName(hashFunctionName);
|
||||||
|
if (hashFunction == null) {
|
||||||
|
await dm.addCachedDiscoInfo(
|
||||||
|
MapEntry<DiscoCacheKey, DiscoInfo>(
|
||||||
|
DiscoCacheKey(
|
||||||
|
event.jid.toString(),
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
discoInfo,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the disco#info result according to XEP-0115 § 5.4
|
||||||
|
// > If the response includes more than one service discovery identity with the
|
||||||
|
// > same category/type/lang/name, consider the entire response to be ill-formed.
|
||||||
|
for (final identity in discoInfo.identities) {
|
||||||
|
final identityString = _identityString(identity);
|
||||||
|
if (discoInfo.identities
|
||||||
|
.count((i) => _identityString(i) == identityString) >
|
||||||
|
1) {
|
||||||
|
logger.warning(
|
||||||
|
'Malformed disco#info response: More than one equal identity',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// > If the response includes more than one service discovery feature with the same
|
||||||
|
// > XML character data, consider the entire response to be ill-formed.
|
||||||
|
for (final feature in discoInfo.features) {
|
||||||
|
if (discoInfo.features.count((f) => f == feature) > 1) {
|
||||||
|
logger.warning(
|
||||||
|
'Malformed disco#info response: More than one equal feature',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// > If the response includes more than one extended service discovery information
|
||||||
|
// > form with the same FORM_TYPE or the FORM_TYPE field contains more than one
|
||||||
|
// > <value/> element with different XML character data, consider the entire response
|
||||||
|
// > to be ill-formed.
|
||||||
|
// >
|
||||||
|
// > If the response includes an extended service discovery information form where
|
||||||
|
// > the FORM_TYPE field is not of type "hidden" or the form does not include a
|
||||||
|
// > FORM_TYPE field, ignore the form but continue processing.
|
||||||
|
final validExtendedInfoItems = List<DataForm>.empty(growable: true);
|
||||||
|
for (final extendedInfo in discoInfo.extendedInfo) {
|
||||||
|
final formType = extendedInfo.getFieldByVar('FORM_TYPE');
|
||||||
|
|
||||||
|
// Form should have a FORM_TYPE field
|
||||||
|
if (formType == null) {
|
||||||
|
logger.fine('Skipping extended info as it contains no FORM_TYPE field');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we only have one unique FORM_TYPE value
|
||||||
|
if (formType.values.length > 1) {
|
||||||
|
if (Set<String>.from(formType.values).length > 1) {
|
||||||
|
logger.warning(
|
||||||
|
'Malformed disco#info response: Extended Info FORM_TYPE contains more than one value(s) of different value.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have more than one extended info forms of the same type
|
||||||
|
final sameFormTypeFormsNumber = discoInfo.extendedInfo.count((form) {
|
||||||
|
final type = form.getFieldByVar('FORM_TYPE')?.values.first;
|
||||||
|
if (type == null) return false;
|
||||||
|
|
||||||
|
return type == formType.values.first;
|
||||||
|
});
|
||||||
|
if (sameFormTypeFormsNumber > 1) {
|
||||||
|
logger.warning(
|
||||||
|
'Malformed disco#info response: More than one Extended Disco Info forms with the same FORM_TYPE value',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the field type is hidden
|
||||||
|
if (formType.type != 'hidden') {
|
||||||
|
logger.fine(
|
||||||
|
'Skipping extended info as the FORM_TYPE field is not of type "hidden"',
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
validExtendedInfoItems.add(extendedInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the capability hash
|
||||||
|
final newDiscoInfo = DiscoInfo(
|
||||||
|
discoInfo.features,
|
||||||
|
discoInfo.identities,
|
||||||
|
validExtendedInfoItems,
|
||||||
|
discoInfo.node,
|
||||||
|
discoInfo.jid,
|
||||||
|
);
|
||||||
|
final computedCapabilityHash = await calculateCapabilityHash(
|
||||||
|
hashFunction,
|
||||||
|
newDiscoInfo,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (computedCapabilityHash == ver) {
|
||||||
|
await _cacheLock.synchronized(() {
|
||||||
|
_jidToCapHashCache[event.jid.toString()] = ver;
|
||||||
|
_capHashCache[ver] = newDiscoInfo;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.warning(
|
||||||
|
'Capability hash mismatch from ${event.jid}: Received "$ver", expected "$computedCapabilityHash".',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
void injectIntoCache(JID jid, String ver, DiscoInfo info) {
|
||||||
|
_jidToCapHashCache[jid.toString()] = ver;
|
||||||
|
_capHashCache[ver] = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onXmppEvent(XmppEvent event) async {
|
||||||
|
if (event is PresenceReceivedEvent) {
|
||||||
|
unawaited(onPresence(event));
|
||||||
|
} else if (event is StreamNegotiationsDoneEvent) {
|
||||||
|
// Clear the JID to cap. hash mapping.
|
||||||
|
await _cacheLock.synchronized(_jidToCapHashCache.clear);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> postRegisterCallback() async {
|
Future<void> postRegisterCallback() async {
|
||||||
await super.postRegisterCallback();
|
await super.postRegisterCallback();
|
||||||
|
|
||||||
getAttributes()
|
getAttributes()
|
||||||
.getManagerById<DiscoManager>(discoManager)!
|
.getManagerById<DiscoManager>(discoManager)
|
||||||
.registerInfoCallback(
|
?.registerInfoCallback(
|
||||||
await _getNode(),
|
await _getNode(),
|
||||||
_onInfoQuery,
|
_onInfoQuery,
|
||||||
);
|
);
|
||||||
|
|
||||||
getAttributes()
|
getAttributes()
|
||||||
.getManagerById<PresenceManager>(presenceManager)!
|
.getManagerById<PresenceManager>(presenceManager)
|
||||||
.registerPreSendCallback(
|
?.registerPreSendCallback(
|
||||||
_prePresenceSent,
|
_prePresenceSent,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import 'package:moxxmpp/src/namespaces.dart';
|
|||||||
import 'package:moxxmpp/src/stringxml.dart';
|
import 'package:moxxmpp/src/stringxml.dart';
|
||||||
|
|
||||||
/// Hash names
|
/// Hash names
|
||||||
|
const _hashSha1 = 'sha-1';
|
||||||
const _hashSha256 = 'sha-256';
|
const _hashSha256 = 'sha-256';
|
||||||
const _hashSha512 = 'sha-512';
|
const _hashSha512 = 'sha-512';
|
||||||
const _hashSha3256 = 'sha3-256';
|
const _hashSha3256 = 'sha3-256';
|
||||||
@ -23,6 +24,9 @@ XMLNode constructHashElement(HashFunction hash, String value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum HashFunction {
|
enum HashFunction {
|
||||||
|
/// SHA-1
|
||||||
|
sha1,
|
||||||
|
|
||||||
/// SHA-256
|
/// SHA-256
|
||||||
sha256,
|
sha256,
|
||||||
|
|
||||||
@ -46,6 +50,8 @@ enum HashFunction {
|
|||||||
/// - XEP-0300
|
/// - XEP-0300
|
||||||
factory HashFunction.fromName(String name) {
|
factory HashFunction.fromName(String name) {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
|
case _hashSha1:
|
||||||
|
return HashFunction.sha1;
|
||||||
case _hashSha256:
|
case _hashSha256:
|
||||||
return HashFunction.sha256;
|
return HashFunction.sha256;
|
||||||
case _hashSha512:
|
case _hashSha512:
|
||||||
@ -63,9 +69,33 @@ enum HashFunction {
|
|||||||
throw Exception();
|
throw Exception();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Like [HashFunction.fromName], but returns null if the hash function is unknown
|
||||||
|
static HashFunction? maybeFromName(String name) {
|
||||||
|
switch (name) {
|
||||||
|
case _hashSha1:
|
||||||
|
return HashFunction.sha1;
|
||||||
|
case _hashSha256:
|
||||||
|
return HashFunction.sha256;
|
||||||
|
case _hashSha512:
|
||||||
|
return HashFunction.sha512;
|
||||||
|
case _hashSha3256:
|
||||||
|
return HashFunction.sha3_256;
|
||||||
|
case _hashSha3512:
|
||||||
|
return HashFunction.sha3_512;
|
||||||
|
case _hashBlake2b256:
|
||||||
|
return HashFunction.blake2b256;
|
||||||
|
case _hashBlake2b512:
|
||||||
|
return HashFunction.blake2b512;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Return the hash function's name according to IANA's hash name register or XEP-0300.
|
/// Return the hash function's name according to IANA's hash name register or XEP-0300.
|
||||||
String toName() {
|
String toName() {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
|
case HashFunction.sha1:
|
||||||
|
return _hashSha1;
|
||||||
case HashFunction.sha256:
|
case HashFunction.sha256:
|
||||||
return _hashSha256;
|
return _hashSha256;
|
||||||
case HashFunction.sha512:
|
case HashFunction.sha512:
|
||||||
@ -88,6 +118,9 @@ class CryptographicHashManager extends XmppManagerBase {
|
|||||||
@override
|
@override
|
||||||
Future<bool> isSupported() async => true;
|
Future<bool> isSupported() async => true;
|
||||||
|
|
||||||
|
/// NOTE: We intentionally do not advertise support for SHA-1, as it is marked as
|
||||||
|
/// MUST NOT. Sha-1 support is only for providing a wrapper over its hash
|
||||||
|
/// function, for example for XEP-0115.
|
||||||
@override
|
@override
|
||||||
List<String> getDiscoFeatures() => [
|
List<String> getDiscoFeatures() => [
|
||||||
'$hashFunctionNameBaseXmlns:$_hashSha256',
|
'$hashFunctionNameBaseXmlns:$_hashSha256',
|
||||||
@ -107,6 +140,9 @@ class CryptographicHashManager extends XmppManagerBase {
|
|||||||
// TODO(PapaTutuWawa): Implement the others as well
|
// TODO(PapaTutuWawa): Implement the others as well
|
||||||
HashAlgorithm algo;
|
HashAlgorithm algo;
|
||||||
switch (function) {
|
switch (function) {
|
||||||
|
case HashFunction.sha1:
|
||||||
|
algo = Sha1();
|
||||||
|
break;
|
||||||
case HashFunction.sha256:
|
case HashFunction.sha256:
|
||||||
algo = Sha256();
|
algo = Sha256();
|
||||||
break;
|
break;
|
||||||
|
@ -22,6 +22,9 @@ class TestingManagerHolder {
|
|||||||
|
|
||||||
final Map<String, XmppManagerBase> _managers = {};
|
final Map<String, XmppManagerBase> _managers = {};
|
||||||
|
|
||||||
|
// The amount of stanzas sent
|
||||||
|
int sentStanzas = 0;
|
||||||
|
|
||||||
static final JID jid = JID.fromString('testuser@example.org/abc123');
|
static final JID jid = JID.fromString('testuser@example.org/abc123');
|
||||||
static final ConnectionSettings settings = ConnectionSettings(
|
static final ConnectionSettings settings = ConnectionSettings(
|
||||||
jid: jid,
|
jid: jid,
|
||||||
@ -36,6 +39,7 @@ class TestingManagerHolder {
|
|||||||
bool encrypted = false,
|
bool encrypted = false,
|
||||||
bool forceEncryption = false,
|
bool forceEncryption = false,
|
||||||
}) async {
|
}) async {
|
||||||
|
sentStanzas++;
|
||||||
return XMLNode.fromString('<iq />');
|
return XMLNode.fromString('<iq />');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import 'package:moxxmpp/src/xeps/xep_0030/cache.dart';
|
|||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
import '../helpers/logging.dart';
|
import '../helpers/logging.dart';
|
||||||
|
import '../helpers/manager.dart';
|
||||||
import '../helpers/xmpp.dart';
|
import '../helpers/xmpp.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
@ -114,4 +115,34 @@ void main() {
|
|||||||
expect(await result1, await result2);
|
expect(await result1, await result2);
|
||||||
expect(disco.infoTracker.hasTasksRunning(), false);
|
expect(disco.infoTracker.hasTasksRunning(), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('Interactions with Entity Capabilities', () {
|
||||||
|
test('Do not query when the capability hash is cached', () async {
|
||||||
|
final tm = TestingManagerHolder();
|
||||||
|
final ecm = EntityCapabilitiesManager('');
|
||||||
|
final dm = DiscoManager([]);
|
||||||
|
|
||||||
|
await tm.register(dm);
|
||||||
|
await tm.register(ecm);
|
||||||
|
|
||||||
|
// Inject a capability hash into the cache
|
||||||
|
final aliceJid = JID.fromString('alice@example.org/abc123');
|
||||||
|
ecm.injectIntoCache(
|
||||||
|
aliceJid,
|
||||||
|
'AAAAAAAAAAAAA',
|
||||||
|
DiscoInfo(
|
||||||
|
const [],
|
||||||
|
const [],
|
||||||
|
const [],
|
||||||
|
'',
|
||||||
|
aliceJid,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Query Alice's device
|
||||||
|
final result = await dm.discoInfoQuery(aliceJid.toString());
|
||||||
|
expect(result.isType<DiscoError>(), false);
|
||||||
|
expect(tm.sentStanzas, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ class StubbedDiscoManager extends DiscoManager {
|
|||||||
String entity, {
|
String entity, {
|
||||||
String? node,
|
String? node,
|
||||||
bool shouldEncrypt = true,
|
bool shouldEncrypt = true,
|
||||||
|
bool shouldCache = true,
|
||||||
}) async {
|
}) async {
|
||||||
final result = DiscoInfo.fromQuery(
|
final result = DiscoInfo.fromQuery(
|
||||||
XMLNode.fromString('''
|
XMLNode.fromString('''
|
||||||
|
@ -1,8 +1,151 @@
|
|||||||
import 'package:cryptography/cryptography.dart';
|
|
||||||
import 'package:moxxmpp/moxxmpp.dart';
|
import 'package:moxxmpp/moxxmpp.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
import '../helpers/logging.dart';
|
||||||
|
import '../helpers/manager.dart';
|
||||||
|
|
||||||
|
class StubbedDiscoManager extends DiscoManager {
|
||||||
|
StubbedDiscoManager() : super([]);
|
||||||
|
|
||||||
|
/// Inject an identity twice.
|
||||||
|
bool multipleEqualIdentities = false;
|
||||||
|
|
||||||
|
/// Inject a disco feature twice.
|
||||||
|
bool multipleEqualFeatures = false;
|
||||||
|
|
||||||
|
/// Inject the same (correct) extended info form twice.
|
||||||
|
bool multipleExtendedFormsWithSameType = false;
|
||||||
|
|
||||||
|
/// No FORM_TYPE
|
||||||
|
bool invalidExtension1 = false;
|
||||||
|
|
||||||
|
/// FORM_TYPE is not hidden
|
||||||
|
bool invalidExtension2 = false;
|
||||||
|
|
||||||
|
/// FORM_TYPE has more than one different values
|
||||||
|
bool invalidExtension3 = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Result<DiscoError, DiscoInfo>> discoInfoQuery(
|
||||||
|
String entity, {
|
||||||
|
String? node,
|
||||||
|
bool shouldEncrypt = true,
|
||||||
|
bool shouldCache = true,
|
||||||
|
}) async {
|
||||||
|
return Result(
|
||||||
|
DiscoInfo(
|
||||||
|
[
|
||||||
|
'http://jabber.org/protocol/caps',
|
||||||
|
'http://jabber.org/protocol/disco#info',
|
||||||
|
'http://jabber.org/protocol/disco#items',
|
||||||
|
'http://jabber.org/protocol/muc',
|
||||||
|
if (multipleEqualFeatures) 'http://jabber.org/protocol/muc',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
const Identity(
|
||||||
|
category: 'client',
|
||||||
|
type: 'pc',
|
||||||
|
name: 'Exodus 0.9.1',
|
||||||
|
),
|
||||||
|
if (multipleEqualIdentities)
|
||||||
|
const Identity(
|
||||||
|
category: 'client',
|
||||||
|
type: 'pc',
|
||||||
|
name: 'Exodus 0.9.1',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
if (multipleExtendedFormsWithSameType)
|
||||||
|
const DataForm(
|
||||||
|
type: 'result',
|
||||||
|
instructions: [],
|
||||||
|
fields: [
|
||||||
|
DataFormField(
|
||||||
|
options: [],
|
||||||
|
values: [
|
||||||
|
'http://jabber.org/network/serverinfo',
|
||||||
|
],
|
||||||
|
isRequired: false,
|
||||||
|
varAttr: 'FORM_TYPE',
|
||||||
|
type: 'hidden',
|
||||||
|
)
|
||||||
|
],
|
||||||
|
reported: [],
|
||||||
|
items: [],
|
||||||
|
),
|
||||||
|
if (multipleExtendedFormsWithSameType)
|
||||||
|
const DataForm(
|
||||||
|
type: 'result',
|
||||||
|
instructions: [],
|
||||||
|
fields: [
|
||||||
|
DataFormField(
|
||||||
|
options: [],
|
||||||
|
values: [
|
||||||
|
'http://jabber.org/network/serverinfo',
|
||||||
|
],
|
||||||
|
isRequired: false,
|
||||||
|
varAttr: 'FORM_TYPE',
|
||||||
|
type: 'hidden',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
reported: [],
|
||||||
|
items: [],
|
||||||
|
),
|
||||||
|
if (invalidExtension1)
|
||||||
|
const DataForm(
|
||||||
|
type: 'result',
|
||||||
|
instructions: [],
|
||||||
|
fields: [],
|
||||||
|
reported: [],
|
||||||
|
items: [],
|
||||||
|
),
|
||||||
|
if (invalidExtension2)
|
||||||
|
const DataForm(
|
||||||
|
type: 'result',
|
||||||
|
instructions: [],
|
||||||
|
fields: [
|
||||||
|
DataFormField(
|
||||||
|
options: [],
|
||||||
|
values: [
|
||||||
|
'http://jabber.org/network/serverinfo',
|
||||||
|
],
|
||||||
|
isRequired: false,
|
||||||
|
varAttr: 'FORM_TYPE',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
reported: [],
|
||||||
|
items: [],
|
||||||
|
),
|
||||||
|
if (invalidExtension3)
|
||||||
|
const DataForm(
|
||||||
|
type: 'result',
|
||||||
|
instructions: [],
|
||||||
|
fields: [
|
||||||
|
DataFormField(
|
||||||
|
options: [],
|
||||||
|
values: [
|
||||||
|
'http://jabber.org/network/serverinfo',
|
||||||
|
'http://jabber.org/network/better-serverinfo',
|
||||||
|
],
|
||||||
|
isRequired: false,
|
||||||
|
varAttr: 'FORM_TYPE',
|
||||||
|
type: 'hidden',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
reported: [],
|
||||||
|
items: [],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
null,
|
||||||
|
JID.fromString('some@user.local/test'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
initLogger();
|
||||||
|
|
||||||
test('Test XEP example', () async {
|
test('Test XEP example', () async {
|
||||||
final data = DiscoInfo(
|
final data = DiscoInfo(
|
||||||
const [
|
const [
|
||||||
@ -23,7 +166,7 @@ void main() {
|
|||||||
JID.fromString('some@user.local/test'),
|
JID.fromString('some@user.local/test'),
|
||||||
);
|
);
|
||||||
|
|
||||||
final hash = await calculateCapabilityHash(data, Sha1());
|
final hash = await calculateCapabilityHash(HashFunction.sha1, data);
|
||||||
expect(hash, 'QgayPKawpkPSDYmwT/WM94uAlu0=');
|
expect(hash, 'QgayPKawpkPSDYmwT/WM94uAlu0=');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -56,7 +199,7 @@ void main() {
|
|||||||
JID.fromString('some@user.local/test'),
|
JID.fromString('some@user.local/test'),
|
||||||
);
|
);
|
||||||
|
|
||||||
final hash = await calculateCapabilityHash(data, Sha1());
|
final hash = await calculateCapabilityHash(HashFunction.sha1, data);
|
||||||
expect(hash, 'q07IKJEyjvHSyhy//CH0CxmKi8w=');
|
expect(hash, 'q07IKJEyjvHSyhy//CH0CxmKi8w=');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -114,7 +257,7 @@ void main() {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
final hash = await calculateCapabilityHash(data, Sha1());
|
final hash = await calculateCapabilityHash(HashFunction.sha1, data);
|
||||||
expect(hash, "T7fOZrtBnV8sDA2fFTS59vyOyUs=");
|
expect(hash, "T7fOZrtBnV8sDA2fFTS59vyOyUs=");
|
||||||
*/
|
*/
|
||||||
});
|
});
|
||||||
@ -165,7 +308,291 @@ void main() {
|
|||||||
JID.fromString('user@server.local/test'),
|
JID.fromString('user@server.local/test'),
|
||||||
);
|
);
|
||||||
|
|
||||||
final hash = await calculateCapabilityHash(data, Sha1());
|
final hash = await calculateCapabilityHash(HashFunction.sha1, data);
|
||||||
expect(hash, 'zcIke+Rk13ah4d1pwDG7bEZsVwA=');
|
expect(hash, 'zcIke+Rk13ah4d1pwDG7bEZsVwA=');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('Receiving a capability hash', () {
|
||||||
|
final aliceJid = JID.fromString('alice@example.org/abc123');
|
||||||
|
|
||||||
|
test('Caching a correct capability hash', () async {
|
||||||
|
final tm = TestingManagerHolder();
|
||||||
|
final manager = EntityCapabilitiesManager('');
|
||||||
|
|
||||||
|
await tm.register(StubbedDiscoManager());
|
||||||
|
await tm.register(manager);
|
||||||
|
|
||||||
|
await manager.onPresence(
|
||||||
|
PresenceReceivedEvent(
|
||||||
|
aliceJid,
|
||||||
|
Stanza.presence(
|
||||||
|
from: aliceJid.toString(),
|
||||||
|
children: [
|
||||||
|
XMLNode.xmlns(
|
||||||
|
tag: 'c',
|
||||||
|
xmlns: capsXmlns,
|
||||||
|
attributes: {
|
||||||
|
'hash': 'sha-1',
|
||||||
|
'node': 'http://example.org/client',
|
||||||
|
'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await manager.getCachedDiscoInfoFromJid(aliceJid) != null,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Not caching an incorrect capability hash string', () async {
|
||||||
|
final tm = TestingManagerHolder();
|
||||||
|
final manager = EntityCapabilitiesManager('');
|
||||||
|
|
||||||
|
await tm.register(StubbedDiscoManager());
|
||||||
|
await tm.register(manager);
|
||||||
|
|
||||||
|
await manager.onPresence(
|
||||||
|
PresenceReceivedEvent(
|
||||||
|
aliceJid,
|
||||||
|
Stanza.presence(
|
||||||
|
from: aliceJid.toString(),
|
||||||
|
children: [
|
||||||
|
XMLNode.xmlns(
|
||||||
|
tag: 'c',
|
||||||
|
xmlns: capsXmlns,
|
||||||
|
attributes: {
|
||||||
|
'hash': 'sha-1',
|
||||||
|
'node': 'http://example.org/client',
|
||||||
|
'ver': 'QgayPKawpkPSDYmwT/WM94AAAAA=',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await manager.getCachedDiscoInfoFromJid(aliceJid),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Not caching ill-formed identities', () async {
|
||||||
|
final tm = TestingManagerHolder();
|
||||||
|
final manager = EntityCapabilitiesManager('');
|
||||||
|
|
||||||
|
await tm.register(
|
||||||
|
StubbedDiscoManager()..multipleEqualIdentities = true,
|
||||||
|
);
|
||||||
|
await tm.register(manager);
|
||||||
|
|
||||||
|
await manager.onPresence(
|
||||||
|
PresenceReceivedEvent(
|
||||||
|
aliceJid,
|
||||||
|
Stanza.presence(
|
||||||
|
from: aliceJid.toString(),
|
||||||
|
children: [
|
||||||
|
XMLNode.xmlns(
|
||||||
|
tag: 'c',
|
||||||
|
xmlns: capsXmlns,
|
||||||
|
attributes: {
|
||||||
|
'hash': 'sha-1',
|
||||||
|
'node': 'http://example.org/client',
|
||||||
|
'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await manager.getCachedDiscoInfoFromJid(aliceJid),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Not caching ill-formed features', () async {
|
||||||
|
final tm = TestingManagerHolder();
|
||||||
|
final manager = EntityCapabilitiesManager('');
|
||||||
|
|
||||||
|
await tm.register(
|
||||||
|
StubbedDiscoManager()..multipleEqualFeatures = true,
|
||||||
|
);
|
||||||
|
await tm.register(manager);
|
||||||
|
|
||||||
|
await manager.onPresence(
|
||||||
|
PresenceReceivedEvent(
|
||||||
|
aliceJid,
|
||||||
|
Stanza.presence(
|
||||||
|
from: aliceJid.toString(),
|
||||||
|
children: [
|
||||||
|
XMLNode.xmlns(
|
||||||
|
tag: 'c',
|
||||||
|
xmlns: capsXmlns,
|
||||||
|
attributes: {
|
||||||
|
'hash': 'sha-1',
|
||||||
|
'node': 'http://example.org/client',
|
||||||
|
'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await manager.getCachedDiscoInfoFromJid(aliceJid),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Not caching multiple forms with equal FORM_TYPE', () async {
|
||||||
|
final tm = TestingManagerHolder();
|
||||||
|
final manager = EntityCapabilitiesManager('');
|
||||||
|
|
||||||
|
await tm.register(
|
||||||
|
StubbedDiscoManager()..multipleExtendedFormsWithSameType = true,
|
||||||
|
);
|
||||||
|
await tm.register(manager);
|
||||||
|
|
||||||
|
await manager.onPresence(
|
||||||
|
PresenceReceivedEvent(
|
||||||
|
aliceJid,
|
||||||
|
Stanza.presence(
|
||||||
|
from: aliceJid.toString(),
|
||||||
|
children: [
|
||||||
|
XMLNode.xmlns(
|
||||||
|
tag: 'c',
|
||||||
|
xmlns: capsXmlns,
|
||||||
|
attributes: {
|
||||||
|
'hash': 'sha-1',
|
||||||
|
'node': 'http://example.org/client',
|
||||||
|
'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await manager.getCachedDiscoInfoFromJid(aliceJid),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Caching without invalid form (no FORM_TYPE)', () async {
|
||||||
|
final tm = TestingManagerHolder();
|
||||||
|
final manager = EntityCapabilitiesManager('');
|
||||||
|
|
||||||
|
await tm.register(
|
||||||
|
StubbedDiscoManager()..invalidExtension1 = true,
|
||||||
|
);
|
||||||
|
await tm.register(manager);
|
||||||
|
|
||||||
|
await manager.onPresence(
|
||||||
|
PresenceReceivedEvent(
|
||||||
|
aliceJid,
|
||||||
|
Stanza.presence(
|
||||||
|
from: aliceJid.toString(),
|
||||||
|
children: [
|
||||||
|
XMLNode.xmlns(
|
||||||
|
tag: 'c',
|
||||||
|
xmlns: capsXmlns,
|
||||||
|
attributes: {
|
||||||
|
'hash': 'sha-1',
|
||||||
|
'node': 'http://example.org/client',
|
||||||
|
'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final cachedItem = await manager.getCachedDiscoInfoFromJid(aliceJid);
|
||||||
|
expect(
|
||||||
|
cachedItem != null,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(cachedItem!.extendedInfo.isEmpty, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Caching without invalid form (FORM_TYPE not hidden)', () async {
|
||||||
|
final tm = TestingManagerHolder();
|
||||||
|
final manager = EntityCapabilitiesManager('');
|
||||||
|
|
||||||
|
await tm.register(
|
||||||
|
StubbedDiscoManager()..invalidExtension2 = true,
|
||||||
|
);
|
||||||
|
await tm.register(manager);
|
||||||
|
|
||||||
|
await manager.onPresence(
|
||||||
|
PresenceReceivedEvent(
|
||||||
|
aliceJid,
|
||||||
|
Stanza.presence(
|
||||||
|
from: aliceJid.toString(),
|
||||||
|
children: [
|
||||||
|
XMLNode.xmlns(
|
||||||
|
tag: 'c',
|
||||||
|
xmlns: capsXmlns,
|
||||||
|
attributes: {
|
||||||
|
'hash': 'sha-1',
|
||||||
|
'node': 'http://example.org/client',
|
||||||
|
'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final cachedItem = await manager.getCachedDiscoInfoFromJid(aliceJid);
|
||||||
|
expect(
|
||||||
|
cachedItem != null,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(cachedItem!.extendedInfo.isEmpty, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Not caching as FORM_TYPE's values are distinct", () async {
|
||||||
|
final tm = TestingManagerHolder();
|
||||||
|
final manager = EntityCapabilitiesManager('');
|
||||||
|
|
||||||
|
await tm.register(
|
||||||
|
StubbedDiscoManager()..invalidExtension3 = true,
|
||||||
|
);
|
||||||
|
await tm.register(manager);
|
||||||
|
|
||||||
|
await manager.onPresence(
|
||||||
|
PresenceReceivedEvent(
|
||||||
|
aliceJid,
|
||||||
|
Stanza.presence(
|
||||||
|
from: aliceJid.toString(),
|
||||||
|
children: [
|
||||||
|
XMLNode.xmlns(
|
||||||
|
tag: 'c',
|
||||||
|
xmlns: capsXmlns,
|
||||||
|
attributes: {
|
||||||
|
'hash': 'sha-1',
|
||||||
|
'node': 'http://example.org/client',
|
||||||
|
'ver': 'QgayPKawpkPSDYmwT/WM94uAlu0=',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await manager.getCachedDiscoInfoFromJid(aliceJid),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user