From ae643e700975372f14c894261d86e7e36e6326e9 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Fri, 11 Mar 2022 21:39:42 +0100 Subject: [PATCH] xmpp: Rework the stanza handler --- lib/service/managers/roster.dart | 2 +- lib/service/service.dart | 40 ++++--- lib/xmpp/connection.dart | 121 +++++++++++++++----- lib/xmpp/events.dart | 7 -- lib/xmpp/managers/base.dart | 27 ++--- lib/xmpp/managers/data.dart | 29 +++++ lib/xmpp/managers/handlers.dart | 45 ++++++-- lib/xmpp/managers/namespaces.dart | 6 + lib/xmpp/managers/priorities.dart | 0 lib/xmpp/message.dart | 161 +++------------------------ lib/xmpp/presence.dart | 9 +- lib/xmpp/roster.dart | 11 +- lib/xmpp/socket.dart | 5 +- lib/xmpp/xeps/xep_0030/xep_0030.dart | 19 ++-- lib/xmpp/xeps/xep_0054.dart | 14 ++- lib/xmpp/xeps/xep_0060.dart | 20 ++-- lib/xmpp/xeps/xep_0066.dart | 43 +++++++ lib/xmpp/xeps/xep_0184.dart | 61 ++++++++++ lib/xmpp/xeps/xep_0198/xep_0198.dart | 24 ++-- lib/xmpp/xeps/xep_0280.dart | 34 +++++- lib/xmpp/xeps/xep_0333.dart | 48 ++++++++ lib/xmpp/xeps/xep_0359.dart | 70 ++++++++++++ lib/xmpp/xeps/xep_0385.dart | 38 +++++++ lib/xmpp/xeps/xep_0447.dart | 33 ++++++ test/stanzahandler_test.dart | 42 +++++-- test/xeps/xep_0198_test.dart | 33 ++++-- test/xmpp_test.dart | 28 ++--- 27 files changed, 663 insertions(+), 307 deletions(-) create mode 100644 lib/xmpp/managers/data.dart create mode 100644 lib/xmpp/managers/priorities.dart diff --git a/lib/service/managers/roster.dart b/lib/service/managers/roster.dart index cde4d891..2e275a3d 100644 --- a/lib/service/managers/roster.dart +++ b/lib/service/managers/roster.dart @@ -5,7 +5,7 @@ import "package:moxxyv2/xmpp/roster.dart"; import "package:get_it/get_it.dart"; -class MoxxyRosterManger extends RosterManager { +class MoxxyRosterManager extends RosterManager { @override Future commitLastRosterVersion(String version) async { await GetIt.I.get().modifyXmppState((state) => state.copyWith( diff --git a/lib/service/service.dart b/lib/service/service.dart index b2c4e15d..351543d0 100644 --- a/lib/service/service.dart +++ b/lib/service/service.dart @@ -11,10 +11,16 @@ import "package:moxxyv2/xmpp/presence.dart"; import "package:moxxyv2/xmpp/message.dart"; import "package:moxxyv2/xmpp/managers/namespaces.dart"; import "package:moxxyv2/xmpp/xeps/xep_0054.dart"; -import "package:moxxyv2/xmpp/xeps/xep_0280.dart"; -import "package:moxxyv2/xmpp/xeps/xep_0352.dart"; import "package:moxxyv2/xmpp/xeps/xep_0060.dart"; +import "package:moxxyv2/xmpp/xeps/xep_0066.dart"; import "package:moxxyv2/xmpp/xeps/xep_0084.dart"; +import "package:moxxyv2/xmpp/xeps/xep_0184.dart"; +import "package:moxxyv2/xmpp/xeps/xep_0280.dart"; +import "package:moxxyv2/xmpp/xeps/xep_0333.dart"; +import "package:moxxyv2/xmpp/xeps/xep_0352.dart"; +import "package:moxxyv2/xmpp/xeps/xep_0359.dart"; +import "package:moxxyv2/xmpp/xeps/xep_0385.dart"; +import "package:moxxyv2/xmpp/xeps/xep_0447.dart"; import "package:moxxyv2/xmpp/xeps/xep_0030/cachemanager.dart"; import "package:moxxyv2/service/managers/roster.dart"; import "package:moxxyv2/service/managers/disco.dart"; @@ -209,17 +215,25 @@ void onStart() { GetIt.I.registerSingleton(RosterService(sendData: middleware)); final connection = XmppConnection(); - connection.registerManager(MoxxyStreamManagementManager()); - connection.registerManager(MoxxyDiscoManager()); - connection.registerManager(MessageManager()); - connection.registerManager(MoxxyRosterManger()); - connection.registerManager(PresenceManager()); - connection.registerManager(CSIManager()); - connection.registerManager(DiscoCacheManager()); - connection.registerManager(CarbonsManager()); - connection.registerManager(PubSubManager()); - connection.registerManager(vCardManager()); - connection.registerManager(UserAvatarManager()); + connection.registerManagers([ + MoxxyStreamManagementManager(), + MoxxyDiscoManager(), + MoxxyRosterManager(), + MessageManager(), + PresenceManager(), + CSIManager(), + DiscoCacheManager(), + CarbonsManager(), + PubSubManager(), + vCardManager(), + UserAvatarManager(), + StableIdManager(), + SIMSManager(), + MessageDeliveryReceiptManager(), + ChatMarkerManager(), + OOBManager(), + SFSManager() + ]); GetIt.I.registerSingleton(connection); final account = await xmpp.getAccountData(); diff --git a/lib/xmpp/connection.dart b/lib/xmpp/connection.dart index 938c165e..5be6c824 100644 --- a/lib/xmpp/connection.dart +++ b/lib/xmpp/connection.dart @@ -15,7 +15,9 @@ import "package:moxxyv2/xmpp/roster.dart"; import "package:moxxyv2/xmpp/sasl/authenticator.dart"; import "package:moxxyv2/xmpp/sasl/authenticators.dart"; import "package:moxxyv2/xmpp/managers/base.dart"; +import "package:moxxyv2/xmpp/managers/handlers.dart"; import "package:moxxyv2/xmpp/managers/attributes.dart"; +import "package:moxxyv2/xmpp/managers/data.dart"; import "package:moxxyv2/xmpp/managers/namespaces.dart"; import "package:moxxyv2/xmpp/xeps/xep_0030/xep_0030.dart"; import "package:moxxyv2/xmpp/xeps/xep_0030/cachemanager.dart"; @@ -56,33 +58,46 @@ class StartTLSNonza extends XMLNode { } class XmppConnection { - late ConnectionSettings _connectionSettings; + final StreamController _eventStreamController; + final Map> _awaitingResponse; + final Map _xmppManagers; + final List _incomingStanzaHandlers; + final List _outgoingStanzaHandlers; final BaseSocketWrapper _socket; XmppConnectionState _connectionState; late final Stream _socketStream; - final StreamController _eventStreamController; - final Map> _awaitingResponse = {}; - final Map _xmppManagers = {}; + late ConnectionSettings _connectionSettings; - // Stream properties - // - // Stream feature XMLNS + /// Stream properties + /// + /// Features we got after SASL auth (xmlns) final List _streamFeatures = List.empty(growable: true); + /// Disco info we got after binding a resource (xmlns) final List _serverFeatures = List.empty(growable: true); - RoutingState _routingState; - String _resource; + /// The buffer object to keep split up stanzas together final XmlStreamBuffer _streamBuffer; - Timer? _connectionPingTimer; - int _currentBackoffAttempt; - Timer? _backoffTimer; + /// UUID object to generate stanza and origin IDs final Uuid _uuid; - bool _resuming; // For indicating in a [ConnectionStateChangedEvent] that the event occured because we did a reconnection + /// The time between sending a ping to keep the connection open + // TODO: Only start the timer if we did not send a stanza after n seconds final Duration connectionPingDuration; + /// The current state of the connection handling state machine. + RoutingState _routingState; + /// The currently bound resource or "" if none has been bound yet. + String _resource; + /// Counter for how manyy we have tried to reconnect. + int _currentBackoffAttempt; + /// For indicating in a [ConnectionStateChangedEvent] that the event occured because we + /// did a reconnection. + bool _resuming; + /// Timers for the keep-alive ping and the backoff connection process. + Timer? _connectionPingTimer; + Timer? _backoffTimer; - // Negotiators + /// Negotiators late AuthenticationNegotiator _authenticator; - // Misc + /// Misc final Logger _log; /// [socket] is for debugging purposes. @@ -102,6 +117,10 @@ class XmppConnection { _uuid = const Uuid(), // NOTE: For testing _socket = socket ?? TCPSocketWrapper(), + _awaitingResponse = {}, + _xmppManagers = {}, + _incomingStanzaHandlers = List.empty(growable: true), + _outgoingStanzaHandlers = List.empty(growable: true), _log = Logger("XmppConnection") { _socketStream = _socket.getDataStream(); // TODO: Handle on done @@ -109,8 +128,11 @@ class XmppConnection { _socket.getErrorStream().listen(_handleError); } - /// Registers an [XmppManagerBase] subclass as a manager on this connection - void registerManager(XmppManagerBase manager) { + /// Registers an [XmppManagerBase] sub-class as a manager on this connection. + /// [sortHandlers] should NOT be touched. It specified if the handler priorities + /// should be set up. The only time this should be false is when called via + /// [registerManagers]. + void registerManager(XmppManagerBase manager, { bool sortHandlers = true }) { _log.finest("Registering ${manager.getId()}"); manager.register(XmppManagerAttributes( sendStanza: sendStanza, @@ -136,8 +158,27 @@ class XmppConnection { } else if (_xmppManagers.containsKey(discoManager)) { (_xmppManagers[discoManager] as DiscoManager).addDiscoFeatures(manager.getDiscoFeatures()); } + + _incomingStanzaHandlers.addAll(manager.getIncomingStanzaHandlers()); + _outgoingStanzaHandlers.addAll(manager.getOutgoingStanzaHandlers()); + + if (sortHandlers) { + _incomingStanzaHandlers.sort(stanzaHandlerSortComparator); + _outgoingStanzaHandlers.sort(stanzaHandlerSortComparator); + } } + /// Like [registerManager], but for a list of managers. + void registerManagers(List managers) { + for (final manager in managers) { + registerManager(manager, sortHandlers: false); + } + + // Sort them + _incomingStanzaHandlers.sort(stanzaHandlerSortComparator); + _outgoingStanzaHandlers.sort(stanzaHandlerSortComparator); + } + /// Generate an Id suitable for an origin-id or stanza id String generateId() { return _uuid.v4(); @@ -256,7 +297,7 @@ class XmppConnection { /// [stanza] has none. /// If addId is true, then an "id" attribute will be added to the stanza if [stanza] has /// none. - Future sendStanza(Stanza stanza, { bool addFrom = true, bool addId = true, bool awaitable = true }) { + Future sendStanza(Stanza stanza, { bool addFrom = true, bool addId = true, bool awaitable = true }) async { // Add extra data in case it was not set if (addId && (stanza.id == null || stanza.id == "")) { stanza = stanza.copyWith(id: generateId()); @@ -271,6 +312,9 @@ class XmppConnection { _awaitingResponse[stanza.id!] = Completer(); } + // Tell the SM manager that we're about to send a stanza + await _runOutoingStanzaHandlers(stanza); + // This uses the StreamManager to behave like a send queue if (_canSendData()) { _socket.write(stanzaString); @@ -279,9 +323,6 @@ class XmppConnection { // NOTE: Here we have send an Ack request nonza. This is now done by StreamManagementManager when receiving the StanzaSentEvent } - // Tell the SM manager that we're about to send a stanza - _sendEvent(StanzaSentEvent(stanza: stanza)); - if (awaitable) { return _awaitingResponse[stanza.id!]!.future; } else { @@ -385,6 +426,34 @@ class XmppConnection { _log.warning("Failed to discover server items using XEP-0030"); } } + + /// Iterate over [handlers] and check if the handler matches [stanza]. If it does, + /// call its callback and end the processing if the callback returned true; continue + /// if it returned false. + Future _runStanzaHandlers(List handlers, Stanza stanza) async { + StanzaHandlerData state = StanzaHandlerData(false, stanza); + for (final handler in handlers) { + if (handler.matches(stanza)) { + state = await handler.callback(stanza, state); + if (state.done) return true; + } + } + + return false; + } + + Future _runIncomingStanzaHandlers(Stanza stanza) async { + return await _runStanzaHandlers( + _incomingStanzaHandlers, + stanza + ); + } + Future _runOutoingStanzaHandlers(Stanza stanza) async { + return await _runStanzaHandlers( + _outgoingStanzaHandlers, + stanza + ); + } /// Called whenever we receive a stanza after resource binding or stream resumption. Future _handleStanza(XMLNode nonza) async { @@ -413,16 +482,8 @@ class XmppConnection { _awaitingResponse.remove(id); return; } - - bool stanzaHandled = false; - await Future.forEach( - _xmppManagers.values, - (XmppManagerBase manager) async { - final handled = await manager.runStanzaHandlers(stanza); - if (!stanzaHandled && handled) stanzaHandled = true; - } - ); + final stanzaHandled = await _runIncomingStanzaHandlers(stanza); if (!stanzaHandled) { handleUnhandledStanza(this, stanza); diff --git a/lib/xmpp/events.dart b/lib/xmpp/events.dart index 5e8196d5..b898ce57 100644 --- a/lib/xmpp/events.dart +++ b/lib/xmpp/events.dart @@ -33,13 +33,6 @@ class AuthenticationFailedEvent extends XmppEvent { AuthenticationFailedEvent({ required this.saslError }); } -/// Triggered when we send a stanza to the socket -class StanzaSentEvent extends XmppEvent { - final Stanza stanza; - - StanzaSentEvent({ required this.stanza }); -} - /// Triggered when we want to ping the connection open class SendPingEvent extends XmppEvent {} diff --git a/lib/xmpp/managers/base.dart b/lib/xmpp/managers/base.dart index 04c9be56..5a21c801 100644 --- a/lib/xmpp/managers/base.dart +++ b/lib/xmpp/managers/base.dart @@ -1,6 +1,5 @@ import "package:moxxyv2/xmpp/events.dart"; import "package:moxxyv2/xmpp/stringxml.dart"; -import "package:moxxyv2/xmpp/stanza.dart"; import "package:moxxyv2/xmpp/managers/handlers.dart"; import "package:moxxyv2/xmpp/managers/attributes.dart"; @@ -22,9 +21,14 @@ abstract class XmppManagerBase { return _managerAttributes; } - /// Return the [StanzaHandler]s associated with this manager. - List getStanzaHandlers() => []; + /// Return the [StanzaHandler]s associated with this manager that deal with stanzas we + /// send. + List getOutgoingStanzaHandlers() => []; + /// Return the [StanzaHandler]s associated with this manager that deal with stanzas we + /// receive. + List getIncomingStanzaHandlers() => []; + /// Return the [NonzaHandler]s associated with this manager. List getNonzaHandlers() => []; @@ -59,21 +63,4 @@ abstract class XmppManagerBase { return handled; } - - /// Runs all [StanzaHandlers] of this Manager which match the nonza. Resolves to true if - /// the nonza has been handled by one of the handlers. Resolves to false otherwise. - Future runStanzaHandlers(Stanza stanza) async { - bool handled = false; - await Future.forEach( - getStanzaHandlers(), - (StanzaHandler handler) async { - if (handler.matches(stanza)) { - handled = true; - await handler.callback(stanza); - } - } - ); - - return handled; - } } diff --git a/lib/xmpp/managers/data.dart b/lib/xmpp/managers/data.dart new file mode 100644 index 00000000..f4b5404e --- /dev/null +++ b/lib/xmpp/managers/data.dart @@ -0,0 +1,29 @@ +import "package:freezed_annotation/freezed_annotation.dart"; + +import "package:moxxyv2/xmpp/stanza.dart"; +import "package:moxxyv2/xmpp/xeps/xep_0066.dart"; +import "package:moxxyv2/xmpp/xeps/xep_0359.dart"; +import "package:moxxyv2/xmpp/xeps/xep_0385.dart"; +import "package:moxxyv2/xmpp/xeps/xep_0447.dart"; + +part "data.freezed.dart"; + +@freezed +class StanzaHandlerData with _$StanzaHandlerData { + factory StanzaHandlerData( + // Indicates to the runner that processing is now done. This means that all + // pre-processing is done and no other handlers should be consulted. + bool done, + // The stanza that is being dealt with + Stanza stanza, + { + StatelessMediaSharingData? sims, + StatelessFileSharingData? sfs, + OOBData? oob, + StableStanzaId? stableId, + @Default(false) bool isCarbon, + @Default(false) bool deliveryReceiptRequested, + @Default(false) bool isMarkable + } + ) = _StanzaHandlerData; +} diff --git a/lib/xmpp/managers/handlers.dart b/lib/xmpp/managers/handlers.dart index 356e943d..e7bd3e60 100644 --- a/lib/xmpp/managers/handlers.dart +++ b/lib/xmpp/managers/handlers.dart @@ -2,16 +2,16 @@ import "package:moxxyv2/shared/helpers.dart"; import "package:moxxyv2/xmpp/namespaces.dart"; import "package:moxxyv2/xmpp/stanza.dart"; import "package:moxxyv2/xmpp/stringxml.dart"; +import "package:moxxyv2/xmpp/managers/data.dart"; -class NonzaHandler { +abstract class Handler { final String? nonzaTag; final String? nonzaXmlns; final bool matchStanzas; - final Future Function(XMLNode) callback; - NonzaHandler({ this.nonzaTag, this.nonzaXmlns, required this.callback, this.matchStanzas = false }); + const Handler(this.matchStanzas, { this.nonzaTag, this.nonzaXmlns }); - /// Returns true if the node matches the description provided by this NonzaHandler + /// Returns true if the node matches the description provided by this [Handler]. bool matches(XMLNode node) { bool matches = false; @@ -31,17 +31,38 @@ class NonzaHandler { } } -class StanzaHandler extends NonzaHandler { - final String? tagXmlns; +class NonzaHandler extends Handler { + final Future Function(XMLNode) callback; + + NonzaHandler({ + required this.callback, + String? nonzaTag, + String? nonzaXmlns + }) : super( + false, + nonzaTag: nonzaTag, + nonzaXmlns: nonzaXmlns + ); +} + +class StanzaHandler extends Handler { final String? tagName; + final String? tagXmlns; + final int priority; + final Future Function(Stanza, StanzaHandlerData) callback; - StanzaHandler({ this.tagXmlns, this.tagName, String? stanzaTag, required Future Function(Stanza) callback }) : super( - matchStanzas: true, + StanzaHandler({ + required this.callback, + this.tagXmlns, + this.tagName, + this.priority = 0, + String? stanzaTag, + }) : super( + true, nonzaTag: stanzaTag, - nonzaXmlns: stanzaXmlns, - callback: (XMLNode node) async => await callback(Stanza.fromXMLNode(node)) + nonzaXmlns: stanzaXmlns ); - + @override bool matches(XMLNode node) { bool matches = super.matches(node); @@ -68,3 +89,5 @@ class StanzaHandler extends NonzaHandler { return matches; } } + +int stanzaHandlerSortComparator(StanzaHandler a, StanzaHandler b) => b.priority.compareTo(a.priority); diff --git a/lib/xmpp/managers/namespaces.dart b/lib/xmpp/managers/namespaces.dart index 7984c819..591c1469 100644 --- a/lib/xmpp/managers/namespaces.dart +++ b/lib/xmpp/managers/namespaces.dart @@ -9,3 +9,9 @@ const carbonsManager = "im.moxxy.carbonsmanager"; const vcardManager = "im.moxxy.vcardmanager"; const pubsubManager = "im.moxxy.pubsubmanager"; const userAvatarManager = "im.moxxy.useravatarmanager"; +const stableIdManager = "im.moxxy.stableidmanager"; +const simsManager = "im.moxxy.simsmanager"; +const messageDeliveryReceiptManager = "im.moxxy.messagedeliveryreceiptmanager"; +const chatMarkerManager = "im.moxxy.chatmarkermanager"; +const oobManager = "im.moxxy.oobmanager"; +const sfsManager = "im.moxxy.sfsmanager"; diff --git a/lib/xmpp/managers/priorities.dart b/lib/xmpp/managers/priorities.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/xmpp/message.dart b/lib/xmpp/message.dart index 115fb48a..613f5b0e 100644 --- a/lib/xmpp/message.dart +++ b/lib/xmpp/message.dart @@ -4,17 +4,12 @@ import "package:moxxyv2/xmpp/stringxml.dart"; import "package:moxxyv2/xmpp/jid.dart"; import "package:moxxyv2/xmpp/namespaces.dart"; import "package:moxxyv2/xmpp/managers/base.dart"; +import "package:moxxyv2/xmpp/managers/data.dart"; import "package:moxxyv2/xmpp/managers/namespaces.dart"; import "package:moxxyv2/xmpp/managers/handlers.dart"; -import "package:moxxyv2/xmpp/xeps/xep_0030/cachemanager.dart"; -import "package:moxxyv2/xmpp/xeps/xep_0066.dart"; import "package:moxxyv2/xmpp/xeps/xep_0184.dart"; -import "package:moxxyv2/xmpp/xeps/xep_0280.dart"; -import "package:moxxyv2/xmpp/xeps/xep_0297.dart"; import "package:moxxyv2/xmpp/xeps/xep_0333.dart"; import "package:moxxyv2/xmpp/xeps/xep_0359.dart"; -import "package:moxxyv2/xmpp/xeps/xep_0385.dart"; -import "package:moxxyv2/xmpp/xeps/xep_0447.dart"; class MessageDetails { final String to; @@ -48,162 +43,34 @@ class MessageManager extends XmppManagerBase { String getName() => "MessageManager"; @override - List getStanzaHandlers() => [ + List getIncomingStanzaHandlers() => [ StanzaHandler( stanzaTag: "message", - callback: _onMessage + callback: _onMessage, + priority: -100 ) ]; - - @override - List getDiscoFeatures() => [ chatMarkersXmlns, oobDataXmlns, deliveryXmlns, stableIdXmlns ]; - - /// Helper function to extract and verify the origin and stanza Id according to - /// XEP-0359. - /// Requires a [DiscoCacheManager] to be registered in order to provide anything - /// other than [null]. - Future _getStanzaId(Stanza message) async { - final from = JID.fromString(message.attributes["from"]!); - String? originId; - String? stanzaId; - String? stanzaIdBy; - final originIdTag = message.firstTag("origin-id", xmlns: stableIdXmlns); - final stanzaIdTag = message.firstTag("stanza-id", xmlns: stableIdXmlns); - if (originIdTag != null || stanzaIdTag != null) { - logger.finest("Found Unique and Stable Stanza Id tag"); - final attrs = getAttributes(); - final cache = attrs.getManagerById(discoCacheManager) as DiscoCacheManager?; - if (cache != null) { - final info = await cache.getInfoByJid(from.toString()); - if (info != null) { - logger.finest("Got info for ${from.toString()}"); - if (info.features.contains(stableIdXmlns)) { - logger.finest("${from.toString()} supports $stableIdXmlns."); - - if (originIdTag != null) { - originId = originIdTag.attributes["id"]!; - } - - if (stanzaIdTag != null) { - stanzaId = stanzaIdTag.attributes["id"]!; - stanzaIdBy = stanzaIdTag.attributes["by"]!; - } - } else { - logger.finest("${from.toString()} does not support $stableIdXmlns. Ignoring... "); - } - } - } - } - - return StableStanzaId( - originId: originId, - stanzaId: stanzaId, - stanzaIdBy: stanzaIdBy - ); - } - Future _handleChatMarker(Stanza message, XMLNode marker) async { - final attrs = getAttributes(); - - if (!["received", "displayed", "acknowledged"].contains(marker.tag)) { - logger.warning("Unknown message marker '${marker.tag}' found."); - return; - } - - attrs.sendEvent(ChatMarkerEvent( - from: JID.fromString(message.from!), - type: marker.tag, - id: marker.attributes["id"]!, - )); - } - StatelessMediaSharingData? _getSIMS(Stanza message) { - final references = message.findTags("reference", xmlns: referenceXmlns); - for (final ref in references) { - final sims = ref.firstTag("media-sharing", xmlns: simsXmlns); - if (sims != null) return parseSIMSElement(sims); - } - - return null; - } - - String? _isDeliveryReceiptResponse(Stanza message) { - final received = message.firstTag("received", xmlns: deliveryXmlns); - if (received == null) return null; - - for (final item in message.children) { - if (!["origin-id", "stanza-id", "delay"].contains(item.tag)) { - logger.info("Won't handle stanza as delivery receipt because we found an '${item.tag}' element"); - - return null; - } - } - - return received.attributes["id"]!; - } - - Future _onMessage(Stanza message) async { + Future _onMessage(Stanza message, StanzaHandlerData state) async { // First check if it's a carbon final from = JID.fromString(message.attributes["from"]!); - final received = message.firstTag("received", xmlns: carbonsXmlns); - final attrs = getAttributes(); - bool isCarbon = false; - if (received != null) { - final cm = attrs.getManagerById(carbonsManager) as CarbonsManager?; - - // Ignore invalid carbons - if (cm == null || !cm.isCarbonValid(from)) return true; - - final forwarded = received.firstTag("forwarded", xmlns: forwardedXmlns)!; - message = unpackForwarded(forwarded); - isCarbon = true; - } - - final did = _isDeliveryReceiptResponse(message); - if (did != null) { - attrs.sendEvent(DeliveryReceiptReceivedEvent(from: from, id: did)); - return true; - } - - final sfs = message.firstTag("file-sharing", xmlns: sfsXmlns); - final sims = _getSIMS(message); final body = message.firstTag("body"); - if (body == null && sfs == null && sims == null) { - final marker = message.firstTagByXmlns(chatMarkersXmlns); - if (marker != null) { - // Response to a marker - await _handleChatMarker(message, marker); - return true; - } - - return false; - } - - OOBData? oob; - final oobTag = message.firstTag("x", xmlns: oobDataXmlns); - if (oobTag != null) { - final url = oobTag.firstTag("url"); - final desc = oobTag.firstTag("desc"); - oob = OOBData( - url: url?.innerText(), - desc: desc?.innerText() - ); - } - + getAttributes().sendEvent(MessageEvent( body: body != null ? body.innerText() : "", fromJid: from, sid: message.attributes["id"]!, + stanzaId: state.stableId ?? StableStanzaId(), + isCarbon: state.isCarbon, + deliveryReceiptRequested: state.deliveryReceiptRequested, + isMarkable: state.isMarkable, type: message.attributes["type"], - stanzaId: await _getStanzaId(message), - isMarkable: message.firstTag("markable", xmlns: chatMarkersXmlns) != null, - isCarbon: isCarbon, - deliveryReceiptRequested: message.firstTag("request", xmlns: deliveryXmlns) != null, - oob: oob, - sfs: sfs != null ? parseSFSElement(sfs) : null, - sims: sims + oob: state.oob, + sfs: state.sfs, + sims: state.sims )); - return true; + return state.copyWith(done: true); } /// Send a message to [to] with the content [body]. If [deliveryRequest] is true, then diff --git a/lib/xmpp/presence.dart b/lib/xmpp/presence.dart index 46147d68..b12f45ab 100644 --- a/lib/xmpp/presence.dart +++ b/lib/xmpp/presence.dart @@ -5,6 +5,7 @@ import "package:moxxyv2/xmpp/jid.dart"; import "package:moxxyv2/xmpp/events.dart"; import "package:moxxyv2/xmpp/managers/base.dart"; import "package:moxxyv2/xmpp/managers/namespaces.dart"; +import "package:moxxyv2/xmpp/managers/data.dart"; import "package:moxxyv2/xmpp/managers/handlers.dart"; import "package:moxxyv2/xmpp/xeps/xep_0030/xep_0030.dart"; import "package:moxxyv2/xmpp/xeps/xep_0030/helpers.dart"; @@ -23,7 +24,7 @@ class PresenceManager extends XmppManagerBase { String getName() => "PresenceManager"; @override - List getStanzaHandlers() => [ + List getIncomingStanzaHandlers() => [ StanzaHandler( stanzaTag: "presence", callback: _onPresence @@ -33,14 +34,14 @@ class PresenceManager extends XmppManagerBase { @override List getDiscoFeatures() => [ capsXmlns ]; - Future _onPresence(Stanza presence) async { + Future _onPresence(Stanza presence, StanzaHandlerData state) async { final attrs = getAttributes(); switch (presence.type) { case "subscribed": { attrs.sendEvent( SubscriptionRequestReceivedEvent(from: JID.fromString(presence.from!)) ); - return true; + return state.copyWith(done: true); } default: break; } @@ -51,7 +52,7 @@ class PresenceManager extends XmppManagerBase { getAttributes().sendEvent(PresenceReceivedEvent(JID.fromString(presence.from!), presence)); } - return false; + return state.copyWith(done: false); } /// Returns the capability hash. diff --git a/lib/xmpp/roster.dart b/lib/xmpp/roster.dart index 7fd12311..c5d1d542 100644 --- a/lib/xmpp/roster.dart +++ b/lib/xmpp/roster.dart @@ -5,6 +5,7 @@ import "package:moxxyv2/xmpp/stringxml.dart"; import "package:moxxyv2/xmpp/jid.dart"; import "package:moxxyv2/xmpp/managers/base.dart"; import "package:moxxyv2/xmpp/managers/namespaces.dart"; +import "package:moxxyv2/xmpp/managers/data.dart"; import "package:moxxyv2/xmpp/managers/handlers.dart"; class XmppRosterItem { @@ -48,7 +49,7 @@ class RosterManager extends XmppManagerBase { String getName() => "RosterManager"; @override - List getStanzaHandlers() => [ + List getIncomingStanzaHandlers() => [ StanzaHandler( stanzaTag: "iq", tagName: "query", @@ -67,7 +68,7 @@ class RosterManager extends XmppManagerBase { _rosterVersion = ver; } - Future _onRosterPush(Stanza stanza) async { + Future _onRosterPush(Stanza stanza, StanzaHandlerData state) async { final attrs = getAttributes(); final from = stanza.attributes["from"]; final selfJid = attrs.getConnectionSettings().jid; @@ -79,7 +80,7 @@ class RosterManager extends XmppManagerBase { // - a full JID of our own if (from != null && JID.fromString(stanza.attributes["from"]).toBare() != selfJid) { logger.warning("Roster push invalid! Unexpected from attribute: ${stanza.toXml()}"); - return true; + return state.copyWith(done: true); } final query = stanza.firstTag("query", xmlns: rosterXmlns)!; @@ -87,7 +88,7 @@ class RosterManager extends XmppManagerBase { if (item == null) { logger.warning("Received empty roster push"); - return true; + return state.copyWith(done: true); } if (query.attributes["ver"] != null) { @@ -105,7 +106,7 @@ class RosterManager extends XmppManagerBase { )); attrs.sendStanza(stanza.reply()); - return true; + return state.copyWith(done: true); } /// Requests the roster from the server. [lastVersion] refers to the last version diff --git a/lib/xmpp/socket.dart b/lib/xmpp/socket.dart index 0da23ec0..d77bce84 100644 --- a/lib/xmpp/socket.dart +++ b/lib/xmpp/socket.dart @@ -64,13 +64,10 @@ class TCPSocketWrapper extends BaseSocketWrapper { bool isSecure() => _secure; bool _onBadCertificate(certificate, String domain) { + _log.fine("Bad certificate: ${certificate.toString()}"); final isExpired = certificate.endValidity.isAfter(DateTime.now()); // TODO: Remove the kDebugMode once I am sure this works as it should return !isExpired && certificate.domain == domain && kDebugMode; - - _log.fine("Bad certificate: ${certificate.toString()}"); - - return false; } Future _xep368Connect(String domain) async { diff --git a/lib/xmpp/xeps/xep_0030/xep_0030.dart b/lib/xmpp/xeps/xep_0030/xep_0030.dart index 3ab74ef9..0918c46c 100644 --- a/lib/xmpp/xeps/xep_0030/xep_0030.dart +++ b/lib/xmpp/xeps/xep_0030/xep_0030.dart @@ -5,6 +5,7 @@ import "package:moxxyv2/xmpp/presence.dart"; import "package:moxxyv2/xmpp/managers/base.dart"; import "package:moxxyv2/xmpp/managers/namespaces.dart"; import "package:moxxyv2/xmpp/managers/handlers.dart"; +import "package:moxxyv2/xmpp/managers/data.dart"; import "package:moxxyv2/xmpp/xeps/xep_0030/helpers.dart"; class DiscoManager extends XmppManagerBase { @@ -14,7 +15,7 @@ class DiscoManager extends XmppManagerBase { DiscoManager() : _features = List.empty(growable: true), super(); @override - List getStanzaHandlers() => [ + List getIncomingStanzaHandlers() => [ StanzaHandler( tagName: "query", tagXmlns: discoInfoXmlns, @@ -54,8 +55,8 @@ class DiscoManager extends XmppManagerBase { /// May be overriden. Specifies the identities which will be returned in a disco info response. List getIdentities() => const [ Identity(category: "client", type: "pc", name: "moxxmpp", lang: "en") ]; - Future _onDiscoInfoRequest(Stanza stanza) async { - if (stanza.type != "get") return false; + Future _onDiscoInfoRequest(Stanza stanza, StanzaHandlerData state) async { + if (stanza.type != "get") return state; final presence = getAttributes().getManagerById(presenceManager)! as PresenceManager; final query = stanza.firstTag("query")!; @@ -93,7 +94,7 @@ class DiscoManager extends XmppManagerBase { ) )); - return true; + return state.copyWith(done: true); } getAttributes().sendStanza(stanza.reply( @@ -114,11 +115,11 @@ class DiscoManager extends XmppManagerBase { ] )); - return true; + return state.copyWith(done: true); } - Future _onDiscoItemsRequest(Stanza stanza) async { - if (stanza.type != "get") return false; + Future _onDiscoItemsRequest(Stanza stanza, StanzaHandlerData state) async { + if (stanza.type != "get") return state; final query = stanza.firstTag("query")!; if (query.attributes["node"] != null) { @@ -152,7 +153,7 @@ class DiscoManager extends XmppManagerBase { ) )); - return true; + return state.copyWith(done: true); } getAttributes().sendStanza(stanza.reply( @@ -163,7 +164,7 @@ class DiscoManager extends XmppManagerBase { ) ] )); - return true; + return state.copyWith(done: true); } /// Sends a disco info query to the (full) jid [entity], optionally with node=[node]. diff --git a/lib/xmpp/xeps/xep_0054.dart b/lib/xmpp/xeps/xep_0054.dart index e10995fa..17b51f2e 100644 --- a/lib/xmpp/xeps/xep_0054.dart +++ b/lib/xmpp/xeps/xep_0054.dart @@ -5,6 +5,7 @@ import "package:moxxyv2/xmpp/jid.dart"; import "package:moxxyv2/xmpp/events.dart"; import "package:moxxyv2/xmpp/managers/base.dart"; import "package:moxxyv2/xmpp/managers/namespaces.dart"; +import "package:moxxyv2/xmpp/managers/data.dart"; import "package:moxxyv2/xmpp/managers/handlers.dart"; class vCardPhoto { @@ -33,8 +34,13 @@ class vCardManager extends XmppManagerBase { String getName() => "vCardManager"; @override - List getStanzaHandlers() => [ - StanzaHandler(stanzaTag: "presence", tagName: "x", tagXmlns: vCardTempUpdate, callback: _onPresence) + List getIncomingStanzaHandlers() => [ + StanzaHandler( + stanzaTag: "presence", + tagName: "x", + tagXmlns: vCardTempUpdate, + callback: _onPresence, + ) ]; /// In case we get the avatar hash some other way. @@ -42,7 +48,7 @@ class vCardManager extends XmppManagerBase { _lastHash[jid] = hash; } - Future _onPresence(Stanza presence) async { + Future _onPresence(Stanza presence, StanzaHandlerData state) async { final x = presence.firstTag("x", xmlns: vCardTempUpdate)!; final hash = x.firstTag("photo")!.innerText(); @@ -64,7 +70,7 @@ class vCardManager extends XmppManagerBase { } } - return true; + return state.copyWith(done: true); } vCardPhoto? _parseVCardPhoto(XMLNode? node) { diff --git a/lib/xmpp/xeps/xep_0060.dart b/lib/xmpp/xeps/xep_0060.dart index cfa0cb3c..dbc412fc 100644 --- a/lib/xmpp/xeps/xep_0060.dart +++ b/lib/xmpp/xeps/xep_0060.dart @@ -1,10 +1,11 @@ -import "package:moxxyv2/xmpp/managers/base.dart"; -import "package:moxxyv2/xmpp/managers/namespaces.dart"; -import "package:moxxyv2/xmpp/managers/handlers.dart"; import "package:moxxyv2/xmpp/stringxml.dart"; import "package:moxxyv2/xmpp/namespaces.dart"; import "package:moxxyv2/xmpp/stanza.dart"; import "package:moxxyv2/xmpp/events.dart"; +import "package:moxxyv2/xmpp/managers/base.dart"; +import "package:moxxyv2/xmpp/managers/data.dart"; +import "package:moxxyv2/xmpp/managers/namespaces.dart"; +import "package:moxxyv2/xmpp/managers/handlers.dart"; class PubSubItem { final String id; @@ -25,11 +26,16 @@ class PubSubManager extends XmppManagerBase { String getName() => "pubsubManager"; @override - List getStanzaHandlers() => [ - StanzaHandler(stanzaTag: "message", tagName: "event", tagXmlns: pubsubEventXmlns, callback: _onPubsubMessage) + List getIncomingStanzaHandlers() => [ + StanzaHandler( + stanzaTag: "message", + tagName: "event", + tagXmlns: pubsubEventXmlns, + callback: _onPubsubMessage + ) ]; - Future _onPubsubMessage(Stanza message) async { + Future _onPubsubMessage(Stanza message, StanzaHandlerData state) async { logger.finest("Received PubSub event"); final event = message.firstTag("event", xmlns: pubsubEventXmlns)!; final items = event.firstTag("items")!; @@ -44,7 +50,7 @@ class PubSubManager extends XmppManagerBase { from: message.attributes["from"]! )); - return true; + return state.copyWith(done: true); } Future subscribe(String jid, String node) async { diff --git a/lib/xmpp/xeps/xep_0066.dart b/lib/xmpp/xeps/xep_0066.dart index 49704188..d28ddd91 100644 --- a/lib/xmpp/xeps/xep_0066.dart +++ b/lib/xmpp/xeps/xep_0066.dart @@ -1,3 +1,10 @@ +import "package:moxxyv2/xmpp/stanza.dart"; +import "package:moxxyv2/xmpp/namespaces.dart"; +import "package:moxxyv2/xmpp/managers/base.dart"; +import "package:moxxyv2/xmpp/managers/data.dart"; +import "package:moxxyv2/xmpp/managers/namespaces.dart"; +import "package:moxxyv2/xmpp/managers/handlers.dart"; + /// A data class representing the jabber:x:oob tag. class OOBData { final String? url; @@ -5,3 +12,39 @@ class OOBData { const OOBData({ this.url, this.desc }); } + +class OOBManager extends XmppManagerBase { + @override + String getName() => "OOBName"; + + @override + String getId() => oobManager; + + @override + List getDiscoFeatures() => [ oobDataXmlns ]; + + @override + List getIncomingStanzaHandlers() => [ + StanzaHandler( + stanzaTag: "message", + tagName: "x", + tagXmlns: oobDataXmlns, + callback: _onMessage, + // Before the message manager + priority: -99 + ) + ]; + + Future _onMessage(Stanza message, StanzaHandlerData state) async { + final x = message.firstTag("x", xmlns: oobDataXmlns)!; + final url = x.firstTag("url"); + final desc = x.firstTag("desc"); + + return state.copyWith( + oob: OOBData( + url: url?.innerText(), + desc: desc?.innerText() + ) + ); + } +} diff --git a/lib/xmpp/xeps/xep_0184.dart b/lib/xmpp/xeps/xep_0184.dart index e04aebc0..bc5b34f9 100644 --- a/lib/xmpp/xeps/xep_0184.dart +++ b/lib/xmpp/xeps/xep_0184.dart @@ -1,5 +1,12 @@ +import "package:moxxyv2/xmpp/events.dart"; import "package:moxxyv2/xmpp/stringxml.dart"; +import "package:moxxyv2/xmpp/stanza.dart"; import "package:moxxyv2/xmpp/namespaces.dart"; +import "package:moxxyv2/xmpp/jid.dart"; +import "package:moxxyv2/xmpp/managers/base.dart"; +import "package:moxxyv2/xmpp/managers/data.dart"; +import "package:moxxyv2/xmpp/managers/namespaces.dart"; +import "package:moxxyv2/xmpp/managers/handlers.dart"; XMLNode makeMessageDeliveryRequest() { return XMLNode.xmlns( @@ -15,3 +22,57 @@ XMLNode makeMessageDeliveryResponse(String id) { attributes: { "id": id } ); } + +class MessageDeliveryReceiptManager extends XmppManagerBase { + @override + List getDiscoFeatures() => [ deliveryXmlns ]; + + @override + String getName() => "MessageDeliveryReceiptManager"; + + @override + String getId() => messageDeliveryReceiptManager; + + @override + List getIncomingStanzaHandlers() => [ + StanzaHandler( + stanzaTag: "message", + tagName: "received", + tagXmlns: deliveryXmlns, + callback: _onDeliveryReceiptReceived, + // Before the message handler + priority: -99 + ), + StanzaHandler( + stanzaTag: "message", + tagName: "request", + tagXmlns: deliveryXmlns, + callback: _onDeliveryRequestReceived, + // Before the message handler + priority: -99 + ) + ]; + + Future _onDeliveryRequestReceived(Stanza message, StanzaHandlerData state) async { + return state.copyWith(deliveryReceiptRequested: true); + } + + Future _onDeliveryReceiptReceived(Stanza message, StanzaHandlerData state) async { + final received = message.firstTag("received", xmlns: deliveryXmlns)!; + for (final item in message.children) { + if (!["origin-id", "stanza-id", "delay"].contains(item.tag)) { + logger.info("Won't handle stanza as delivery receipt because we found an '${item.tag}' element"); + + return state; + } + } + + getAttributes().sendEvent( + DeliveryReceiptReceivedEvent( + from: JID.fromString(message.attributes["from"]!), + id: received.attributes["id"]! + ) + ); + return state.copyWith(done: true); + } +} diff --git a/lib/xmpp/xeps/xep_0198/xep_0198.dart b/lib/xmpp/xeps/xep_0198/xep_0198.dart index bce8a357..6aac2c05 100644 --- a/lib/xmpp/xeps/xep_0198/xep_0198.dart +++ b/lib/xmpp/xeps/xep_0198/xep_0198.dart @@ -6,6 +6,7 @@ import "package:moxxyv2/xmpp/namespaces.dart"; import "package:moxxyv2/xmpp/stringxml.dart"; import "package:moxxyv2/xmpp/managers/handlers.dart"; import "package:moxxyv2/xmpp/managers/base.dart"; +import "package:moxxyv2/xmpp/managers/data.dart"; import "package:moxxyv2/xmpp/managers/namespaces.dart"; import "package:moxxyv2/xmpp/xeps/xep_0198/state.dart"; import "package:moxxyv2/xmpp/xeps/xep_0198/nonzas.dart"; @@ -123,17 +124,22 @@ class StreamManagementManager extends XmppManagerBase { ]; @override - List getStanzaHandlers() => [ + List getIncomingStanzaHandlers() => [ StanzaHandler( - callback: _serverStanzaReceived + callback: _onServerStanzaReceived ) ]; + @override + List getOutgoingStanzaHandlers() => [ + StanzaHandler( + callback: _onClientStanzaSent + ) + ]; + @override void onXmppEvent(XmppEvent event) { - if (event is StanzaSentEvent) { - _onClientStanzaSent(event.stanza); - } else if (event is SendPingEvent) { + if (event is SendPingEvent) { if (isStreamManagementEnabled()) { _sendAckRequestPing(); } else { @@ -220,13 +226,13 @@ class StreamManagementManager extends XmppManagerBase { } /// Called whenever we receive a stanza from the server. - Future _serverStanzaReceived(stanza) async { + Future _onServerStanzaReceived(Stanza stanza, StanzaHandlerData state) async { _incrementS2C(); - return false; + return state; } /// Called whenever we send a stanza. - void _onClientStanzaSent(Stanza stanza) { + Future _onClientStanzaSent(Stanza stanza, StanzaHandlerData state) async { _startTimer(); _incrementC2S(); @@ -237,6 +243,8 @@ class StreamManagementManager extends XmppManagerBase { if (isStreamManagementEnabled()) { getAttributes().sendNonza(StreamManagementRequestNonza()); } + + return state; } /// Removes all stanzas in the unacked queue that have a sequence number less-than or diff --git a/lib/xmpp/xeps/xep_0280.dart b/lib/xmpp/xeps/xep_0280.dart index 377d01f7..323b056b 100644 --- a/lib/xmpp/xeps/xep_0280.dart +++ b/lib/xmpp/xeps/xep_0280.dart @@ -1,10 +1,14 @@ -import "package:moxxyv2/xmpp/managers/base.dart"; -import "package:moxxyv2/xmpp/managers/namespaces.dart"; import "package:moxxyv2/xmpp/events.dart"; import "package:moxxyv2/xmpp/stanza.dart"; import "package:moxxyv2/xmpp/stringxml.dart"; import "package:moxxyv2/xmpp/namespaces.dart"; import "package:moxxyv2/xmpp/jid.dart"; +import "package:moxxyv2/xmpp/managers/base.dart"; +import "package:moxxyv2/xmpp/managers/namespaces.dart"; +import "package:moxxyv2/xmpp/managers/data.dart"; +import "package:moxxyv2/xmpp/managers/handlers.dart"; +import "package:moxxyv2/xmpp/xeps/xep_0297.dart"; + class CarbonsManager extends XmppManagerBase { bool _isEnabled; @@ -17,6 +21,32 @@ class CarbonsManager extends XmppManagerBase { @override String getName() => "CarbonsManager"; + @override + List getIncomingStanzaHandlers() => [ + StanzaHandler( + stanzaTag: "message", + tagName: "received", + tagXmlns: carbonsXmlns, + callback: _onMessage, + // Before all managers the message manager depends on + priority: -98 + ) + ]; + + Future _onMessage(Stanza message, StanzaHandlerData state) async { + final from = JID.fromString(message.attributes["from"]!); + final received = message.firstTag("received", xmlns: carbonsXmlns)!; + if (!isCarbonValid(from)) return state.copyWith(done: true); + + final forwarded = received.firstTag("forwarded", xmlns: forwardedXmlns)!; + final carbon = unpackForwarded(forwarded); + + return state.copyWith( + isCarbon: true, + stanza: carbon + ); + } + Future enableCarbons() async { final result = await getAttributes().sendStanza( Stanza.iq( diff --git a/lib/xmpp/xeps/xep_0333.dart b/lib/xmpp/xeps/xep_0333.dart index 1266209f..292d81ee 100644 --- a/lib/xmpp/xeps/xep_0333.dart +++ b/lib/xmpp/xeps/xep_0333.dart @@ -1,5 +1,12 @@ +import "package:moxxyv2/xmpp/events.dart"; import "package:moxxyv2/xmpp/stringxml.dart"; +import "package:moxxyv2/xmpp/stanza.dart"; import "package:moxxyv2/xmpp/namespaces.dart"; +import "package:moxxyv2/xmpp/jid.dart"; +import "package:moxxyv2/xmpp/managers/base.dart"; +import "package:moxxyv2/xmpp/managers/data.dart"; +import "package:moxxyv2/xmpp/managers/namespaces.dart"; +import "package:moxxyv2/xmpp/managers/handlers.dart"; XMLNode makeChatMarkerMarkable() { return XMLNode.xmlns( @@ -16,3 +23,44 @@ XMLNode makeChatMarker(String tag, String id) { attributes: { "id": id } ); } + +class ChatMarkerManager extends XmppManagerBase { + @override + String getName() => "ChatMarkerManager"; + + @override + String getId() => chatMarkerManager; + + @override + List getDiscoFeatures() => [ chatMarkersXmlns ]; + + @override + List getIncomingStanzaHandlers() => [ + StanzaHandler( + stanzaTag: "message", + tagXmlns: chatMarkersXmlns, + callback: _onMessage, + // Before the message handler + priority: -99, + ) + ]; + + Future _onMessage(Stanza message, StanzaHandlerData state) async { + final marker = message.firstTagByXmlns(chatMarkersXmlns)!; + + // Handle the explicitly + if (marker.tag == "markable") return state.copyWith(isMarkable: true); + + if (!["received", "displayed", "acknowledged"].contains(marker.tag)) { + logger.warning("Unknown message marker '${marker.tag}' found."); + } else { + getAttributes().sendEvent(ChatMarkerEvent( + from: JID.fromString(message.from!), + type: marker.tag, + id: marker.attributes["id"]!, + )); + } + + return state.copyWith(done: true); + } +} diff --git a/lib/xmpp/xeps/xep_0359.dart b/lib/xmpp/xeps/xep_0359.dart index 17928076..b73bf1d4 100644 --- a/lib/xmpp/xeps/xep_0359.dart +++ b/lib/xmpp/xeps/xep_0359.dart @@ -1,5 +1,12 @@ +import "package:moxxyv2/xmpp/stanza.dart"; +import "package:moxxyv2/xmpp/jid.dart"; import "package:moxxyv2/xmpp/stringxml.dart"; import "package:moxxyv2/xmpp/namespaces.dart"; +import "package:moxxyv2/xmpp/managers/base.dart"; +import "package:moxxyv2/xmpp/managers/namespaces.dart"; +import "package:moxxyv2/xmpp/managers/handlers.dart"; +import "package:moxxyv2/xmpp/managers/data.dart"; +import "package:moxxyv2/xmpp/xeps/xep_0030/cachemanager.dart"; /// Represents data provided by XEP-0359. /// NOTE: [StableStanzaId.stanzaId] must not be confused with the actual id attribute of @@ -19,3 +26,66 @@ XMLNode makeOriginIdElement(String id) { attributes: { "id": id } ); } + +class StableIdManager extends XmppManagerBase { + @override + String getName() => "StableIdManager"; + + @override + String getId() => stableIdManager; + + @override + List getDiscoFeatures() => [ stableIdXmlns ]; + + @override + List getIncomingStanzaHandlers() => [ + StanzaHandler( + stanzaTag: "message", + callback: _onMessage, + // Before the MessageManager + priority: -99 + ) + ]; + + Future _onMessage(Stanza message, StanzaHandlerData state) async { + final from = JID.fromString(message.attributes["from"]!); + String? originId; + String? stanzaId; + String? stanzaIdBy; + final originIdTag = message.firstTag("origin-id", xmlns: stableIdXmlns); + final stanzaIdTag = message.firstTag("stanza-id", xmlns: stableIdXmlns); + if (originIdTag != null || stanzaIdTag != null) { + logger.finest("Found Unique and Stable Stanza Id tag"); + final attrs = getAttributes(); + final cache = attrs.getManagerById(discoCacheManager) as DiscoCacheManager?; + if (cache != null) { + final info = await cache.getInfoByJid(from.toString()); + if (info != null) { + logger.finest("Got info for ${from.toString()}"); + if (info.features.contains(stableIdXmlns)) { + logger.finest("${from.toString()} supports $stableIdXmlns."); + + if (originIdTag != null) { + originId = originIdTag.attributes["id"]!; + } + + if (stanzaIdTag != null) { + stanzaId = stanzaIdTag.attributes["id"]!; + stanzaIdBy = stanzaIdTag.attributes["by"]!; + } + } else { + logger.finest("${from.toString()} does not support $stableIdXmlns. Ignoring... "); + } + } + } + } + + return state.copyWith( + stableId: StableStanzaId( + originId: originId, + stanzaId: stanzaId, + stanzaIdBy: stanzaIdBy + ) + ); + } +} diff --git a/lib/xmpp/xeps/xep_0385.dart b/lib/xmpp/xeps/xep_0385.dart index a9e6f670..38b4062b 100644 --- a/lib/xmpp/xeps/xep_0385.dart +++ b/lib/xmpp/xeps/xep_0385.dart @@ -1,5 +1,10 @@ +import "package:moxxyv2/xmpp/stanza.dart"; import "package:moxxyv2/xmpp/stringxml.dart"; import "package:moxxyv2/xmpp/namespaces.dart"; +import "package:moxxyv2/xmpp/managers/base.dart"; +import "package:moxxyv2/xmpp/managers/data.dart"; +import "package:moxxyv2/xmpp/managers/namespaces.dart"; +import "package:moxxyv2/xmpp/managers/handlers.dart"; import "package:moxxyv2/xmpp/xeps/staging/file_thumbnails.dart"; class StatelessMediaSharingData { @@ -53,3 +58,36 @@ StatelessMediaSharingData parseSIMSElement(XMLNode node) { thumbnails: thumbnails ); } + +class SIMSManager extends XmppManagerBase { + @override + String getName() => "SIMSManager"; + + @override + String getId() => simsManager; + + @override + List getDiscoFeatures() => [ simsXmlns ]; + + @override + List getIncomingStanzaHandlers() => [ + StanzaHandler( + stanzaTag: "message", + callback: _onMessage, + tagName: "reference", + tagXmlns: referenceXmlns, + // Before the message handler + priority: -99 + ) + ]; + + Future _onMessage(Stanza message, StanzaHandlerData state) async { + final references = message.findTags("reference", xmlns: referenceXmlns); + for (final ref in references) { + final sims = ref.firstTag("media-sharing", xmlns: simsXmlns); + if (sims != null) return state.copyWith(sims: parseSIMSElement(sims)); + } + + return state; + } +} diff --git a/lib/xmpp/xeps/xep_0447.dart b/lib/xmpp/xeps/xep_0447.dart index 688b603e..e2dd0325 100644 --- a/lib/xmpp/xeps/xep_0447.dart +++ b/lib/xmpp/xeps/xep_0447.dart @@ -1,5 +1,10 @@ import "package:moxxyv2/xmpp/stringxml.dart"; +import "package:moxxyv2/xmpp/stanza.dart"; import "package:moxxyv2/xmpp/namespaces.dart"; +import "package:moxxyv2/xmpp/managers/base.dart"; +import "package:moxxyv2/xmpp/managers/data.dart"; +import "package:moxxyv2/xmpp/managers/namespaces.dart"; +import "package:moxxyv2/xmpp/managers/handlers.dart"; import "package:moxxyv2/xmpp/xeps/xep_0446.dart"; class StatelessFileSharingData { @@ -23,3 +28,31 @@ StatelessFileSharingData parseSFSElement(XMLNode node) { url: url ); } + +class SFSManager extends XmppManagerBase { + @override + String getName() => "SFSManager"; + + @override + String getId() => sfsManager; + + @override + List getIncomingStanzaHandlers() => [ + StanzaHandler( + stanzaTag: "message", + tagName: "file-sharing", + tagXmlns: sfsXmlns, + callback: _onMessage, + // Before the message handler + priority: -99, + ) + ]; + + Future _onMessage(Stanza message, StanzaHandlerData state) async { + final sfs = message.firstTag("file-sharing", xmlns: sfsXmlns)!; + + return state.copyWith( + sfs: parseSFSElement(sfs) + ); + } +} diff --git a/test/stanzahandler_test.dart b/test/stanzahandler_test.dart index aa1cf361..c59eac62 100644 --- a/test/stanzahandler_test.dart +++ b/test/stanzahandler_test.dart @@ -1,8 +1,9 @@ import "package:test/test.dart"; import "package:moxxyv2/xmpp/stanza.dart"; -import "package:moxxyv2/xmpp/managers/handlers.dart"; import "package:moxxyv2/xmpp/stringxml.dart"; +import "package:moxxyv2/xmpp/managers/handlers.dart"; +import "package:moxxyv2/xmpp/managers/data.dart"; final stanza1 = Stanza.iq(children: [ XMLNode.xmlns(tag: "tag", xmlns: "owo") @@ -13,7 +14,7 @@ final stanza2 = Stanza.message(children: [ void main() { test("match all", () { - final handler = StanzaHandler(callback: (_) async => true); + final handler = StanzaHandler(callback: (stanza, _) async => StanzaHandlerData(true,stanza)); expect(handler.matches(Stanza.iq()), true); expect(handler.matches(Stanza.message()), true); @@ -22,7 +23,10 @@ void main() { expect(handler.matches(stanza2), true); }); test("xmlns matching", () { - final handler = StanzaHandler(callback: (_) async => true, tagXmlns: "owo"); + final handler = StanzaHandler( + callback: (stanza, _) async => StanzaHandlerData(true, stanza), + tagXmlns: "owo" + ); expect(handler.matches(Stanza.iq()), false); expect(handler.matches(Stanza.message()), false); @@ -32,9 +36,9 @@ void main() { }); test("stanzaTag matching", () { bool run = false; - final handler = StanzaHandler(callback: (_) async { + final handler = StanzaHandler(callback: (stanza, _) async { run = true; - return true; + return StanzaHandlerData(true, stanza); }, stanzaTag: "iq"); expect(handler.matches(Stanza.iq()), true); @@ -43,11 +47,14 @@ void main() { expect(handler.matches(stanza1), true); expect(handler.matches(stanza2), false); - handler.callback(stanza2); + handler.callback(stanza2, StanzaHandlerData(false, stanza2)); expect(run, true); }); test("tagName matching", () { - final handler = StanzaHandler(callback: (_) async => true, tagName: "tag"); + final handler = StanzaHandler( + callback: (stanza, _) async => StanzaHandlerData(true, stanza), + tagName: "tag" + ); expect(handler.matches(Stanza.iq()), false); expect(handler.matches(Stanza.message()), false); @@ -56,7 +63,12 @@ void main() { expect(handler.matches(stanza2), false); }); test("combined matching", () { - final handler = StanzaHandler(callback: (_) async => true, tagName: "tag", stanzaTag: "iq", tagXmlns: "owo"); + final handler = StanzaHandler( + callback: (stanza, _) async => StanzaHandlerData(true, stanza), + tagName: "tag", + stanzaTag: "iq", + tagXmlns: "owo" + ); expect(handler.matches(Stanza.iq()), false); expect(handler.matches(Stanza.message()), false); @@ -64,4 +76,18 @@ void main() { expect(handler.matches(stanza1), true); expect(handler.matches(stanza2), false); }); + + test("sorting", () { + final handlerList = [ + StanzaHandler(callback: (stanza, _) async => StanzaHandlerData(true, stanza), tagName: "1", priority: 100), + StanzaHandler(callback: (stanza, _) async => StanzaHandlerData(true, stanza), tagName: "2"), + StanzaHandler(callback: (stanza, _) async => StanzaHandlerData(true, stanza), tagName: "3", priority: 50) + ]; + + handlerList.sort(stanzaHandlerSortComparator); + + expect(handlerList[0].tagName, "1"); + expect(handlerList[1].tagName, "3"); + expect(handlerList[2].tagName, "2"); + }); } diff --git a/test/xeps/xep_0198_test.dart b/test/xeps/xep_0198_test.dart index 1f55e60f..f707088b 100644 --- a/test/xeps/xep_0198_test.dart +++ b/test/xeps/xep_0198_test.dart @@ -1,9 +1,10 @@ -import "package:moxxyv2/xmpp/managers/attributes.dart"; import "package:moxxyv2/xmpp/stringxml.dart"; import "package:moxxyv2/xmpp/events.dart"; import "package:moxxyv2/xmpp/stanza.dart"; import "package:moxxyv2/xmpp/settings.dart"; import "package:moxxyv2/xmpp/jid.dart"; +import "package:moxxyv2/xmpp/managers/attributes.dart"; +import "package:moxxyv2/xmpp/managers/data.dart"; import "package:moxxyv2/xmpp/xeps/xep_0198/xep_0198.dart"; import "package:moxxyv2/xmpp/xeps/xep_0198/state.dart"; @@ -11,6 +12,18 @@ import "../helpers/xml.dart"; import "package:test/test.dart"; +Future runIncomingStanzaHandlers(StreamManagementManager man, Stanza stanza) async { + for (final handler in man.getIncomingStanzaHandlers()) { + if (handler.matches(stanza)) await handler.callback(stanza, StanzaHandlerData(false, stanza)); + } +} + +Future runOutgoingStanzaHandlers(StreamManagementManager man, Stanza stanza) async { + for (final handler in man.getOutgoingStanzaHandlers()) { + if (handler.matches(stanza)) await handler.callback(stanza, StanzaHandlerData(false, stanza)); + } +} + void main() { final stanza = Stanza( to: "some.user@server.example", @@ -46,14 +59,14 @@ void main() { manager.onXmppEvent(StreamManagementEnabledEvent(id: "0", resource: "h")); // Receive a fake stanza - await manager.runStanzaHandlers(stanza); - await manager.runStanzaHandlers(stanza); + await runIncomingStanzaHandlers(manager, stanza); + await runIncomingStanzaHandlers(manager, stanza); expect(manager.state.s2c, 2, reason: "The S2C counter must count correctly"); // Send some fake stanzas - manager.onXmppEvent(StanzaSentEvent(stanza: stanza)); - manager.onXmppEvent(StanzaSentEvent(stanza: stanza)); - manager.onXmppEvent(StanzaSentEvent(stanza: stanza)); + runOutgoingStanzaHandlers(manager, stanza); + runOutgoingStanzaHandlers(manager, stanza); + runOutgoingStanzaHandlers(manager, stanza); expect(manager.state.c2s, 3, reason: "The C2S counter must count correctly"); final ack = XMLNode.xmlns(tag: "a", xmlns: "urn:xmpp:sm:3", attributes: { "h": "3" }); @@ -63,7 +76,7 @@ void main() { expect(manager.state.s2c, 2, reason: "Sending stanzas must not change the S2C counter"); // Send a stanza which we will not acknowledge - manager.onXmppEvent(StanzaSentEvent(stanza: stanza)); + runOutgoingStanzaHandlers(manager, stanza); expect(manager.state.c2s, 4, reason: "Sending a stanza must increment the C2S counter"); await manager.runNonzaHandlers(ack); manager.onTimerElapsed(null, ignoreTimestamps: true); @@ -101,9 +114,9 @@ void main() { manager.register(attributes); // Send some stanzas - manager.onXmppEvent(StanzaSentEvent(stanza: stanza)); - manager.onXmppEvent(StanzaSentEvent(stanza: stanza)); - manager.onXmppEvent(StanzaSentEvent(stanza: stanza)); + runOutgoingStanzaHandlers(manager, stanza); + runOutgoingStanzaHandlers(manager, stanza); + runOutgoingStanzaHandlers(manager, stanza); // Simulate a resumption manager.onXmppEvent(StreamResumedEvent(h: 2)); diff --git a/test/xmpp_test.dart b/test/xmpp_test.dart index 12d1c9c7..eced1a0b 100644 --- a/test/xmpp_test.dart +++ b/test/xmpp_test.dart @@ -9,8 +9,9 @@ import "package:moxxyv2/xmpp/presence.dart"; import "package:moxxyv2/xmpp/roster.dart"; import "package:moxxyv2/xmpp/events.dart"; import "package:moxxyv2/xmpp/managers/attributes.dart"; -import "package:moxxyv2/xmpp/managers/handlers.dart"; +import "package:moxxyv2/xmpp/managers/data.dart"; import "package:moxxyv2/xmpp/xeps/xep_0030/xep_0030.dart"; +import "package:moxxyv2/xmpp/xeps/xep_0030/cachemanager.dart"; import "helpers/xmpp.dart"; @@ -40,14 +41,9 @@ Future testRosterManager(String bareJid, String resource, String stanzaStr )); final stanza = Stanza.fromXMLNode(XMLNode.fromString(stanzaString)); - await Future.forEach( - roster.getStanzaHandlers(), - (StanzaHandler handler) async { - if (handler.matches(stanza)) { - await handler.callback(stanza); - } - } - ); + for (final handler in roster.getIncomingStanzaHandlers()) { + if (handler.matches(stanza)) await handler.callback(stanza, StanzaHandlerData(false, stanza)); + } return eventTriggered; } @@ -216,6 +212,7 @@ void main() { ), ] ); + // TODO: This test is broken since we query the server and enable carbons final XmppConnection conn = XmppConnection(socket: fakeSocket); conn.setConnectionSettings(ConnectionSettings( jid: JID.fromString("polynomdivision@test.server"), @@ -225,6 +222,7 @@ void main() { )); conn.registerManager(RosterManager()); conn.registerManager(DiscoManager()); + conn.registerManager(DiscoCacheManager()); conn.registerManager(PresenceManager()); await conn.connect(); @@ -509,14 +507,10 @@ void main() { // NOTE: Based on https://gultsch.de/gajim_roster_push_and_message_interception.html // NOTE: Added a from attribute as a server would add it itself. final maliciousStanza = Stanza.fromXMLNode(XMLNode.fromString("")); - await Future.forEach( - roster.getStanzaHandlers(), - (StanzaHandler handler) async { - if (handler.matches(maliciousStanza)) { - await handler.callback(maliciousStanza); - } - } - ); + + for (final handler in roster.getIncomingStanzaHandlers()) { + if (handler.matches(maliciousStanza)) await handler.callback(maliciousStanza, StanzaHandlerData(false, maliciousStanza)); + } expect(eventTriggered, false, reason: "Was able to inject a malicious roster push"); });