190 lines
7.8 KiB
Dart
190 lines
7.8 KiB
Dart
import "dart:async";
|
|
|
|
import "package:moxxyv2/xmpp/connection.dart";
|
|
import "package:moxxyv2/xmpp/settings.dart";
|
|
import "package:moxxyv2/xmpp/namespaces.dart";
|
|
import "package:moxxyv2/xmpp/nonzas/stream.dart";
|
|
import "package:moxxyv2/xmpp/stringxml.dart";
|
|
import "package:moxxyv2/xmpp/sasl/scram.dart";
|
|
import "package:moxxyv2/xmpp/jid.dart";
|
|
import "package:moxxyv2/xmpp/xeps/0368.dart";
|
|
|
|
import "package:test/test.dart";
|
|
import "package:xml/xml.dart";
|
|
import "package:hex/hex.dart";
|
|
|
|
class FakeSocket implements SocketWrapper {
|
|
int state;
|
|
final StreamController<String> _streamController = StreamController<String>();
|
|
final String server;
|
|
|
|
FakeSocket({ required this.server }) : state = 0;
|
|
|
|
@override
|
|
Future<void> connect(String host, int port) async {}
|
|
|
|
@override
|
|
Stream<String> asBroadcastStream() {
|
|
return this._streamController.stream.asBroadcastStream();
|
|
}
|
|
|
|
@override
|
|
void write(Object? object) {
|
|
final str = object as String;
|
|
|
|
print("==> " + str);
|
|
|
|
switch (this.state) {
|
|
case 0: {
|
|
this.state++;
|
|
expect(str, "<?xml version='1.0'?><stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='${this.server}' xml:lang='en'>");
|
|
|
|
this._streamController.add("<stream:stream xmlns:stream='http://etherx.jabber.org/streams' xmlns='jabber:client' from='${this.server}' xml:lang='en' version='1.0' id='aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'><stream:features xmlns='http://etherx.jabber.org/streams'><mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><mechanism>PLAIN</mechanism></mechanisms></stream:features>");
|
|
}
|
|
break;
|
|
case 1: {
|
|
this.state++;
|
|
expect(str, "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>AHBvbHlub21kaXZpc2lvbgBhYWFh</auth>");
|
|
|
|
this._streamController.add("<success xmlns='$SASL_XMLNS' />");
|
|
}
|
|
break;
|
|
case 2: {
|
|
this.state++;
|
|
expect(str, "<?xml version='1.0'?><stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='${this.server}' xml:lang='en'>");
|
|
|
|
this._streamController.add("<stream:stream xmlns:stream='http://etherx.jabber.org/streams' from='${this.server}' xmlns='jabber:client' version='1.0' id='aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' xml:lang='en'><stream:features><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><required/></bind><session xmlns='urn:ietf:params:xml:ns:xmpp-session'><optional/></session><ver xmlns='urn:xmpp:features:rosterver'/><c hash='sha-1' ver='e6y9LzWVyTcm31DV0THfhNwlHZo=' node='http://prosody.im' xmlns='http://jabber.org/protocol/caps'/><csi xmlns='urn:xmpp:csi:0'/></stream:features>");
|
|
}
|
|
break;
|
|
case 3: {
|
|
this.state++;
|
|
expect(
|
|
compareXMLNodes(
|
|
fromString(str),
|
|
fromString("<iq xmlns='jabber:client' id='aaaaaaaaaa' type='set'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind' /></iq>")),
|
|
true);
|
|
|
|
this._streamController.add("<iq type='result' id='aaaaaaaaaa'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><jid>polynomdivision@test.server/MU29eEZn</jid></bind></iq>");
|
|
}
|
|
break;
|
|
case 4: {
|
|
this.state++;
|
|
expect(str, "<presence xmlns='jabber:client' from='polynomdivision@test.server/MU29eEZn'><show>show</show></presence>");
|
|
|
|
this._streamController.add("<presence /><message />");
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
XMLNode fromString(String str) {
|
|
return XMLNode.fromXmlElement(XmlDocument.parse(str).firstElementChild!);
|
|
}
|
|
|
|
bool compareXMLNodes(XMLNode actual, XMLNode expectation, { bool ignoreId = true}) {
|
|
// Compare attributes
|
|
if (expectation.tag != actual.tag) return false;
|
|
|
|
final attributesEqual = expectation.attributes.keys.every((key) {
|
|
// Ignore the stanza ID
|
|
if (key == "id" && ignoreId) return true;
|
|
|
|
return actual.attributes[key] == expectation.attributes[key];
|
|
});
|
|
if (!attributesEqual) return false;
|
|
if (actual.attributes.length != expectation.attributes.length) return false;
|
|
|
|
if (expectation.innerText() != "" && actual.innerText() != expectation.innerText()) return false;
|
|
|
|
return expectation.children.every((childe) {
|
|
return actual.children.any((childa) => compareXMLNodes(childa, childe));
|
|
});
|
|
}
|
|
|
|
void main() {
|
|
test("Test SASL PLAIN", () async {
|
|
final fakeSocket = FakeSocket(server: "test.server");
|
|
final XmppConnection conn = XmppConnection(socket: fakeSocket, settings: ConnectionSettings(
|
|
jid: BareJID.fromString("polynomdivision@test.server"),
|
|
password: "aaaa",
|
|
useDirectTLS: true,
|
|
allowPlainAuth: true
|
|
));
|
|
await conn.connect();
|
|
await Future.delayed(Duration(seconds: 3), () {
|
|
expect(fakeSocket.state, 4);
|
|
});
|
|
});
|
|
|
|
test("Test XMPP Scram-Sha-1", () async {
|
|
|
|
final challenge = ServerChallenge.fromBase64("cj02ZDQ0MmI1ZDllNTFhNzQwZjM2OWUzZGNlY2YzMTc4ZWMxMmIzOTg1YmJkNGE4ZTZmODE0YjQyMmFiNzY2NTczLHM9UVNYQ1IrUTZzZWs4YmY5MixpPTQwOTY=");
|
|
expect(challenge.nonce, "6d442b5d9e51a740f369e3dcecf3178ec12b3985bbd4a8e6f814b422ab766573");
|
|
expect(challenge.salt, "QSXCR+Q6sek8bf92");
|
|
expect(challenge.iterations, 4096);
|
|
|
|
final negotiator = SaslScramNegotiator(
|
|
settings: ConnectionSettings(jid: BareJID.fromString("user@server"), password: "pencil", useDirectTLS: true, allowPlainAuth: true),
|
|
clientNonce: "fyko+d2lbbFgONRv9qkxdawL",
|
|
initialMessageNoGS2: "n=user,r=fyko+d2lbbFgONRv9qkxdawL",
|
|
sendRawXML: (data) {},
|
|
hashType: ScramHashType.SHA1
|
|
);
|
|
|
|
expect(
|
|
HEX.encode(await negotiator.calculateSaltedPassword("QSXCR+Q6sek8bf92", 4096)),
|
|
"1d96ee3a529b5a5f9e47c01f229a2cb8a6e15f7d"
|
|
);
|
|
expect(
|
|
HEX.encode(
|
|
await negotiator.calculateClientKey(HEX.decode("1d96ee3a529b5a5f9e47c01f229a2cb8a6e15f7d"))
|
|
),
|
|
"e234c47bf6c36696dd6d852b99aaa2ba26555728"
|
|
);
|
|
final String authMessage = "n=user,r=fyko+d2lbbFgONRv9qkxdawL,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096,c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j";
|
|
expect(
|
|
HEX.encode(
|
|
await negotiator.calculateClientSignature(authMessage, HEX.decode("e9d94660c39d65c38fbad91c358f14da0eef2bd6"))
|
|
),
|
|
"5d7138c486b0bfabdf49e3e2da8bd6e5c79db613"
|
|
);
|
|
expect(
|
|
HEX.encode(
|
|
negotiator.calculateClientProof(HEX.decode("e234c47bf6c36696dd6d852b99aaa2ba26555728"), HEX.decode("5d7138c486b0bfabdf49e3e2da8bd6e5c79db613"))
|
|
),
|
|
"bf45fcbf7073d93d022466c94321745fe1c8e13b"
|
|
);
|
|
expect(
|
|
HEX.encode(
|
|
await negotiator.calculateServerSignature(authMessage, HEX.decode("0fe09258b3ac852ba502cc62ba903eaacdbf7d31"))
|
|
),
|
|
"ae617da6a57c4bbb2e0286568dae1d251905b0a4"
|
|
);
|
|
expect(
|
|
HEX.encode(
|
|
await negotiator.calculateServerKey(HEX.decode("1d96ee3a529b5a5f9e47c01f229a2cb8a6e15f7d"))
|
|
),
|
|
"0fe09258b3ac852ba502cc62ba903eaacdbf7d31"
|
|
);
|
|
expect(
|
|
HEX.encode(
|
|
negotiator.calculateClientProof(
|
|
HEX.decode("e234c47bf6c36696dd6d852b99aaa2ba26555728"),
|
|
HEX.decode("5d7138c486b0bfabdf49e3e2da8bd6e5c79db613")
|
|
)
|
|
),
|
|
"bf45fcbf7073d93d022466c94321745fe1c8e13b"
|
|
);
|
|
|
|
expect(await negotiator.calculateChallengeResponse("cj1meWtvK2QybGJiRmdPTlJ2OXFreGRhd0wzcmZjTkhZSlkxWlZ2V1ZzN2oscz1RU1hDUitRNnNlazhiZjkyLGk9NDA5Ng=="), "c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=");
|
|
});
|
|
|
|
test("Test bare JIDs", () {
|
|
expect(BareJID.fromString("hallo@welt").toString(), "hallo@welt");
|
|
expect(BareJID.fromString("@welt").toString(), "@welt");
|
|
expect(BareJID.fromString("hallo@").toString(), "hallo@");
|
|
expect(BareJID.fromString("hallo@welt/whatever").toString(), "hallo@welt");
|
|
});
|
|
}
|