376 lines
15 KiB
Dart
376 lines
15 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:moxxyv2/xmpp/connection.dart';
|
|
import 'package:moxxyv2/xmpp/events.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/negotiators/resource_binding.dart';
|
|
import 'package:moxxyv2/xmpp/negotiators/sasl/plain.dart';
|
|
import 'package:moxxyv2/xmpp/negotiators/sasl/scram.dart';
|
|
import 'package:moxxyv2/xmpp/ping.dart';
|
|
import 'package:moxxyv2/xmpp/presence.dart';
|
|
import 'package:moxxyv2/xmpp/reconnect.dart';
|
|
import 'package:moxxyv2/xmpp/roster.dart';
|
|
import 'package:moxxyv2/xmpp/settings.dart';
|
|
import 'package:moxxyv2/xmpp/stanza.dart';
|
|
import 'package:moxxyv2/xmpp/stringxml.dart';
|
|
import 'package:moxxyv2/xmpp/xeps/xep_0030/xep_0030.dart';
|
|
import 'package:moxxyv2/xmpp/xeps/xep_0198/negotiator.dart';
|
|
import 'package:moxxyv2/xmpp/xeps/xep_0198/xep_0198.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
import 'helpers/logging.dart';
|
|
import 'helpers/xmpp.dart';
|
|
|
|
/// Returns true if the roster manager triggeres an event for a given stanza
|
|
Future<bool> testRosterManager(String bareJid, String resource, String stanzaString) async {
|
|
var eventTriggered = false;
|
|
final roster = RosterManager();
|
|
roster.register(XmppManagerAttributes(
|
|
sendStanza: (_, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool retransmitted = false, bool awaitable = true }) async => XMLNode(tag: 'hallo'),
|
|
sendEvent: (event) {
|
|
eventTriggered = true;
|
|
},
|
|
sendNonza: (_) {},
|
|
sendRawXml: (_) {},
|
|
getConnectionSettings: () => ConnectionSettings(
|
|
jid: JID.fromString(bareJid),
|
|
password: 'password',
|
|
useDirectTLS: true,
|
|
allowPlainAuth: false,
|
|
),
|
|
getManagerById: getManagerNullStub,
|
|
getNegotiatorById: getNegotiatorNullStub,
|
|
isFeatureSupported: (_) => false,
|
|
getFullJID: () => JID.fromString('$bareJid/$resource'),
|
|
getSocket: () => StubTCPSocket(play: []),
|
|
getConnection: () => XmppConnection(TestingReconnectionPolicy()),
|
|
),);
|
|
|
|
final stanza = Stanza.fromXMLNode(XMLNode.fromString(stanzaString));
|
|
for (final handler in roster.getIncomingStanzaHandlers()) {
|
|
if (handler.matches(stanza)) await handler.callback(stanza, StanzaHandlerData(false, stanza));
|
|
}
|
|
|
|
return eventTriggered;
|
|
}
|
|
|
|
void main() {
|
|
initLogger();
|
|
|
|
test('Test a successful login attempt with no SM', () async {
|
|
final fakeSocket = StubTCPSocket(
|
|
play: [
|
|
StringExpectation(
|
|
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
|
|
'''
|
|
<stream:stream
|
|
xmlns="jabber:client"
|
|
version="1.0"
|
|
xmlns:stream="http://etherx.jabber.org/streams"
|
|
from="test.server"
|
|
xml:lang="en">
|
|
<stream:features xmlns="http://etherx.jabber.org/streams">
|
|
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
|
|
<mechanism>PLAIN</mechanism>
|
|
</mechanisms>
|
|
</stream:features>''',
|
|
),
|
|
StringExpectation(
|
|
"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>AHBvbHlub21kaXZpc2lvbgBhYWFh</auth>",
|
|
'<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />'
|
|
),
|
|
StringExpectation(
|
|
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
|
|
'''
|
|
<stream:stream
|
|
xmlns="jabber:client"
|
|
version="1.0"
|
|
xmlns:stream="http://etherx.jabber.org/streams"
|
|
from="test.server"
|
|
xml:lang="en">
|
|
<stream:features xmlns="http://etherx.jabber.org/streams">
|
|
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
|
|
<required/>
|
|
</bind>
|
|
<session xmlns="urn:ietf:params:xml:ns:xmpp-session">
|
|
<optional/>
|
|
</session>
|
|
<csi xmlns="urn:xmpp:csi:0"/>
|
|
<sm xmlns="urn:xmpp:sm:3"/>
|
|
</stream:features>
|
|
''',
|
|
),
|
|
StanzaExpectation(
|
|
'<iq xmlns="jabber:client" type="set" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/></iq>',
|
|
'<iq xmlns="jabber:client" type="result" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>polynomdivision@test.server/MU29eEZn</jid></bind></iq>',
|
|
ignoreId: true,
|
|
),
|
|
/*
|
|
Expectation(
|
|
XMLNode.xmlns(
|
|
tag: 'presence',
|
|
xmlns: 'jabber:client',
|
|
attributes: { 'from': 'polynomdivision@test.server/MU29eEZn' },
|
|
children: [
|
|
XMLNode(
|
|
tag: 'show',
|
|
text: 'chat',
|
|
),
|
|
XMLNode.xmlns(
|
|
tag: 'c',
|
|
xmlns: 'http://jabber.org/protocol/caps',
|
|
attributes: {
|
|
// TODO: Somehow make the test ignore this attribute
|
|
'ver': 'QRTBC5cg/oYd+UOTYazSQR4zb/I=',
|
|
'node': 'http://moxxy.im',
|
|
'hash': 'sha-1'
|
|
},
|
|
)
|
|
],
|
|
),
|
|
XMLNode(
|
|
tag: 'presence',
|
|
),
|
|
),
|
|
*/
|
|
],
|
|
);
|
|
// TODO: This test is broken since we query the server and enable carbons
|
|
final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), socket: fakeSocket);
|
|
conn.setConnectionSettings(ConnectionSettings(
|
|
jid: JID.fromString('polynomdivision@test.server'),
|
|
password: 'aaaa',
|
|
useDirectTLS: true,
|
|
allowPlainAuth: true,
|
|
),);
|
|
conn.registerManagers([
|
|
PresenceManager(),
|
|
RosterManager(),
|
|
DiscoManager(),
|
|
PingManager(),
|
|
StreamManagementManager(),
|
|
]);
|
|
conn.registerFeatureNegotiators(
|
|
[
|
|
SaslPlainNegotiator(),
|
|
SaslScramNegotiator(10, '', '', ScramHashType.sha512),
|
|
ResourceBindingNegotiator(),
|
|
StreamManagementNegotiator(),
|
|
]
|
|
);
|
|
|
|
await conn.connect();
|
|
await Future.delayed(const Duration(seconds: 3), () {
|
|
expect(fakeSocket.getState(), /*6*/ 5);
|
|
});
|
|
});
|
|
|
|
test('Test a failed SASL auth', () async {
|
|
final fakeSocket = StubTCPSocket(
|
|
play: [
|
|
StringExpectation(
|
|
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
|
|
'''
|
|
<stream:stream
|
|
xmlns="jabber:client"
|
|
version="1.0"
|
|
xmlns:stream="http://etherx.jabber.org/streams"
|
|
from="test.server"
|
|
xml:lang="en">
|
|
<stream:features xmlns="http://etherx.jabber.org/streams">
|
|
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
|
|
<mechanism>PLAIN</mechanism>
|
|
</mechanisms>
|
|
</stream:features>''',
|
|
),
|
|
StringExpectation(
|
|
"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>AHBvbHlub21kaXZpc2lvbgBhYWFh</auth>",
|
|
'<failure xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><not-authorized /></failure>'
|
|
),
|
|
],
|
|
);
|
|
var receivedEvent = false;
|
|
final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), socket: fakeSocket);
|
|
conn.setConnectionSettings(ConnectionSettings(
|
|
jid: JID.fromString('polynomdivision@test.server'),
|
|
password: 'aaaa',
|
|
useDirectTLS: true,
|
|
allowPlainAuth: true,
|
|
),);
|
|
conn.registerManagers([
|
|
PresenceManager(),
|
|
RosterManager(),
|
|
DiscoManager(),
|
|
PingManager(),
|
|
]);
|
|
conn.registerFeatureNegotiators([
|
|
SaslPlainNegotiator()
|
|
]);
|
|
|
|
conn.asBroadcastStream().listen((event) {
|
|
if (event is AuthenticationFailedEvent && event.saslError == 'not-authorized') {
|
|
receivedEvent = true;
|
|
}
|
|
});
|
|
|
|
await conn.connect();
|
|
await Future.delayed(const Duration(seconds: 3), () {
|
|
expect(receivedEvent, true);
|
|
});
|
|
});
|
|
|
|
test('Test another failed SASL auth', () async {
|
|
final fakeSocket = StubTCPSocket(
|
|
play: [
|
|
StringExpectation(
|
|
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
|
|
'''
|
|
<stream:stream
|
|
xmlns="jabber:client"
|
|
version="1.0"
|
|
xmlns:stream="http://etherx.jabber.org/streams"
|
|
from="test.server"
|
|
xml:lang="en">
|
|
<stream:features xmlns="http://etherx.jabber.org/streams">
|
|
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
|
|
<mechanism>PLAIN</mechanism>
|
|
</mechanisms>
|
|
</stream:features>''',
|
|
),
|
|
StringExpectation(
|
|
"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>AHBvbHlub21kaXZpc2lvbgBhYWFh</auth>",
|
|
'<failure xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><mechanism-too-weak /></failure>',
|
|
),
|
|
],
|
|
);
|
|
var receivedEvent = false;
|
|
final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), socket: fakeSocket);
|
|
conn.setConnectionSettings(ConnectionSettings(
|
|
jid: JID.fromString('polynomdivision@test.server'),
|
|
password: 'aaaa',
|
|
useDirectTLS: true,
|
|
allowPlainAuth: true,
|
|
),);
|
|
conn.registerManagers([
|
|
PresenceManager(),
|
|
RosterManager(),
|
|
DiscoManager(),
|
|
PingManager(),
|
|
]);
|
|
conn.registerFeatureNegotiators([
|
|
SaslPlainNegotiator()
|
|
]);
|
|
|
|
conn.asBroadcastStream().listen((event) {
|
|
if (event is AuthenticationFailedEvent && event.saslError == 'mechanism-too-weak') {
|
|
receivedEvent = true;
|
|
}
|
|
});
|
|
|
|
await conn.connect();
|
|
await Future.delayed(const Duration(seconds: 3), () {
|
|
expect(receivedEvent, true);
|
|
});
|
|
});
|
|
|
|
/*test('Test choosing SCRAM-SHA-1', () async {
|
|
final fakeSocket = StubTCPSocket(
|
|
play: [
|
|
StringExpectation(
|
|
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
|
|
'''
|
|
<stream:stream
|
|
xmlns="jabber:client"
|
|
version="1.0"
|
|
xmlns:stream="http://etherx.jabber.org/streams"
|
|
from="test.server"
|
|
xml:lang="en">
|
|
<stream:features xmlns="http://etherx.jabber.org/streams">
|
|
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
|
|
<mechanism>PLAIN</mechanism>
|
|
<mechanism>SCRAM-SHA-1</mechanism>
|
|
</mechanisms>
|
|
</stream:features>''',
|
|
),
|
|
// TODO(Unknown): This test is currently broken
|
|
StringExpectation(
|
|
"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='SCRAM-SHA-1'>AHBvbHlub21kaXZpc2lvbgBhYWFh</auth>",
|
|
"..."
|
|
)
|
|
],
|
|
);
|
|
final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), socket: fakeSocket);
|
|
conn.setConnectionSettings(ConnectionSettings(
|
|
jid: JID.fromString('polynomdivision@test.server'),
|
|
password: 'aaaa',
|
|
useDirectTLS: true,
|
|
allowPlainAuth: false,
|
|
),);
|
|
conn.registerManagers([
|
|
PresenceManager(),
|
|
RosterManager(),
|
|
DiscoManager(),
|
|
PingManager(),
|
|
]);
|
|
conn.registerFeatureNegotiators([
|
|
SaslPlainNegotiator(),
|
|
SaslScramNegotiator(10, '', '', ScramHashType.sha1),
|
|
]);
|
|
|
|
await conn.connect();
|
|
await Future.delayed(const Duration(seconds: 3), () {
|
|
expect(fakeSocket.getState(), 2);
|
|
});
|
|
});*/
|
|
|
|
group('Test roster pushes', () {
|
|
test('Test for a CVE-2015-8688 style vulnerability', () async {
|
|
var eventTriggered = false;
|
|
final roster = RosterManager();
|
|
roster.register(XmppManagerAttributes(
|
|
sendStanza: (_, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool retransmitted = false, bool awaitable = true }) async => XMLNode(tag: 'hallo'),
|
|
sendEvent: (event) {
|
|
eventTriggered = true;
|
|
},
|
|
sendNonza: (_) {},
|
|
sendRawXml: (_) {},
|
|
getConnectionSettings: () => ConnectionSettings(
|
|
jid: JID.fromString('some.user@example.server'),
|
|
password: 'password',
|
|
useDirectTLS: true,
|
|
allowPlainAuth: false,
|
|
),
|
|
getManagerById: getManagerNullStub,
|
|
getNegotiatorById: getNegotiatorNullStub,
|
|
isFeatureSupported: (_) => false,
|
|
getFullJID: () => JID.fromString('some.user@example.server/aaaaa'),
|
|
getSocket: () => StubTCPSocket(play: []),
|
|
getConnection: () => XmppConnection(TestingReconnectionPolicy()),
|
|
),);
|
|
|
|
// 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("<iq type=\"set\" from=\"eve@siacs.eu/bbbbb\" to=\"some.user@example.server/aaaaa\"><query xmlns='jabber:iq:roster'><item subscription=\"both\" jid=\"eve@siacs.eu\" name=\"Bob\" /></query></iq>"));
|
|
|
|
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');
|
|
});
|
|
test('The manager should accept pushes from our bare jid', () async {
|
|
final result = await testRosterManager('test.user@server.example', 'aaaaa', "<iq from='test.user@server.example' type='result' id='82c2aa1e-cac3-4f62-9e1f-bbe6b057daf3' to='test.user@server.example/aaaaa' xmlns='jabber:client'><query ver='64' xmlns='jabber:iq:roster'><item jid='some.other.user@server.example' subscription='to' /></query></iq>");
|
|
expect(result, true, reason: 'Roster pushes from our bare JID should be accepted');
|
|
});
|
|
test('The manager should accept pushes from a jid that, if the resource is stripped, is our bare jid', () async {
|
|
final result1 = await testRosterManager('test.user@server.example', 'aaaaa', "<iq from='test.user@server.example/aaaaa' type='result' id='82c2aa1e-cac3-4f62-9e1f-bbe6b057daf3' to='test.user@server.example/aaaaa' xmlns='jabber:client'><query ver='64' xmlns='jabber:iq:roster'><item jid='some.other.user@server.example' subscription='to' /></query></iq>");
|
|
expect(result1, true, reason: 'Roster pushes should be accepted if the bare JIDs are the same');
|
|
|
|
final result2 = await testRosterManager('test.user@server.example', 'aaaaa', "<iq from='test.user@server.example/bbbbb' type='result' id='82c2aa1e-cac3-4f62-9e1f-bbe6b057daf3' to='test.user@server.example/aaaaa' xmlns='jabber:client'><query ver='64' xmlns='jabber:iq:roster'><item jid='some.other.user@server.example' subscription='to' /></query></iq>");
|
|
expect(result2, true, reason: 'Roster pushes should be accepted if the bare JIDs are the same');
|
|
});
|
|
});
|
|
}
|