moxxmpp/packages/moxxmpp/lib/src/xeps/xep_0280.dart

283 lines
7.6 KiB
Dart

import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:moxxmpp/src/connection.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/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/negotiators/sasl2.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
import 'package:moxxmpp/src/xeps/xep_0297.dart';
import 'package:moxxmpp/src/xeps/xep_0386.dart';
/// This manager class implements support for XEP-0280.
class CarbonsManager extends XmppManagerBase {
CarbonsManager() : super(carbonsManager);
/// Indicates that message carbons are enabled.
bool _isEnabled = false;
/// Indicates that the server supports message carbons.
bool _supported = false;
/// Indicates that we know that [CarbonsManager._supported] is accurate.
bool _gotSupported = false;
@override
List<StanzaHandler> getIncomingPreStanzaHandlers() => [
StanzaHandler(
stanzaTag: 'message',
tagName: 'received',
tagXmlns: carbonsXmlns,
callback: _onMessageReceived,
priority: -98,
),
StanzaHandler(
stanzaTag: 'message',
tagName: 'sent',
tagXmlns: carbonsXmlns,
callback: _onMessageSent,
priority: -98,
)
];
@override
Future<bool> isSupported() async {
if (_gotSupported) return _supported;
// Query the server
final disco = getAttributes().getManagerById<DiscoManager>(discoManager)!;
_supported = await disco.supportsFeature(
getAttributes().getConnectionSettings().jid.toBare(),
carbonsXmlns,
);
_gotSupported = true;
return _supported;
}
@override
Future<void> onXmppEvent(XmppEvent event) async {
if (event is StreamNegotiationsDoneEvent) {
// Reset disco cache info on a new stream
final newStream = await isNewStream();
if (newStream) {
_gotSupported = false;
_supported = false;
}
}
}
Future<StanzaHandlerData> _onMessageReceived(
Stanza message,
StanzaHandlerData state,
) async {
final from = JID.fromString(message.attributes['from']! as String);
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<StanzaHandlerData> _onMessageSent(
Stanza message,
StanzaHandlerData state,
) async {
final from = JID.fromString(message.attributes['from']! as String);
final sent = message.firstTag('sent', xmlns: carbonsXmlns)!;
if (!isCarbonValid(from)) return state.copyWith(done: true);
final forwarded = sent.firstTag('forwarded', xmlns: forwardedXmlns)!;
final carbon = unpackForwarded(forwarded);
return state.copyWith(
isCarbon: true,
stanza: carbon,
);
}
/// Send a request to the server, asking it to enable Message Carbons.
///
/// Returns true if carbons were enabled. False, if not.
Future<bool> enableCarbons() async {
final attrs = getAttributes();
final result = await attrs.sendStanza(
Stanza.iq(
to: attrs.getFullJID().toBare().toString(),
type: 'set',
children: [
XMLNode.xmlns(
tag: 'enable',
xmlns: carbonsXmlns,
)
],
),
addFrom: StanzaFromType.full,
addId: true,
);
if (result.attributes['type'] != 'result') {
logger.warning('Failed to enable message carbons');
return false;
}
logger.fine('Successfully enabled message carbons');
_isEnabled = true;
return true;
}
/// Send a request to the server, asking it to disable Message Carbons.
///
/// Returns true if carbons were disabled. False, if not.
Future<bool> disableCarbons() async {
final result = await getAttributes().sendStanza(
Stanza.iq(
type: 'set',
children: [
XMLNode.xmlns(
tag: 'disable',
xmlns: carbonsXmlns,
)
],
),
addFrom: StanzaFromType.full,
addId: true,
);
if (result.attributes['type'] != 'result') {
logger.warning('Failed to disable message carbons');
return false;
}
logger.fine('Successfully disabled message carbons');
_isEnabled = false;
return true;
}
/// True if Message Carbons are enabled. False, if not.
bool get isEnabled => _isEnabled;
@visibleForTesting
void forceEnable() {
_isEnabled = true;
}
@internal
void setEnabled() {
_isEnabled = true;
}
@internal
void setDisabled() {
_isEnabled = false;
}
/// Checks if a carbon sent by [senderJid] is valid to prevent vulnerabilities like
/// the ones listed at https://xmpp.org/extensions/xep-0280.html#security.
///
/// Returns true if the carbon is valid. Returns false if not.
bool isCarbonValid(JID senderJid) {
return _isEnabled &&
getAttributes().getFullJID().bareCompare(
senderJid,
ensureBare: true,
);
}
}
class CarbonsNegotiator extends Sasl2FeatureNegotiator
implements Bind2FeatureNegotiator {
CarbonsNegotiator() : super(0, false, carbonsXmlns, carbonsNegotiator);
/// Flag indicating whether we requested to enable carbons inline (true) or not
/// (false).
bool _requestedEnablement = false;
/// Logger
final Logger _log = Logger('CarbonsNegotiator');
@override
bool canInlineFeature(List<XMLNode> features) => true;
@override
Future<List<XMLNode>> onSasl2FeaturesReceived(XMLNode sasl2Features) async {
return [];
}
@override
Future<Result<bool, NegotiatorError>> onSasl2Success(XMLNode response) async {
if (_requestedEnablement) {
final enabled = response
.firstTag('bound', xmlns: bind2Xmlns)
?.firstTag('enabled', xmlns: carbonsXmlns);
final cm = attributes.getManagerById<CarbonsManager>(carbonsManager)!;
if (enabled != null) {
_log.finest('Successfully enabled Message Carbons inline');
cm.setEnabled();
} else {
_log.warning('Failed to enable Message Carbons inline');
cm.setDisabled();
}
}
return const Result(true);
}
@override
Future<Result<NegotiatorState, NegotiatorError>> negotiate(
XMLNode nonza,
) async {
return const Result(NegotiatorState.done);
}
@override
Future<List<XMLNode>> onBind2FeaturesReceived(
List<String> bind2Features) async {
if (!bind2Features.contains(carbonsXmlns)) {
return [];
}
_requestedEnablement = true;
return [
XMLNode.xmlns(
tag: 'enable',
xmlns: carbonsXmlns,
),
];
}
@override
Future<void> postRegisterCallback() async {
attributes
.getNegotiatorById<Sasl2Negotiator>(sasl2Negotiator)
?.registerNegotiator(this);
attributes
.getNegotiatorById<Bind2Negotiator>(bind2Negotiator)
?.registerNegotiator(this);
}
@override
void reset() {
_requestedEnablement = false;
super.reset();
}
}