feat: Allow easier responding to incoming stanzas

Should fix #20.
This commit is contained in:
PapaTutuWawa 2023-01-23 12:47:30 +01:00
parent a01022c217
commit c9c45baabc
15 changed files with 269 additions and 247 deletions

View File

@ -459,7 +459,7 @@ class XmppConnection {
/// If addId is true, then an 'id' attribute will be added to the stanza if [stanza] has
/// none.
// TODO(Unknown): if addId = false, the function crashes.
Future<XMLNode> sendStanza(Stanza stanza, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool awaitable = true, bool encrypted = false }) async {
Future<XMLNode> sendStanza(Stanza stanza, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool awaitable = true, bool encrypted = false, bool forceEncryption = false, }) async {
assert(implies(addId == false && stanza.id == null, !awaitable), 'Cannot await a stanza with no id');
// Add extra data in case it was not set
@ -490,6 +490,7 @@ class XmppConnection {
null,
stanza_,
encrypted: encrypted,
forceEncryption: forceEncryption,
),
);
_log.fine('Done');
@ -737,7 +738,7 @@ class XmppConnection {
),
);
if (!incomingHandlers.done) {
handleUnhandledStanza(this, incomingPreHandlers.stanza);
await handleUnhandledStanza(this, incomingPreHandlers);
}
}

View File

@ -1,10 +1,28 @@
import 'package:moxxmpp/src/connection.dart';
import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/stanza.dart';
bool handleUnhandledStanza(XmppConnection conn, Stanza stanza) {
if (stanza.type != 'error' && stanza.type != 'result') {
conn.sendStanza(stanza.errorReply('cancel', 'feature-not-implemented'));
}
/// Bounce a stanza if it was not handled by any manager. [conn] is the connection object
/// to use for sending the stanza. [data] is the StanzaHandlerData of the unhandled
/// stanza.
Future<void> handleUnhandledStanza(XmppConnection conn, StanzaHandlerData data) async {
if (data.stanza.type != 'error' && data.stanza.type != 'result') {
final stanza = data.stanza.copyWith(
to: data.stanza.from,
from: data.stanza.to,
type: 'error',
children: [
buildErrorElement(
'cancel',
'feature-not-implemented',
),
],
);
return true;
await conn.sendStanza(
stanza,
awaitable: false,
forceEncryption: data.encrypted,
);
}
}

View File

@ -23,7 +23,7 @@ class XmppManagerAttributes {
required this.getNegotiatorById,
});
/// Send a stanza whose response can be awaited.
final Future<XMLNode> Function(Stanza stanza, { StanzaFromType addFrom, bool addId, bool awaitable, bool encrypted}) sendStanza;
final Future<XMLNode> Function(Stanza stanza, { StanzaFromType addFrom, bool addId, bool awaitable, bool encrypted, bool forceEncryption}) sendStanza;
/// Send a nonza.
final void Function(XMLNode) sendNonza;

View File

@ -1,6 +1,7 @@
import 'package:logging/logging.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/managers/attributes.dart';
import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/stringxml.dart';
@ -79,4 +80,25 @@ abstract class XmppManagerBase {
return handled;
}
/// Sends a reply of the stanza in [data] with [type]. Replaces the original stanza's
/// children with [children].
///
/// Note that this function currently only accepts IQ stanzas.
Future<void> reply(StanzaHandlerData data, String type, List<XMLNode> children) async {
assert(data.stanza.tag == 'iq', 'Reply makes little sense for non-IQ stanzas');
final stanza = data.stanza.copyWith(
to: data.stanza.from,
from: data.stanza.to,
type: type,
children: children,
);
await getAttributes().sendStanza(
stanza,
awaitable: false,
forceEncryption: data.encrypted,
);
}
}

View File

@ -50,6 +50,11 @@ class StanzaHandlerData with _$StanzaHandlerData {
String? funCancellation,
// Whether the stanza was received encrypted
@Default(false) bool encrypted,
// If true, forces the encryption manager to encrypt to the JID, even if it
// would not normally. In the case of OMEMO: If shouldEncrypt returns false
// but forceEncryption is true, then the OMEMO manager will try to encrypt
// to the JID anyway.
@Default(false) bool forceEncryption,
// The stated type of encryption used, if any was used
ExplicitEncryptionType? encryptionType,
// Delayed Delivery

View File

@ -48,6 +48,11 @@ mixin _$StanzaHandlerData {
String? get funCancellation =>
throw _privateConstructorUsedError; // Whether the stanza was received encrypted
bool get encrypted =>
throw _privateConstructorUsedError; // If true, forces the encryption manager to encrypt to the JID, even if it
// would not normally. In the case of OMEMO: If shouldEncrypt returns false
// but forceEncryption is true, then the OMEMO manager will try to encrypt
// to the JID anyway.
bool get forceEncryption =>
throw _privateConstructorUsedError; // The stated type of encryption used, if any was used
ExplicitEncryptionType? get encryptionType =>
throw _privateConstructorUsedError; // Delayed Delivery
@ -94,6 +99,7 @@ abstract class $StanzaHandlerDataCopyWith<$Res> {
String? funReplacement,
String? funCancellation,
bool encrypted,
bool forceEncryption,
ExplicitEncryptionType? encryptionType,
DelayedDelivery? delayedDelivery,
Map<String, dynamic> other,
@ -132,6 +138,7 @@ class _$StanzaHandlerDataCopyWithImpl<$Res>
Object? funReplacement = freezed,
Object? funCancellation = freezed,
Object? encrypted = freezed,
Object? forceEncryption = freezed,
Object? encryptionType = freezed,
Object? delayedDelivery = freezed,
Object? other = freezed,
@ -213,6 +220,10 @@ class _$StanzaHandlerDataCopyWithImpl<$Res>
? _value.encrypted
: encrypted // ignore: cast_nullable_to_non_nullable
as bool,
forceEncryption: forceEncryption == freezed
? _value.forceEncryption
: forceEncryption // ignore: cast_nullable_to_non_nullable
as bool,
encryptionType: encryptionType == freezed
? _value.encryptionType
: encryptionType // ignore: cast_nullable_to_non_nullable
@ -271,6 +282,7 @@ abstract class _$$_StanzaHandlerDataCopyWith<$Res>
String? funReplacement,
String? funCancellation,
bool encrypted,
bool forceEncryption,
ExplicitEncryptionType? encryptionType,
DelayedDelivery? delayedDelivery,
Map<String, dynamic> other,
@ -311,6 +323,7 @@ class __$$_StanzaHandlerDataCopyWithImpl<$Res>
Object? funReplacement = freezed,
Object? funCancellation = freezed,
Object? encrypted = freezed,
Object? forceEncryption = freezed,
Object? encryptionType = freezed,
Object? delayedDelivery = freezed,
Object? other = freezed,
@ -392,6 +405,10 @@ class __$$_StanzaHandlerDataCopyWithImpl<$Res>
? _value.encrypted
: encrypted // ignore: cast_nullable_to_non_nullable
as bool,
forceEncryption: forceEncryption == freezed
? _value.forceEncryption
: forceEncryption // ignore: cast_nullable_to_non_nullable
as bool,
encryptionType: encryptionType == freezed
? _value.encryptionType
: encryptionType // ignore: cast_nullable_to_non_nullable
@ -442,6 +459,7 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
this.funReplacement,
this.funCancellation,
this.encrypted = false,
this.forceEncryption = false,
this.encryptionType,
this.delayedDelivery,
final Map<String, dynamic> other = const <String, dynamic>{},
@ -506,6 +524,13 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
@override
@JsonKey()
final bool encrypted;
// If true, forces the encryption manager to encrypt to the JID, even if it
// would not normally. In the case of OMEMO: If shouldEncrypt returns false
// but forceEncryption is true, then the OMEMO manager will try to encrypt
// to the JID anyway.
@override
@JsonKey()
final bool forceEncryption;
// The stated type of encryption used, if any was used
@override
final ExplicitEncryptionType? encryptionType;
@ -540,7 +565,7 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
@override
String toString() {
return 'StanzaHandlerData(done: $done, cancel: $cancel, cancelReason: $cancelReason, stanza: $stanza, retransmitted: $retransmitted, sims: $sims, sfs: $sfs, oob: $oob, stableId: $stableId, reply: $reply, chatState: $chatState, isCarbon: $isCarbon, deliveryReceiptRequested: $deliveryReceiptRequested, isMarkable: $isMarkable, fun: $fun, funReplacement: $funReplacement, funCancellation: $funCancellation, encrypted: $encrypted, encryptionType: $encryptionType, delayedDelivery: $delayedDelivery, other: $other, messageRetraction: $messageRetraction, lastMessageCorrectionSid: $lastMessageCorrectionSid, messageReactions: $messageReactions, stickerPackId: $stickerPackId)';
return 'StanzaHandlerData(done: $done, cancel: $cancel, cancelReason: $cancelReason, stanza: $stanza, retransmitted: $retransmitted, sims: $sims, sfs: $sfs, oob: $oob, stableId: $stableId, reply: $reply, chatState: $chatState, isCarbon: $isCarbon, deliveryReceiptRequested: $deliveryReceiptRequested, isMarkable: $isMarkable, fun: $fun, funReplacement: $funReplacement, funCancellation: $funCancellation, encrypted: $encrypted, forceEncryption: $forceEncryption, encryptionType: $encryptionType, delayedDelivery: $delayedDelivery, other: $other, messageRetraction: $messageRetraction, lastMessageCorrectionSid: $lastMessageCorrectionSid, messageReactions: $messageReactions, stickerPackId: $stickerPackId)';
}
@override
@ -572,6 +597,8 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
const DeepCollectionEquality()
.equals(other.funCancellation, funCancellation) &&
const DeepCollectionEquality().equals(other.encrypted, encrypted) &&
const DeepCollectionEquality()
.equals(other.forceEncryption, forceEncryption) &&
const DeepCollectionEquality()
.equals(other.encryptionType, encryptionType) &&
const DeepCollectionEquality()
@ -608,6 +635,7 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
const DeepCollectionEquality().hash(funReplacement),
const DeepCollectionEquality().hash(funCancellation),
const DeepCollectionEquality().hash(encrypted),
const DeepCollectionEquality().hash(forceEncryption),
const DeepCollectionEquality().hash(encryptionType),
const DeepCollectionEquality().hash(delayedDelivery),
const DeepCollectionEquality().hash(_other),
@ -641,6 +669,7 @@ abstract class _StanzaHandlerData implements StanzaHandlerData {
final String? funReplacement,
final String? funCancellation,
final bool encrypted,
final bool forceEncryption,
final ExplicitEncryptionType? encryptionType,
final DelayedDelivery? delayedDelivery,
final Map<String, dynamic> other,
@ -690,6 +719,11 @@ abstract class _StanzaHandlerData implements StanzaHandlerData {
String? get funCancellation;
@override // Whether the stanza was received encrypted
bool get encrypted;
@override // If true, forces the encryption manager to encrypt to the JID, even if it
// would not normally. In the case of OMEMO: If shouldEncrypt returns false
// but forceEncryption is true, then the OMEMO manager will try to encrypt
// to the JID anyway.
bool get forceEncryption;
@override // The stated type of encryption used, if any was used
ExplicitEncryptionType? get encryptionType;
@override // Delayed Delivery

View File

@ -161,7 +161,11 @@ class RosterManager extends XmppManagerBase {
),
);
await attrs.sendStanza(stanza.reply());
await reply(
state,
'result',
[],
);
return state.copyWith(done: true);
}

View File

@ -114,23 +114,13 @@ class Stanza extends XMLNode {
children: children ?? this.children,
);
}
Stanza reply({ List<XMLNode> children = const [] }) {
return copyWith(
from: attributes['to'] as String?,
to: attributes['from'] as String?,
type: tag == 'iq' ? 'result' : attributes['type'] as String?,
children: children,
);
}
Stanza errorReply(String type, String condition, { String? text }) {
return copyWith(
from: attributes['to'] as String?,
to: attributes['from'] as String?,
type: 'error',
children: [
XMLNode(
/// Build an <error /> element with a child <[condition] type="[type]" />. If [text]
/// is not null, then the condition element will contain a <text /> element with [text]
/// as the body.
XMLNode buildErrorElement(String type, String condition, { String? text }) {
return XMLNode(
tag: 'error',
attributes: <String, dynamic>{ 'type': type },
children: [
@ -144,10 +134,7 @@ class Stanza extends XMLNode {
text: text,
)
] : [],
)
],
)
),
],
);
}
}

View File

@ -20,7 +20,6 @@ import 'package:synchronized/synchronized.dart';
@immutable
class DiscoCacheKey {
const DiscoCacheKey(this.jid, this.node);
final String jid;
final String? node;
@ -35,7 +34,6 @@ class DiscoCacheKey {
}
class DiscoManager extends XmppManagerBase {
DiscoManager()
: _features = List.empty(growable: true),
_capHashCache = {},
@ -47,15 +45,19 @@ class DiscoManager extends XmppManagerBase {
/// Our features
final List<String> _features;
// Map full JID to Capability hashes
/// Map full JID to Capability hashes
final Map<String, CapabilityHashInfo> _capHashCache;
// Map capability hash to the disco info
/// 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;
// Mapping the full JID to a list of running requests
/// Mapping the full JID to a list of running requests
final Map<DiscoCacheKey, List<Completer<Result<DiscoError, DiscoInfo>>>> _runningInfoQueries;
// Cache lock
/// Cache lock
final Lock _cacheLock;
@visibleForTesting
@ -154,22 +156,19 @@ class DiscoManager extends XmppManagerBase {
if (stanza.type != 'get') return state;
final presence = getAttributes().getManagerById(presenceManager)! as PresenceManager;
final query = stanza.firstTag('query')!;
final query = stanza.firstTag('query', xmlns: discoInfoXmlns)!;
final node = query.attributes['node'] as String?;
final capHash = await presence.getCapabilityHash();
final isCapabilityNode = node == '${presence.capabilityHashNode}#$capHash';
if (!isCapabilityNode && node != null) {
await getAttributes().sendStanza(Stanza.iq(
to: stanza.from,
from: stanza.to,
id: stanza.id,
type: 'error',
children: [
await reply(
state,
'error',
[
XMLNode.xmlns(
tag: 'query',
// TODO(PapaTutuWawa): Why are we copying the xmlns?
xmlns: query.attributes['xmlns']! as String,
xmlns: discoInfoXmlns,
attributes: <String, String>{
'node': node
},
@ -185,16 +184,17 @@ class DiscoManager extends XmppManagerBase {
xmlns: fullStanzaXmlns,
)
],
)
),
],
)
,);
);
return state.copyWith(done: true);
}
await getAttributes().sendStanza(stanza.reply(
children: [
await reply(
state,
'result',
[
XMLNode.xmlns(
tag: 'query',
xmlns: discoInfoXmlns,
@ -214,7 +214,7 @@ class DiscoManager extends XmppManagerBase {
],
),
],
),);
);
return state.copyWith(done: true);
}
@ -222,20 +222,16 @@ class DiscoManager extends XmppManagerBase {
Future<StanzaHandlerData> _onDiscoItemsRequest(Stanza stanza, StanzaHandlerData state) async {
if (stanza.type != 'get') return state;
final query = stanza.firstTag('query')!;
final query = stanza.firstTag('query', xmlns: discoItemsXmlns)!;
if (query.attributes['node'] != null) {
// TODO(Unknown): Handle the node we specified for XEP-0115
await getAttributes().sendStanza(
Stanza.iq(
to: stanza.from,
from: stanza.to,
id: stanza.id,
type: 'error',
children: [
await reply(
state,
'error',
[
XMLNode.xmlns(
tag: 'query',
// TODO(PapaTutuWawa): Why copy the xmlns?
xmlns: query.attributes['xmlns']! as String,
xmlns: discoItemsXmlns,
attributes: <String, String>{
'node': query.attributes['node']! as String,
},
@ -253,21 +249,22 @@ class DiscoManager extends XmppManagerBase {
],
),
],
),
);
return state.copyWith(done: true);
}
await getAttributes().sendStanza(
stanza.reply(
children: [
await reply(
state,
'result',
[
XMLNode.xmlns(
tag: 'query',
xmlns: discoItemsXmlns,
),
],
),
);
return state.copyWith(done: true);
}

View File

@ -318,11 +318,12 @@ abstract class BaseOmemoManager extends XmppManagerBase {
}
final toJid = JID.fromString(stanza.to!).toBare();
if (!(await shouldEncryptStanza(toJid, stanza))) {
logger.finest('shouldEncryptStanza returned false for message to $toJid. Not encrypting.');
final shouldEncryptResult = await shouldEncryptStanza(toJid, stanza);
if (!shouldEncryptResult && !state.forceEncryption) {
logger.finest('Not encrypting stanza for $toJid: Both shouldEncryptStanza and forceEncryption are false.');
return state;
} else {
logger.finest('shouldEncryptStanza returned true for message to $toJid.');
logger.finest('Encrypting stanza for $toJid: shouldEncryptResult=$shouldEncryptResult, forceEncryption=${state.forceEncryption}');
}
final toEncrypt = List<XMLNode>.empty(growable: true);

View File

@ -1,50 +0,0 @@
import 'package:moxxmpp/moxxmpp.dart';
import 'package:test/test.dart';
void main() {
test('Make sure reply does not copy the children', () {
final stanza = Stanza.iq(
to: 'hallo',
from: 'world',
id: 'abc123',
type: 'get',
children: [
XMLNode(tag: 'test-tag'),
XMLNode(tag: 'test-tag2')
],
);
final reply = stanza.reply();
expect(reply.children, []);
expect(reply.type, 'result');
expect(reply.from, stanza.to);
expect(reply.to, stanza.from);
expect(reply.id, stanza.id);
});
test('Make sure reply includes the new children', () {
final stanza = Stanza.iq(
to: 'hallo',
from: 'world',
id: 'abc123',
type: 'get',
children: [
XMLNode(tag: 'test-tag'),
XMLNode(tag: 'test-tag2')
],
);
final reply = stanza.reply(
children: [
XMLNode.xmlns(
tag: 'test',
xmlns: 'test',
)
],
);
expect(reply.children.length, 1);
expect(reply.firstTag('test') != null, true);
});
}

View File

@ -17,7 +17,7 @@ Future<void> runOutgoingStanzaHandlers(StreamManagementManager man, Stanza stanz
XmppManagerAttributes mkAttributes(void Function(Stanza) callback) {
return XmppManagerAttributes(
sendStanza: (stanza, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool awaitable = true, bool encrypted = false }) async {
sendStanza: (stanza, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool awaitable = true, bool encrypted = false, bool forceEncryption = false, }) async {
callback(stanza);
return Stanza.message();

View File

@ -5,7 +5,7 @@ import '../helpers/xmpp.dart';
void main() {
test("Test if we're vulnerable against CVE-2020-26547 style vulnerabilities", () async {
final attributes = XmppManagerAttributes(
sendStanza: (stanza, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool retransmitted = false, bool awaitable = true, bool encrypted = false }) async {
sendStanza: (stanza, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool retransmitted = false, bool awaitable = true, bool encrypted = false, bool forceEncryption = false, }) async {
// ignore: avoid_print
print('==> ${stanza.toXml()}');
return XMLNode(tag: 'iq', attributes: { 'type': 'result' });

View File

@ -32,8 +32,9 @@ void main() {
test('Test setting the CSI state when CSI is unsupported', () {
var nonzaSent = false;
final csi = CSIManager();
csi.register(XmppManagerAttributes(
sendStanza: (_, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool retransmitted = false, bool awaitable = true, bool encrypted = false }) async => XMLNode(tag: 'hallo'),
csi.register(
XmppManagerAttributes(
sendStanza: (_, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool retransmitted = false, bool awaitable = true, bool encrypted = false, bool forceEncryption = false, }) async => XMLNode(tag: 'hallo'),
sendEvent: (event) {},
sendNonza: (nonza) {
nonzaSent = true;
@ -60,8 +61,9 @@ void main() {
});
test('Test setting the CSI state when CSI is supported', () {
final csi = CSIManager();
csi.register(XmppManagerAttributes(
sendStanza: (_, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool retransmitted = false, bool awaitable = true, bool encrypted = false }) async => XMLNode(tag: 'hallo'),
csi.register(
XmppManagerAttributes(
sendStanza: (_, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool retransmitted = false, bool awaitable = true, bool encrypted = false, bool forceEncryption = false, }) async => XMLNode(tag: 'hallo'),
sendEvent: (event) {},
sendNonza: (nonza) {
expect(nonza.attributes['xmlns'] == csiXmlns, true, reason: "Expected only nonzas with XMLNS '$csiXmlns'");
@ -78,7 +80,8 @@ void main() {
getFullJID: () => JID.fromString('some.user@example.server/aaaaa'),
getSocket: () => StubTCPSocket(play: []),
getConnection: () => XmppConnection(TestingReconnectionPolicy(), StubTCPSocket(play: [])),
),);
),
);
csi.setActive();
csi.setInactive();

View File

@ -9,7 +9,7 @@ Future<bool> testRosterManager(String bareJid, String resource, String stanzaStr
var eventTriggered = false;
final roster = RosterManager(TestingRosterStateManager('', []));
roster.register(XmppManagerAttributes(
sendStanza: (_, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool retransmitted = false, bool awaitable = true, bool encrypted = false }) async => XMLNode(tag: 'hallo'),
sendStanza: (_, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool retransmitted = false, bool awaitable = true, bool encrypted = false, bool forceEncryption = false, }) async => XMLNode(tag: 'hallo'),
sendEvent: (event) {
eventTriggered = true;
},
@ -310,7 +310,7 @@ void main() {
var eventTriggered = false;
final roster = RosterManager(TestingRosterStateManager('', []));
roster.register(XmppManagerAttributes(
sendStanza: (_, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool retransmitted = false, bool awaitable = true, bool encrypted = false }) async => XMLNode(tag: 'hallo'),
sendStanza: (_, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool retransmitted = false, bool awaitable = true, bool encrypted = false, bool forceEncryption = false, }) async => XMLNode(tag: 'hallo'),
sendEvent: (event) {
eventTriggered = true;
},