Merge pull request 'SASL2 and friends' (#34) from feat/sasl2 into master
Reviewed-on: https://codeberg.org/moxxy/moxxmpp/pulls/34
This commit is contained in:
commit
2947e2c539
25
flake.lock
25
flake.lock
@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"locked": {
|
||||
"lastModified": 1667395993,
|
||||
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
|
||||
"lastModified": 1678901627,
|
||||
"narHash": "sha256-U02riOqrKKzwjsxc/400XnElV+UtPUQWpANPlyazjH0=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
|
||||
"rev": "93a2b84fc4b70d9e089d029deacc3583435c2ed6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -31,10 +31,27 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-unstable": {
|
||||
"locked": {
|
||||
"lastModified": 1680273054,
|
||||
"narHash": "sha256-Bs6/5LpvYp379qVqGt9mXxxx9GSE789k3oFc+OAL07M=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "3364b5b117f65fe1ce65a3cdd5612a078a3b31e3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
"nixpkgs": "nixpkgs",
|
||||
"nixpkgs-unstable": "nixpkgs-unstable"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
31
flake.nix
31
flake.nix
@ -2,10 +2,11 @@
|
||||
description = "moxxmpp";
|
||||
inputs = {
|
||||
nixpkgs.url = "github:AtaraxiaSjel/nixpkgs/update/flutter";
|
||||
nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let
|
||||
outputs = { self, nixpkgs, nixpkgs-unstable, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
config = {
|
||||
@ -13,6 +14,9 @@
|
||||
allowUnfree = true;
|
||||
};
|
||||
};
|
||||
unstable = import nixpkgs-unstable {
|
||||
inherit system;
|
||||
};
|
||||
android = pkgs.androidenv.composeAndroidPackages {
|
||||
# TODO: Find a way to pin these
|
||||
#toolsVersion = "26.1.1";
|
||||
@ -46,7 +50,26 @@
|
||||
};
|
||||
};
|
||||
|
||||
devShell = pkgs.mkShell {
|
||||
devShell = let
|
||||
prosody-newer-community-modules = unstable.prosody.overrideAttrs (old: {
|
||||
communityModules = pkgs.fetchhg {
|
||||
url = "https://hg.prosody.im/prosody-modules";
|
||||
rev = "e3a3a6c86a9f";
|
||||
sha256 = "sha256-C2x6PCv0sYuj4/SroDOJLsNPzfeNCodYKbMqmNodFrk=";
|
||||
};
|
||||
|
||||
src = pkgs.fetchhg {
|
||||
url = "https://hg.prosody.im/trunk";
|
||||
rev = "8a2f75e38eb2";
|
||||
sha256 = "sha256-zMNp9+wQ/hvUVyxFl76DqCVzQUPP8GkNdstiTDkG8Hw=";
|
||||
};
|
||||
});
|
||||
prosody-sasl2 = prosody-newer-community-modules.override {
|
||||
withCommunityModules = [
|
||||
"sasl2" "sasl2_fast" "sasl2_sm" "sasl2_bind2"
|
||||
];
|
||||
};
|
||||
in pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
flutter pinnedJDK android.platform-tools dart # Dart
|
||||
gitlint # Code hygiene
|
||||
@ -71,6 +94,10 @@
|
||||
|
||||
# For the scripts in ./scripts/
|
||||
pythonEnv
|
||||
|
||||
# For integration testing against a local prosody server
|
||||
prosody-sasl2
|
||||
mkcert
|
||||
];
|
||||
|
||||
CPATH = "${pkgs.xorg.libX11.dev}/include:${pkgs.xorg.xorgproto}/include";
|
||||
|
6
integration_tests/.gitignore
vendored
Normal file
6
integration_tests/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
# Files and directories created by pub.
|
||||
.dart_tool/
|
||||
.packages
|
||||
|
||||
# Conventional directory for build output.
|
||||
build/
|
5
integration_tests/README.md
Normal file
5
integration_tests/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Integration Tests
|
||||
|
||||
The included `./prosody.cfg.lua` config file must be used for integration testing.
|
||||
Additionally, ensure that a user `testuser@localhost` with the password `abc123`
|
||||
exists. Note that this currently requires prosody-trunk.
|
1
integration_tests/analysis_options.yaml
Normal file
1
integration_tests/analysis_options.yaml
Normal file
@ -0,0 +1 @@
|
||||
include: ../analysis_options.yaml
|
24
integration_tests/certs/localhost.crt
Normal file
24
integration_tests/certs/localhost.crt
Normal file
@ -0,0 +1,24 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEAzCCAmugAwIBAgIQd61NPnP8++X7h8a+85C6DjANBgkqhkiG9w0BAQsFADBZ
|
||||
MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExFzAVBgNVBAsMDmFsZXhh
|
||||
bmRlckBtaWt1MR4wHAYDVQQDDBVta2NlcnQgYWxleGFuZGVyQG1pa3UwHhcNMjMw
|
||||
NDAyMTM1ODIxWhcNMjUwNzAyMTM1ODIxWjBCMScwJQYDVQQKEx5ta2NlcnQgZGV2
|
||||
ZWxvcG1lbnQgY2VydGlmaWNhdGUxFzAVBgNVBAsMDmFsZXhhbmRlckBtaWt1MIIB
|
||||
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1DElEXPY+VDQP7cSikK0ne0K
|
||||
gDgorGYPG9R7lOeuPLHyFYYry78+hB037OT0BOyA2uTu1yrog0dI/4YGicPDIqXh
|
||||
IgHfjV+4kMi5SgO7ECWOBmZFqTC3bBwvbNtoW40aFjYSFaOkm/nnfp+nalEJJZ/N
|
||||
kSkD4gdT3pH1ClsovlI4BlsxeIoJtyGzxMidJVXDAqMNraLatzJBwnT3OEs93xTf
|
||||
7Kd1KUpQp9OZFrGi15zv/n6tCmrcC3xMOVHuYkhW0UCTFmev7ZqbghQsQ9N9s0E6
|
||||
kk9rUf9xtMNH4Af6+2YRkT1DAGQ6FkXl1nQdB5H5XRgOBl+3k9s8wUrxQvQddQID
|
||||
AQABo14wXDAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHwYD
|
||||
VR0jBBgwFoAU54aUZ+dytAOBTsYIdGtSnjiig/gwFAYDVR0RBA0wC4IJbG9jYWxo
|
||||
b3N0MA0GCSqGSIb3DQEBCwUAA4IBgQBU8p7Ua0Cs+lXlWmtCh2j+YF9R+dvc+3Iw
|
||||
dYEzCmYd375uxPctyHXW0yYjyuH9WuYn0F7OicEFEeC2+exHND+/z0J2Zv5yu34r
|
||||
SfgHVfvE/Vxisn9InYrUCVtfRwLDF3HgLyIlm8FVzIyiIANhpe6vJdqjEWTsiL2X
|
||||
I6hoDf1xlRgEqUx+Wxl2IFWrg+1SPPGTQzDPImiRlz8d+9ZJ9v48vaV5+aITMvDP
|
||||
Gfm/bnNXXd5Gf7nGwL8zFHiwLoYQ5AUYl0IfXYwFAXJ72+LjiRT33IOidVJF0gsQ
|
||||
6k9cTsc4lIrt4FOzdchalbF1Eu2prieWoZxz0apG8OuUeAhaB+t8kT6swAkwvkLW
|
||||
OnlSATm9Cls9Pc4XDHTbZlbMmwF2Jmukgz/l1vlTutt4ZgZwQkSEa9Qfoi9Zym0R
|
||||
iKls1CgD49zguR/cFDKK3agvfv6Afw6HdgaS/WqcI/Ros7b+RCkbAlAG5gqr6BLQ
|
||||
8RGyVjZSC4Mz/ddcnMEpRAnjuFJjhGA=
|
||||
-----END CERTIFICATE-----
|
28
integration_tests/certs/localhost.key
Normal file
28
integration_tests/certs/localhost.key
Normal file
@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDUMSURc9j5UNA/
|
||||
txKKQrSd7QqAOCisZg8b1HuU5648sfIVhivLvz6EHTfs5PQE7IDa5O7XKuiDR0j/
|
||||
hgaJw8MipeEiAd+NX7iQyLlKA7sQJY4GZkWpMLdsHC9s22hbjRoWNhIVo6Sb+ed+
|
||||
n6dqUQkln82RKQPiB1PekfUKWyi+UjgGWzF4igm3IbPEyJ0lVcMCow2totq3MkHC
|
||||
dPc4Sz3fFN/sp3UpSlCn05kWsaLXnO/+fq0KatwLfEw5Ue5iSFbRQJMWZ6/tmpuC
|
||||
FCxD032zQTqST2tR/3G0w0fgB/r7ZhGRPUMAZDoWReXWdB0HkfldGA4GX7eT2zzB
|
||||
SvFC9B11AgMBAAECggEAYaj4yY6LFzxVjG2i79WBsYnOonK2bZpPa9ygwEjdTXwM
|
||||
0lE9SPoNONsFyVca5EVBjP1+27MY7orZkxlJWxCpeAHmmzNHg5bBqIlpliIfb3AJ
|
||||
bPKXLyaH1Q8n2K8m2bQYhI6ARktZ0Jv1KrcqY2lGj3V8NEovSlFbDX4ZzJlmKCly
|
||||
d4Ia6eQ7f9AjgsOwpQGeCTF7WLaVDnch6D4JfCGrW08lFeaqogiBQczsOE3hcNSd
|
||||
tEul21Z0CkC7Iiw28KdkApPINquo1VYdAcOvUCOXkwJfPC1gsJwK4O2jxfi9v5NF
|
||||
uU1niK0/00b396pQKvXpkfViynexwzK0MZCoo3zuQQKBgQDzaZexcniQNDyWqN3C
|
||||
oMe4V3rnxs+aO/lu8Ed3mng+Jf4vuarZlxNot7WRBMGT/T+b7/UIrqRJy50CYAPY
|
||||
3RRR84tLg3UMwUWhDYsPucNc2icODBG4c+QWJ300W19r+J+iT8PwS9AbH2n094Rn
|
||||
LCRYFrX5aMsgIH5uwuncKzweMQKBgQDfKj2i1ptC53aOcr1tMCFYcnMGtaAZ8u6+
|
||||
cKSgnzKlTw/g0EYlGcETUnCyZe0oVYWp3y859FBXU0JMDmxu84aYEZNF6BwRVlpF
|
||||
feQgtUFZHyf9MepQGhjIJ5El8n7jhh1bsBY18QbDFe6/GtqPx/mQEF7vE+wPFl9h
|
||||
putwdv3OhQKBgGKPyi2/BVSW4kW7IPiTM+vP+GNrnFp+mHS0dKvYb4HyzmcyzhyH
|
||||
UQOhB7Mt8thivmP9GQIn/TwoZ24zxLsGYhkA/dFY7Id6pyAcpMd8V7/8Ub4dYvuG
|
||||
acASw1709MF6jeEiXVuqxxyEbtoTc5h3Rkwo/gx8w2tB3RAqepl9JD2xAoGAfVL3
|
||||
ci8a2iOqTKza/Cp/T3BWcHonAuuOb5xKl3lPs84GmLXd7o/cAcHWUBk1aeU9Pvx7
|
||||
RQyS4bd8D8I52sUf3N5h2mxS9tmLsGLWbhfcLvR0PJh/gaRmLmEp/imEYLm8WvU0
|
||||
Q+6rYXs7rE6kVwJygBjxd0m003Q49FoM9gec2RECgYEA5SLAe2UmJSLIb0DKk27o
|
||||
nSfARDSdi9N40vIjDFHmDRdKTOYicED/f7KqXnxVpvFxDdCvJ7xeC4V7vkaqiiwd
|
||||
/oMLQq0GjmBxG/PNd1AFIWDydyH+JcY6U4XWIzIw92OKVYC/KMvd2f9orTfmDyAU
|
||||
RsGMfgV90kCzouAZKy3yPmo=
|
||||
-----END PRIVATE KEY-----
|
56
integration_tests/prosody.cfg.lua
Normal file
56
integration_tests/prosody.cfg.lua
Normal file
@ -0,0 +1,56 @@
|
||||
admins = { }
|
||||
plugin_paths = {}
|
||||
|
||||
modules_enabled = {
|
||||
-- Generally required
|
||||
"disco"; -- Service discovery
|
||||
"roster"; -- Allow users to have a roster. Recommended ;)
|
||||
"saslauth"; -- Authentication for clients and servers. Recommended if you want to log in.
|
||||
"tls"; -- Add support for secure TLS on c2s/s2s connections
|
||||
|
||||
-- Not essential, but recommended
|
||||
"blocklist"; -- Allow users to block communications with other users
|
||||
"bookmarks"; -- Synchronise the list of open rooms between clients
|
||||
"carbons"; -- Keep multiple online clients in sync
|
||||
"dialback"; -- Support for verifying remote servers using DNS
|
||||
"limits"; -- Enable bandwidth limiting for XMPP connections
|
||||
"pep"; -- Allow users to store public and private data in their account
|
||||
"private"; -- Legacy account storage mechanism (XEP-0049)
|
||||
"smacks"; -- Stream management and resumption (XEP-0198)
|
||||
"vcard4"; -- User profiles (stored in PEP)
|
||||
"vcard_legacy"; -- Conversion between legacy vCard and PEP Avatar, vcard
|
||||
|
||||
-- Nice to have
|
||||
"csi_simple"; -- Simple but effective traffic optimizations for mobile devices
|
||||
"invites"; -- Create and manage invites
|
||||
"invites_adhoc"; -- Allow admins/users to create invitations via their client
|
||||
"invites_register"; -- Allows invited users to create accounts
|
||||
"ping"; -- Replies to XMPP pings with pongs
|
||||
"register"; -- Allow users to register on this server using a client and change passwords
|
||||
"time"; -- Let others know the time here on this server
|
||||
"uptime"; -- Report how long server has been running
|
||||
"version"; -- Replies to server version requests
|
||||
|
||||
-- SASL2
|
||||
"sasl2";
|
||||
"sasl2_sm";
|
||||
"sasl2_fast";
|
||||
"sasl2_bind2";
|
||||
}
|
||||
|
||||
s2s_secure_auth = false
|
||||
|
||||
-- Authentication
|
||||
authentication = "internal_plain"
|
||||
|
||||
-- Storage
|
||||
storage = "internal"
|
||||
data_path = "/tmp/prosody-data/"
|
||||
log = {
|
||||
debug = "*console";
|
||||
}
|
||||
|
||||
pidfile = "/tmp/prosody.pid"
|
||||
|
||||
VirtualHost "localhost"
|
||||
|
16
integration_tests/pubspec.yaml
Normal file
16
integration_tests/pubspec.yaml
Normal file
@ -0,0 +1,16 @@
|
||||
name: integration_tests
|
||||
description: A sample command-line application.
|
||||
version: 1.0.0
|
||||
|
||||
environment:
|
||||
sdk: '>=2.18.0 <3.0.0'
|
||||
|
||||
dependencies:
|
||||
logging: ^1.0.2
|
||||
moxxmpp: 0.2.0
|
||||
moxxmpp_socket_tcp: 0.2.1
|
||||
|
||||
dev_dependencies:
|
||||
lints: ^2.0.0
|
||||
test: ^1.16.0
|
||||
very_good_analysis: ^3.0.1
|
67
integration_tests/test/sasl2_test.dart
Normal file
67
integration_tests/test/sasl2_test.dart
Normal file
@ -0,0 +1,67 @@
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:moxxmpp/moxxmpp.dart';
|
||||
import 'package:moxxmpp_socket_tcp/moxxmpp_socket_tcp.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class TestingTCPSocketWrapper extends TCPSocketWrapper {
|
||||
@override
|
||||
bool onBadCertificate(dynamic certificate, String domain) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
Logger.root.level = Level.ALL;
|
||||
Logger.root.onRecord.listen((record) {
|
||||
// ignore: avoid_print
|
||||
print(
|
||||
'[${record.level.name}] (${record.loggerName}) ${record.time}: ${record.message}',
|
||||
);
|
||||
});
|
||||
|
||||
test('Test authenticating against Prosody with SASL2, Bind2, and FAST', () async {
|
||||
final conn = XmppConnection(
|
||||
TestingReconnectionPolicy(),
|
||||
AlwaysConnectedConnectivityManager(),
|
||||
TestingTCPSocketWrapper(),
|
||||
)..setConnectionSettings(
|
||||
ConnectionSettings(
|
||||
jid: JID.fromString('testuser@localhost'),
|
||||
password: 'abc123',
|
||||
useDirectTLS: false,
|
||||
|
||||
host: '127.0.0.1',
|
||||
port: 5222,
|
||||
),
|
||||
);
|
||||
final csi = CSIManager();
|
||||
await csi.setInactive(sendNonza: false);
|
||||
await conn.registerManagers([
|
||||
RosterManager(TestingRosterStateManager('', [])),
|
||||
DiscoManager([]),
|
||||
]);
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
ResourceBindingNegotiator(),
|
||||
FASTSaslNegotiator(),
|
||||
Bind2Negotiator(),
|
||||
StartTlsNegotiator(),
|
||||
Sasl2Negotiator(
|
||||
userAgent: const UserAgent(
|
||||
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
|
||||
software: 'moxxmpp',
|
||||
device: "PapaTutuWawa's awesome device",
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
final result = await conn.connect(
|
||||
waitUntilLogin: true,
|
||||
shouldReconnect: false,
|
||||
enableReconnectOnSuccess: false,
|
||||
);
|
||||
expect(result.isType<bool>(), true);
|
||||
expect(conn.getNegotiatorById<Sasl2Negotiator>(sasl2Negotiator)!.state, NegotiatorState.done);
|
||||
expect(conn.getNegotiatorById<FASTSaslNegotiator>(saslFASTNegotiator)!.fastToken != null, true,);
|
||||
});
|
||||
}
|
@ -3,6 +3,9 @@
|
||||
- **BREAKING**: Removed `connectAwaitable` and merged it with `connect`.
|
||||
- **BREAKING**: Removed `allowPlainAuth` from `ConnectionSettings`. If you don't want to use SASL PLAIN, don't register the negotiator. If you want to only conditionally use SASL PLAIN, extend the `SaslPlainNegotiator` and override its `matchesFeature` method to only call the super method when SASL PLAIN should be used.
|
||||
- **BREAKING**: The user avatar's `subscribe` and `unsubscribe` no longer subscribe to the `:data` PubSub nodes
|
||||
- Renamed `ResourceBindingSuccessEvent` to `ResourceBoundEvent`
|
||||
- **BREAKING**: Removed `isFeatureSupported` from the manager attributes. The managers now all have a method `isFeatureSupported` that works the same
|
||||
- The `PresenceManager` is now optional
|
||||
|
||||
## 0.1.6+1
|
||||
|
||||
|
@ -18,17 +18,17 @@ export 'package:moxxmpp/src/namespaces.dart';
|
||||
export 'package:moxxmpp/src/negotiators/manager.dart';
|
||||
export 'package:moxxmpp/src/negotiators/namespaces.dart';
|
||||
export 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||
export 'package:moxxmpp/src/negotiators/resource_binding.dart';
|
||||
export 'package:moxxmpp/src/negotiators/sasl/errors.dart';
|
||||
export 'package:moxxmpp/src/negotiators/sasl/negotiator.dart';
|
||||
export 'package:moxxmpp/src/negotiators/sasl/plain.dart';
|
||||
export 'package:moxxmpp/src/negotiators/sasl/scram.dart';
|
||||
export 'package:moxxmpp/src/negotiators/starttls.dart';
|
||||
export 'package:moxxmpp/src/ping.dart';
|
||||
export 'package:moxxmpp/src/presence.dart';
|
||||
export 'package:moxxmpp/src/reconnect.dart';
|
||||
export 'package:moxxmpp/src/rfcs/rfc_2782.dart';
|
||||
export 'package:moxxmpp/src/rfcs/rfc_4790.dart';
|
||||
export 'package:moxxmpp/src/rfcs/rfc_6120/resource_binding.dart';
|
||||
export 'package:moxxmpp/src/rfcs/rfc_6120/sasl/errors.dart';
|
||||
export 'package:moxxmpp/src/rfcs/rfc_6120/sasl/negotiator.dart';
|
||||
export 'package:moxxmpp/src/rfcs/rfc_6120/sasl/plain.dart';
|
||||
export 'package:moxxmpp/src/rfcs/rfc_6120/sasl/scram.dart';
|
||||
export 'package:moxxmpp/src/rfcs/rfc_6120/starttls.dart';
|
||||
export 'package:moxxmpp/src/roster/errors.dart';
|
||||
export 'package:moxxmpp/src/roster/roster.dart';
|
||||
export 'package:moxxmpp/src/roster/state.dart';
|
||||
@ -38,6 +38,7 @@ export 'package:moxxmpp/src/stanza.dart';
|
||||
export 'package:moxxmpp/src/stringxml.dart';
|
||||
export 'package:moxxmpp/src/types/result.dart';
|
||||
export 'package:moxxmpp/src/xeps/staging/extensible_file_thumbnails.dart';
|
||||
export 'package:moxxmpp/src/xeps/staging/fast.dart';
|
||||
export 'package:moxxmpp/src/xeps/staging/file_upload_notification.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0004.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
|
||||
@ -76,6 +77,11 @@ export 'package:moxxmpp/src/xeps/xep_0384/helpers.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0384/types.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0384/xep_0384.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0385.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0386.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0388/errors.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0388/user_agent.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0414.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0424.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0444.dart';
|
||||
|
@ -9,6 +9,7 @@ import 'package:moxxmpp/src/connectivity.dart';
|
||||
import 'package:moxxmpp/src/errors.dart';
|
||||
import 'package:moxxmpp/src/events.dart';
|
||||
import 'package:moxxmpp/src/iq.dart';
|
||||
import 'package:moxxmpp/src/jid.dart';
|
||||
import 'package:moxxmpp/src/managers/attributes.dart';
|
||||
import 'package:moxxmpp/src/managers/base.dart';
|
||||
import 'package:moxxmpp/src/managers/data.dart';
|
||||
@ -63,14 +64,15 @@ enum StanzaFromType {
|
||||
|
||||
/// Nonza describing the XMPP stream header.
|
||||
class StreamHeaderNonza extends XMLNode {
|
||||
StreamHeaderNonza(String serverDomain)
|
||||
StreamHeaderNonza(JID jid)
|
||||
: super(
|
||||
tag: 'stream:stream',
|
||||
attributes: <String, String>{
|
||||
'xmlns': stanzaXmlns,
|
||||
'version': '1.0',
|
||||
'xmlns:stream': streamXmlns,
|
||||
'to': serverDomain,
|
||||
'to': jid.domain,
|
||||
'from': jid.toBare().toString(),
|
||||
'xml:lang': 'en',
|
||||
},
|
||||
closeTag: false,
|
||||
@ -133,9 +135,6 @@ class XmppConnection {
|
||||
StreamController.broadcast();
|
||||
final Map<String, XmppManagerBase> _xmppManagers = {};
|
||||
|
||||
/// Disco info we got after binding a resource (xmlns)
|
||||
final List<String> _serverFeatures = List.empty(growable: true);
|
||||
|
||||
/// The buffer object to keep split up stanzas together
|
||||
final XmlStreamBuffer _streamBuffer = XmlStreamBuffer();
|
||||
|
||||
@ -150,11 +149,12 @@ class XmppConnection {
|
||||
|
||||
/// The currently bound resource or '' if none has been bound yet.
|
||||
String _resource = '';
|
||||
String get resource => _resource;
|
||||
|
||||
/// True if we are authenticated. False if not.
|
||||
bool _isAuthenticated = false;
|
||||
|
||||
/// Timer for the connecting timeout
|
||||
/// Timer for the connecting timeout.
|
||||
Timer? _connectingTimeoutTimer;
|
||||
|
||||
/// Completers for certain actions
|
||||
@ -201,8 +201,6 @@ class XmppConnection {
|
||||
|
||||
ReconnectionPolicy get reconnectionPolicy => _reconnectionPolicy;
|
||||
|
||||
List<String> get serverFeatures => _serverFeatures;
|
||||
|
||||
bool get isAuthenticated => _isAuthenticated;
|
||||
|
||||
/// Return the registered feature negotiator that has id [id]. Returns null if
|
||||
@ -221,7 +219,6 @@ class XmppConnection {
|
||||
sendEvent: _sendEvent,
|
||||
getConnectionSettings: () => _connectionSettings,
|
||||
getManagerById: getManagerById,
|
||||
isFeatureSupported: _serverFeatures.contains,
|
||||
getFullJID: () => _connectionSettings.jid.withResource(_resource),
|
||||
getSocket: () => _socket,
|
||||
getConnection: () => this,
|
||||
@ -253,13 +250,29 @@ class XmppConnection {
|
||||
}
|
||||
}
|
||||
|
||||
// Mark the current connection as authenticated.
|
||||
void _setAuthenticated() {
|
||||
_sendEvent(AuthenticationSuccessEvent());
|
||||
_isAuthenticated = true;
|
||||
}
|
||||
|
||||
/// Remove [feature] from the stream features we are currently negotiating.
|
||||
void _removeNegotiatingFeature(String feature) {
|
||||
_streamFeatures.removeWhere((node) {
|
||||
return node.attributes['xmlns'] == feature;
|
||||
});
|
||||
}
|
||||
|
||||
/// Register a list of negotiator with the connection.
|
||||
void registerFeatureNegotiators(List<XmppFeatureNegotiatorBase> negotiators) {
|
||||
Future<void> registerFeatureNegotiators(
|
||||
List<XmppFeatureNegotiatorBase> negotiators,
|
||||
) async {
|
||||
for (final negotiator in negotiators) {
|
||||
_log.finest('Registering ${negotiator.id}');
|
||||
negotiator.register(
|
||||
NegotiatorAttributes(
|
||||
sendRawXML,
|
||||
() => this,
|
||||
() => _connectionSettings,
|
||||
_sendEvent,
|
||||
getNegotiatorById,
|
||||
@ -267,12 +280,19 @@ class XmppConnection {
|
||||
() => _connectionSettings.jid.withResource(_resource),
|
||||
() => _socket,
|
||||
() => _isAuthenticated,
|
||||
_setAuthenticated,
|
||||
setResource,
|
||||
_removeNegotiatingFeature,
|
||||
),
|
||||
);
|
||||
_featureNegotiators[negotiator.id] = negotiator;
|
||||
}
|
||||
|
||||
_log.finest('Negotiators registered');
|
||||
|
||||
for (final negotiator in _featureNegotiators.values) {
|
||||
await negotiator.postRegisterCallback();
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset all registered negotiators.
|
||||
@ -296,13 +316,8 @@ class XmppConnection {
|
||||
|
||||
/// A [PresenceManager] is required, so have a wrapper for getting it.
|
||||
/// Returns the registered [PresenceManager].
|
||||
PresenceManager getPresenceManager() {
|
||||
assert(
|
||||
_xmppManagers.containsKey(presenceManager),
|
||||
'A PresenceManager is mandatory',
|
||||
);
|
||||
|
||||
return getManagerById(presenceManager)!;
|
||||
PresenceManager? getPresenceManager() {
|
||||
return getManagerById(presenceManager);
|
||||
}
|
||||
|
||||
/// A [DiscoManager] is required so, have a wrapper for getting it.
|
||||
@ -399,7 +414,7 @@ class XmppConnection {
|
||||
|
||||
// Close the socket
|
||||
_socket.close();
|
||||
|
||||
|
||||
if (!error.isRecoverable()) {
|
||||
// We cannot recover this error
|
||||
_log.severe(
|
||||
@ -454,10 +469,10 @@ class XmppConnection {
|
||||
}
|
||||
|
||||
/// Sends an [XMLNode] without any further processing to the server.
|
||||
void sendRawXML(XMLNode node, {String? redact}) {
|
||||
void sendRawXML(XMLNode node) {
|
||||
final string = node.toXml();
|
||||
_log.finest('==> $string');
|
||||
_socket.write(string, redact: redact);
|
||||
_socket.write(string);
|
||||
}
|
||||
|
||||
/// Sends [raw] to the server.
|
||||
@ -657,9 +672,14 @@ class XmppConnection {
|
||||
}
|
||||
|
||||
/// Sets the resource of the connection
|
||||
void setResource(String resource) {
|
||||
@visibleForTesting
|
||||
void setResource(String resource, {bool triggerEvent = true}) {
|
||||
_log.finest('Updating _resource to $resource');
|
||||
_resource = resource;
|
||||
|
||||
if (triggerEvent) {
|
||||
_sendEvent(ResourceBoundEvent(resource));
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the connection's events as a stream.
|
||||
@ -839,9 +859,6 @@ class XmppConnection {
|
||||
// Tell consumers of the event stream that we're done with stream feature
|
||||
// negotiations
|
||||
await _sendEvent(StreamNegotiationsDoneEvent());
|
||||
|
||||
// Send out initial presence
|
||||
await getPresenceManager().sendInitialPresence();
|
||||
}
|
||||
|
||||
Future<void> _executeCurrentNegotiator(XMLNode nonza) async {
|
||||
@ -895,10 +912,7 @@ class XmppConnection {
|
||||
_streamFeatures.clear();
|
||||
_sendStreamHeader();
|
||||
} else {
|
||||
_streamFeatures.removeWhere((node) {
|
||||
return node.attributes['xmlns'] ==
|
||||
_currentNegotiator!.negotiatingXmlns;
|
||||
});
|
||||
_removeNegotiatingFeature(_currentNegotiator!.negotiatingXmlns);
|
||||
_currentNegotiator = null;
|
||||
|
||||
if (_isMandatoryNegotiationDone(_streamFeatures) &&
|
||||
@ -1010,25 +1024,12 @@ class XmppConnection {
|
||||
Future<void> _sendEvent(XmppEvent event) async {
|
||||
_log.finest('Event: ${event.toString()}');
|
||||
|
||||
// Specific event handling
|
||||
if (event is ResourceBindingSuccessEvent) {
|
||||
_log.finest(
|
||||
'Received ResourceBindingSuccessEvent. Setting _resource to ${event.resource}',
|
||||
);
|
||||
setResource(event.resource);
|
||||
|
||||
_log.finest('Resetting _serverFeatures');
|
||||
_serverFeatures.clear();
|
||||
} else if (event is AuthenticationSuccessEvent) {
|
||||
_log.finest(
|
||||
'Received AuthenticationSuccessEvent. Setting _isAuthenticated to true',
|
||||
);
|
||||
_isAuthenticated = true;
|
||||
}
|
||||
|
||||
for (final manager in _xmppManagers.values) {
|
||||
await manager.onXmppEvent(event);
|
||||
}
|
||||
for (final negotiator in _featureNegotiators.values) {
|
||||
await negotiator.onXmppEvent(event);
|
||||
}
|
||||
|
||||
_eventStreamController.add(event);
|
||||
}
|
||||
@ -1038,11 +1039,11 @@ class XmppConnection {
|
||||
_socket.write(
|
||||
XMLNode(
|
||||
tag: 'xml',
|
||||
attributes: <String, String>{'version': '1.0'},
|
||||
attributes: {'version': '1.0'},
|
||||
closeTag: false,
|
||||
isDeclaration: true,
|
||||
children: [
|
||||
StreamHeaderNonza(_connectionSettings.jid.domain),
|
||||
StreamHeaderNonza(_connectionSettings.jid),
|
||||
],
|
||||
).toXml(),
|
||||
);
|
||||
@ -1067,7 +1068,7 @@ class XmppConnection {
|
||||
await _reconnectionPolicy.setShouldReconnect(false);
|
||||
|
||||
if (triggeredByUser) {
|
||||
getPresenceManager().sendUnavailablePresence();
|
||||
getPresenceManager()?.sendUnavailablePresence();
|
||||
}
|
||||
|
||||
_socket.prepareDisconnect();
|
||||
@ -1087,10 +1088,6 @@ class XmppConnection {
|
||||
|
||||
/// Make sure that all required managers are registered
|
||||
void _runPreConnectionAssertions() {
|
||||
assert(
|
||||
_xmppManagers.containsKey(presenceManager),
|
||||
'A PresenceManager is mandatory',
|
||||
);
|
||||
assert(
|
||||
_xmppManagers.containsKey(rosterManager),
|
||||
'A RosterManager is mandatory',
|
||||
@ -1138,7 +1135,9 @@ class XmppConnection {
|
||||
}
|
||||
|
||||
if (lastResource != null) {
|
||||
setResource(lastResource);
|
||||
setResource(lastResource, triggerEvent: false);
|
||||
} else {
|
||||
setResource('', triggerEvent: false);
|
||||
}
|
||||
|
||||
_enableReconnectOnSuccess = enableReconnectOnSuccess;
|
||||
@ -1159,13 +1158,16 @@ class XmppConnection {
|
||||
}
|
||||
|
||||
final smManager = getStreamManagementManager();
|
||||
String? host;
|
||||
int? port;
|
||||
var host = _connectionSettings.host;
|
||||
var port = _connectionSettings.port;
|
||||
if (smManager?.state.streamResumptionLocation != null) {
|
||||
// TODO(Unknown): Maybe wrap this in a try catch?
|
||||
final parsed = Uri.parse(smManager!.state.streamResumptionLocation!);
|
||||
host = parsed.host;
|
||||
port = parsed.port;
|
||||
} else {
|
||||
host = _connectionSettings.host;
|
||||
port = _connectionSettings.port;
|
||||
}
|
||||
|
||||
final result = await _socket.connect(
|
||||
|
@ -160,8 +160,10 @@ class StreamManagementEnabledEvent extends XmppEvent {
|
||||
}
|
||||
|
||||
/// Triggered when we bound a resource
|
||||
class ResourceBindingSuccessEvent extends XmppEvent {
|
||||
ResourceBindingSuccessEvent({required this.resource});
|
||||
class ResourceBoundEvent extends XmppEvent {
|
||||
ResourceBoundEvent(this.resource);
|
||||
|
||||
/// The resource that was just bound.
|
||||
final String resource;
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,6 @@ class XmppManagerAttributes {
|
||||
required this.getManagerById,
|
||||
required this.sendEvent,
|
||||
required this.getConnectionSettings,
|
||||
required this.isFeatureSupported,
|
||||
required this.getFullJID,
|
||||
required this.getSocket,
|
||||
required this.getConnection,
|
||||
@ -45,9 +44,6 @@ class XmppManagerAttributes {
|
||||
/// (Maybe) Get a Manager attached to the connection by its Id.
|
||||
final T? Function<T extends XmppManagerBase>(String) getManagerById;
|
||||
|
||||
/// Returns true if a server feature is supported
|
||||
final bool Function(String) isFeatureSupported;
|
||||
|
||||
/// Returns the full JID of the current account
|
||||
final JID Function() getFullJID;
|
||||
|
||||
|
@ -6,6 +6,7 @@ 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/stringxml.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
|
||||
@ -31,6 +32,29 @@ abstract class XmppManagerBase {
|
||||
return _managerAttributes;
|
||||
}
|
||||
|
||||
/// Resolves to true when the server supports the disco feature [xmlns]. Resolves
|
||||
/// to false when either the disco request fails or the server does not
|
||||
/// support [xmlns].
|
||||
/// Note that this function requires a registered DiscoManager.
|
||||
@protected
|
||||
Future<bool> isFeatureSupported(String xmlns) async {
|
||||
final dm = _managerAttributes.getManagerById<DiscoManager>(discoManager);
|
||||
assert(
|
||||
dm != null,
|
||||
'The DiscoManager must be registered for isFeatureSupported to work',
|
||||
);
|
||||
|
||||
final result = await dm!.discoInfoQuery(
|
||||
_managerAttributes.getConnectionSettings().jid.domain,
|
||||
shouldEncrypt: false,
|
||||
);
|
||||
if (result.isType<DiscoError>()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return result.get<DiscoInfo>().features.contains(xmlns);
|
||||
}
|
||||
|
||||
/// Return the StanzaHandlers associated with this manager that deal with stanzas we
|
||||
/// send. These are run before the stanza is sent. The higher the value of the
|
||||
/// handler's priority, the earlier it is run.
|
||||
|
@ -116,6 +116,12 @@ const omemoBundlesXmlns = 'urn:xmpp:omemo:2:bundles';
|
||||
// XEP-0385
|
||||
const simsXmlns = 'urn:xmpp:sims:1';
|
||||
|
||||
// XEP-0386
|
||||
const bind2Xmlns = 'urn:xmpp:bind:0';
|
||||
|
||||
// XEP-0388
|
||||
const sasl2Xmlns = 'urn:xmpp:sasl:2';
|
||||
|
||||
// XEP-0420
|
||||
const sceXmlns = 'urn:xmpp:sce:1';
|
||||
|
||||
@ -154,3 +160,6 @@ const fallbackXmlns = 'urn:xmpp:feature-fallback:0';
|
||||
|
||||
// ???
|
||||
const urlDataXmlns = 'http://jabber.org/protocol/url-data';
|
||||
|
||||
// XEP-XXXX
|
||||
const fastXmlns = 'urn:xmpp:fast:0';
|
||||
|
@ -7,3 +7,7 @@ const rosterNegotiator = 'im.moxxmpp.core.roster';
|
||||
const resourceBindingNegotiator = 'im.moxxmpp.core.resource';
|
||||
const streamManagementNegotiator = 'im.moxxmpp.xeps.sm';
|
||||
const startTlsNegotiator = 'im.moxxmpp.core.starttls';
|
||||
const sasl2Negotiator = 'org.moxxmpp.sasl.sasl2';
|
||||
const bind2Negotiator = 'org.moxxmpp.bind2';
|
||||
const saslFASTNegotiator = 'org.moxxmpp.sasl.fast';
|
||||
const carbonsNegotiator = 'org.moxxmpp.bind2.carbons';
|
||||
|
@ -1,4 +1,6 @@
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:moxlib/moxlib.dart';
|
||||
import 'package:moxxmpp/src/connection.dart';
|
||||
import 'package:moxxmpp/src/errors.dart';
|
||||
import 'package:moxxmpp/src/events.dart';
|
||||
import 'package:moxxmpp/src/jid.dart';
|
||||
@ -27,6 +29,7 @@ abstract class NegotiatorError extends XmppError {}
|
||||
class NegotiatorAttributes {
|
||||
const NegotiatorAttributes(
|
||||
this.sendNonza,
|
||||
this.getConnection,
|
||||
this.getConnectionSettings,
|
||||
this.sendEvent,
|
||||
this.getNegotiatorById,
|
||||
@ -34,15 +37,21 @@ class NegotiatorAttributes {
|
||||
this.getFullJID,
|
||||
this.getSocket,
|
||||
this.isAuthenticated,
|
||||
this.setAuthenticated,
|
||||
this.setResource,
|
||||
this.removeNegotiatingFeature,
|
||||
);
|
||||
|
||||
/// Sends the nonza nonza and optionally redacts it in logs if redact is not null.
|
||||
final void Function(XMLNode nonza, {String? redact}) sendNonza;
|
||||
final void Function(XMLNode nonza) sendNonza;
|
||||
|
||||
/// Returns the connection settings.
|
||||
final ConnectionSettings Function() getConnectionSettings;
|
||||
|
||||
/// Send an event event to the connection's event bus
|
||||
/// Returns the connection object.
|
||||
final XmppConnection Function() getConnection;
|
||||
|
||||
/// Send an event event to the connection's event bus.
|
||||
final Future<void> Function(XmppEvent event) sendEvent;
|
||||
|
||||
/// Returns the negotiator with id id of the connection or null.
|
||||
@ -60,6 +69,17 @@ class NegotiatorAttributes {
|
||||
|
||||
/// Returns true if the stream is authenticated. Returns false if not.
|
||||
final bool Function() isAuthenticated;
|
||||
|
||||
/// Sets the resource of the connection. If triggerEvent is true, then a
|
||||
/// [ResourceBoundEvent] is triggered.
|
||||
final void Function(String, {bool triggerEvent}) setResource;
|
||||
|
||||
/// Sets the authentication state of the connection to true.
|
||||
final void Function() setAuthenticated;
|
||||
|
||||
/// Remove a stream feature from our internal cache. This is useful for when you
|
||||
/// negotiated a feature for another negotiator, like SASL2.
|
||||
final void Function(String) removeNegotiatingFeature;
|
||||
}
|
||||
|
||||
abstract class XmppFeatureNegotiatorBase {
|
||||
@ -104,6 +124,9 @@ abstract class XmppFeatureNegotiatorBase {
|
||||
null;
|
||||
}
|
||||
|
||||
/// Called when an event is triggered in the [XmppConnection].
|
||||
Future<void> onXmppEvent(XmppEvent event) async {}
|
||||
|
||||
/// Called with the currently received nonza [nonza] when the negotiator is active.
|
||||
/// If the negotiator is just elected to be the next one, then [nonza] is equal to
|
||||
/// the <stream:features /> nonza.
|
||||
@ -120,5 +143,10 @@ abstract class XmppFeatureNegotiatorBase {
|
||||
state = NegotiatorState.ready;
|
||||
}
|
||||
|
||||
@protected
|
||||
NegotiatorAttributes get attributes => _attributes;
|
||||
|
||||
/// Run after all negotiators are registered. Useful for registering callbacks against
|
||||
/// other negotiators. By default this function does nothing.
|
||||
Future<void> postRegisterCallback() async {}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'package:moxxmpp/src/connection.dart';
|
||||
import 'package:moxxmpp/src/events.dart';
|
||||
import 'package:moxxmpp/src/jid.dart';
|
||||
@ -6,8 +7,10 @@ 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/stanza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0198/negotiator.dart';
|
||||
|
||||
/// A function that will be called when presence, outside of subscription request
|
||||
/// management, will be sent. Useful for managers that want to add [XMLNode]s to said
|
||||
@ -42,6 +45,20 @@ class PresenceManager extends XmppManagerBase {
|
||||
_presenceCallbacks.add(callback);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onXmppEvent(XmppEvent event) async {
|
||||
if (event is StreamNegotiationsDoneEvent) {
|
||||
// Send initial presence only when we have not resumed the stream
|
||||
final sm = getAttributes().getNegotiatorById<StreamManagementNegotiator>(
|
||||
streamManagementNegotiator,
|
||||
);
|
||||
final isResumed = sm?.isResumed ?? false;
|
||||
if (!isResumed) {
|
||||
unawaited(sendInitialPresence());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<StanzaHandlerData> _onPresence(
|
||||
Stanza presence,
|
||||
StanzaHandlerData state,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:moxxmpp/src/events.dart';
|
||||
import 'package:moxxmpp/src/jid.dart';
|
||||
import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/negotiators/namespaces.dart';
|
||||
@ -30,10 +30,13 @@ class ResourceBindingNegotiator extends XmppFeatureNegotiatorBase {
|
||||
if (sm != null) {
|
||||
return super.matchesFeature(features) &&
|
||||
!sm.streamResumed &&
|
||||
attributes.isAuthenticated();
|
||||
attributes.isAuthenticated() &&
|
||||
attributes.getConnection().resource.isEmpty;
|
||||
}
|
||||
|
||||
return super.matchesFeature(features) && attributes.isAuthenticated();
|
||||
return super.matchesFeature(features) &&
|
||||
attributes.isAuthenticated() &&
|
||||
attributes.getConnection().resource.isEmpty;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -65,11 +68,9 @@ class ResourceBindingNegotiator extends XmppFeatureNegotiatorBase {
|
||||
}
|
||||
|
||||
final bind = nonza.firstTag('bind')!;
|
||||
final jid = bind.firstTag('jid')!;
|
||||
final resource = jid.innerText().split('/')[1];
|
||||
|
||||
await attributes
|
||||
.sendEvent(ResourceBindingSuccessEvent(resource: resource));
|
||||
final rawJid = bind.firstTag('jid')!.innerText();
|
||||
final resource = JID.fromString(rawJid).resource;
|
||||
attributes.setResource(resource);
|
||||
return const Result(NegotiatorState.done);
|
||||
}
|
||||
}
|
@ -3,21 +3,23 @@ import 'package:logging/logging.dart';
|
||||
import 'package:moxxmpp/src/events.dart';
|
||||
import 'package:moxxmpp/src/negotiators/namespaces.dart';
|
||||
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||
import 'package:moxxmpp/src/negotiators/sasl/errors.dart';
|
||||
import 'package:moxxmpp/src/negotiators/sasl/negotiator.dart';
|
||||
import 'package:moxxmpp/src/negotiators/sasl/nonza.dart';
|
||||
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/errors.dart';
|
||||
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/nonza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/types/result.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
|
||||
import 'package:saslprep/saslprep.dart';
|
||||
|
||||
class SaslPlainAuthNonza extends SaslAuthNonza {
|
||||
SaslPlainAuthNonza(String username, String password)
|
||||
SaslPlainAuthNonza(String data)
|
||||
: super(
|
||||
'PLAIN',
|
||||
base64.encode(utf8.encode('\u0000$username\u0000$password')),
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
class SaslPlainNegotiator extends SaslNegotiator {
|
||||
class SaslPlainNegotiator extends Sasl2AuthenticationNegotiator {
|
||||
SaslPlainNegotiator()
|
||||
: _authSent = false,
|
||||
_log = Logger('SaslPlainNegotiator'),
|
||||
@ -47,17 +49,16 @@ class SaslPlainNegotiator extends SaslNegotiator {
|
||||
XMLNode nonza,
|
||||
) async {
|
||||
if (!_authSent) {
|
||||
final settings = attributes.getConnectionSettings();
|
||||
final data = await getRawStep('');
|
||||
attributes.sendNonza(
|
||||
SaslPlainAuthNonza(settings.jid.local, settings.password),
|
||||
redact: SaslPlainAuthNonza('******', '******').toXml(),
|
||||
SaslPlainAuthNonza(data),
|
||||
);
|
||||
_authSent = true;
|
||||
return const Result(NegotiatorState.ready);
|
||||
} else {
|
||||
final tag = nonza.tag;
|
||||
if (tag == 'success') {
|
||||
await attributes.sendEvent(AuthenticationSuccessEvent());
|
||||
attributes.setAuthenticated();
|
||||
return const Result(NegotiatorState.done);
|
||||
} else {
|
||||
// We assume it's a <failure/>
|
||||
@ -76,4 +77,34 @@ class SaslPlainNegotiator extends SaslNegotiator {
|
||||
|
||||
super.reset();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> postRegisterCallback() async {
|
||||
attributes
|
||||
.getNegotiatorById<Sasl2Negotiator>(sasl2Negotiator)
|
||||
?.registerSaslNegotiator(this);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> getRawStep(String input) async {
|
||||
final settings = attributes.getConnectionSettings();
|
||||
final prep = Saslprep.saslprep(settings.password);
|
||||
return base64.encode(
|
||||
utf8.encode('\u0000${settings.jid.local}\u0000$prep'),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<bool, NegotiatorError>> onSasl2Success(XMLNode response) async {
|
||||
state = NegotiatorState.done;
|
||||
return const Result(true);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onSasl2Failure(XMLNode response) async {}
|
||||
|
||||
@override
|
||||
Future<List<XMLNode>> onSasl2FeaturesReceived(XMLNode sasl2Features) async {
|
||||
return [];
|
||||
}
|
||||
}
|
@ -6,15 +6,28 @@ import 'package:moxxmpp/src/events.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/sasl/errors.dart';
|
||||
import 'package:moxxmpp/src/negotiators/sasl/kv.dart';
|
||||
import 'package:moxxmpp/src/negotiators/sasl/negotiator.dart';
|
||||
import 'package:moxxmpp/src/negotiators/sasl/nonza.dart';
|
||||
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/errors.dart';
|
||||
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/kv.dart';
|
||||
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/nonza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/types/result.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
|
||||
import 'package:random_string/random_string.dart';
|
||||
import 'package:saslprep/saslprep.dart';
|
||||
|
||||
abstract class SaslScramError extends NegotiatorError {}
|
||||
|
||||
class NoAdditionalDataError extends SaslScramError {
|
||||
@override
|
||||
bool isRecoverable() => false;
|
||||
}
|
||||
|
||||
class InvalidServerSignatureError extends SaslScramError {
|
||||
@override
|
||||
bool isRecoverable() => false;
|
||||
}
|
||||
|
||||
// NOTE: Inspired by https://github.com/vukoye/xmpp_dart/blob/3b1a0588562b9e591488c99d834088391840911d/lib/src/features/sasl/ScramSaslHandler.dart
|
||||
|
||||
enum ScramHashType { sha1, sha256, sha512 }
|
||||
@ -95,7 +108,7 @@ enum ScramState { preSent, initialMessageSent, challengeResponseSent, error }
|
||||
|
||||
const gs2Header = 'n,,';
|
||||
|
||||
class SaslScramNegotiator extends SaslNegotiator {
|
||||
class SaslScramNegotiator extends Sasl2AuthenticationNegotiator {
|
||||
// NOTE: NEVER, and I mean, NEVER set clientNonce or initalMessageNoGS2. They are just there for testing
|
||||
SaslScramNegotiator(
|
||||
int priority,
|
||||
@ -230,29 +243,23 @@ class SaslScramNegotiator extends SaslNegotiator {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _checkSignature(String base64Signature) {
|
||||
final signature =
|
||||
parseKeyValue(utf8.decode(base64.decode(base64Signature)));
|
||||
return signature['v']! == _serverSignature;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<NegotiatorState, NegotiatorError>> negotiate(
|
||||
XMLNode nonza,
|
||||
) async {
|
||||
switch (_scramState) {
|
||||
case ScramState.preSent:
|
||||
if (clientNonce == null || clientNonce == '') {
|
||||
clientNonce = randomAlphaNumeric(
|
||||
40,
|
||||
provider: CoreRandomProvider.from(Random.secure()),
|
||||
);
|
||||
}
|
||||
|
||||
initialMessageNoGS2 =
|
||||
'n=${attributes.getConnectionSettings().jid.local},r=$clientNonce';
|
||||
|
||||
_scramState = ScramState.initialMessageSent;
|
||||
attributes.sendNonza(
|
||||
SaslScramAuthNonza(
|
||||
body: base64.encode(utf8.encode(gs2Header + initialMessageNoGS2)),
|
||||
body: await getRawStep(''),
|
||||
type: hashType,
|
||||
),
|
||||
redact: SaslScramAuthNonza(body: '******', type: hashType).toXml(),
|
||||
);
|
||||
return const Result(NegotiatorState.ready);
|
||||
case ScramState.initialMessageSent:
|
||||
@ -266,13 +273,8 @@ class SaslScramNegotiator extends SaslNegotiator {
|
||||
);
|
||||
}
|
||||
|
||||
final challengeBase64 = nonza.innerText();
|
||||
final response = await calculateChallengeResponse(challengeBase64);
|
||||
final responseBase64 = base64.encode(utf8.encode(response));
|
||||
_scramState = ScramState.challengeResponseSent;
|
||||
attributes.sendNonza(
|
||||
SaslScramResponseNonza(body: responseBase64),
|
||||
redact: SaslScramResponseNonza(body: '******').toXml(),
|
||||
SaslScramResponseNonza(body: await getRawStep(nonza.innerText())),
|
||||
);
|
||||
return const Result(NegotiatorState.ready);
|
||||
case ScramState.challengeResponseSent:
|
||||
@ -286,10 +288,7 @@ class SaslScramNegotiator extends SaslNegotiator {
|
||||
);
|
||||
}
|
||||
|
||||
// NOTE: This assumes that the string is always "v=..." and contains no other parameters
|
||||
final signature =
|
||||
parseKeyValue(utf8.decode(base64.decode(nonza.innerText())));
|
||||
if (signature['v']! != _serverSignature) {
|
||||
if (!_checkSignature(nonza.innerText())) {
|
||||
// TODO(Unknown): Notify of a signature mismatch
|
||||
//final error = nonza.children.first.tag;
|
||||
//attributes.sendEvent(AuthenticationFailedEvent(error));
|
||||
@ -299,7 +298,7 @@ class SaslScramNegotiator extends SaslNegotiator {
|
||||
);
|
||||
}
|
||||
|
||||
await attributes.sendEvent(AuthenticationSuccessEvent());
|
||||
attributes.setAuthenticated();
|
||||
return const Result(NegotiatorState.done);
|
||||
case ScramState.error:
|
||||
return Result(
|
||||
@ -314,4 +313,65 @@ class SaslScramNegotiator extends SaslNegotiator {
|
||||
|
||||
super.reset();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> getRawStep(String input) async {
|
||||
switch (_scramState) {
|
||||
case ScramState.preSent:
|
||||
if (clientNonce == null || clientNonce == '') {
|
||||
clientNonce = randomAlphaNumeric(
|
||||
40,
|
||||
provider: CoreRandomProvider.from(Random.secure()),
|
||||
);
|
||||
}
|
||||
|
||||
initialMessageNoGS2 =
|
||||
'n=${attributes.getConnectionSettings().jid.local},r=$clientNonce';
|
||||
|
||||
_scramState = ScramState.initialMessageSent;
|
||||
return base64.encode(utf8.encode(gs2Header + initialMessageNoGS2));
|
||||
case ScramState.initialMessageSent:
|
||||
final challengeBase64 = input;
|
||||
final response = await calculateChallengeResponse(challengeBase64);
|
||||
final responseBase64 = base64.encode(utf8.encode(response));
|
||||
_scramState = ScramState.challengeResponseSent;
|
||||
|
||||
return responseBase64;
|
||||
case ScramState.challengeResponseSent:
|
||||
case ScramState.error:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> postRegisterCallback() async {
|
||||
attributes
|
||||
.getNegotiatorById<Sasl2Negotiator>(sasl2Negotiator)
|
||||
?.registerSaslNegotiator(this);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<XMLNode>> onSasl2FeaturesReceived(XMLNode sasl2Features) async {
|
||||
return [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onSasl2Failure(XMLNode response) async {}
|
||||
|
||||
@override
|
||||
Future<Result<bool, NegotiatorError>> onSasl2Success(XMLNode response) async {
|
||||
// When we're done with SASL2, check the additional data to verify the server
|
||||
// signature.
|
||||
state = NegotiatorState.done;
|
||||
final additionalData = response.firstTag('additional-data');
|
||||
if (additionalData == null) {
|
||||
return Result(NoAdditionalDataError());
|
||||
}
|
||||
|
||||
if (!_checkSignature(additionalData.innerText())) {
|
||||
return Result(InvalidServerSignatureError());
|
||||
}
|
||||
|
||||
return const Result(true);
|
||||
}
|
||||
}
|
@ -5,8 +5,13 @@ class ConnectionSettings {
|
||||
required this.jid,
|
||||
required this.password,
|
||||
required this.useDirectTLS,
|
||||
this.host,
|
||||
this.port,
|
||||
});
|
||||
final JID jid;
|
||||
final String password;
|
||||
final bool useDirectTLS;
|
||||
|
||||
final String? host;
|
||||
final int? port;
|
||||
}
|
||||
|
@ -30,9 +30,8 @@ abstract class BaseSocketWrapper {
|
||||
/// reused by calling [this.connect] again.
|
||||
void close();
|
||||
|
||||
/// Write [data] into the socket. If [redact] is not null, then [redact] will be
|
||||
/// logged instead of [data].
|
||||
void write(String data, {String? redact});
|
||||
/// Write [data] into the socket.
|
||||
void write(String data);
|
||||
|
||||
/// This must connect to [host]:[port] and initialize the streams accordingly.
|
||||
/// [domain] is the domain that TLS should be validated against, in case the Socket
|
||||
|
@ -146,4 +146,6 @@ class XMLNode {
|
||||
String innerText() {
|
||||
return text ?? '';
|
||||
}
|
||||
|
||||
String? get xmlns => attributes['xmlns'] as String?;
|
||||
}
|
||||
|
167
packages/moxxmpp/lib/src/xeps/staging/fast.dart
Normal file
167
packages/moxxmpp/lib/src/xeps/staging/fast.dart
Normal file
@ -0,0 +1,167 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:moxxmpp/src/events.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/stringxml.dart';
|
||||
import 'package:moxxmpp/src/types/result.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
|
||||
|
||||
/// This event is triggered whenever a new FAST token is received.
|
||||
class NewFASTTokenReceivedEvent extends XmppEvent {
|
||||
NewFASTTokenReceivedEvent(this.token);
|
||||
|
||||
/// The token.
|
||||
final FASTToken token;
|
||||
}
|
||||
|
||||
/// This event is triggered whenever a new FAST token is invalidated because it's
|
||||
/// invalid.
|
||||
class InvalidateFASTTokenEvent extends XmppEvent {
|
||||
InvalidateFASTTokenEvent();
|
||||
}
|
||||
|
||||
/// The description of a token for FAST authentication.
|
||||
class FASTToken {
|
||||
const FASTToken(
|
||||
this.token,
|
||||
this.expiry,
|
||||
);
|
||||
|
||||
factory FASTToken.fromXml(XMLNode token) {
|
||||
assert(
|
||||
token.tag == 'token',
|
||||
'Token can only be deserialised from a <token /> element',
|
||||
);
|
||||
assert(
|
||||
token.xmlns == fastXmlns,
|
||||
'Token can only be deserialised from a <token /> element',
|
||||
);
|
||||
|
||||
return FASTToken(
|
||||
token.attributes['token']! as String,
|
||||
token.attributes['expiry']! as String,
|
||||
);
|
||||
}
|
||||
|
||||
/// The actual token.
|
||||
final String token;
|
||||
|
||||
/// The token's expiry.
|
||||
final String expiry;
|
||||
}
|
||||
|
||||
// TODO(Unknown): Implement multiple hash functions, similar to how we do SCRAM
|
||||
class FASTSaslNegotiator extends Sasl2AuthenticationNegotiator {
|
||||
FASTSaslNegotiator() : super(20, saslFASTNegotiator, 'HT-SHA-256-NONE');
|
||||
|
||||
final Logger _log = Logger('FASTSaslNegotiator');
|
||||
|
||||
/// The token, if non-null, to use for authentication.
|
||||
FASTToken? fastToken;
|
||||
|
||||
@override
|
||||
bool matchesFeature(List<XMLNode> features) {
|
||||
if (fastToken == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (super.matchesFeature(features)) {
|
||||
if (!attributes.getSocket().isSecure()) {
|
||||
_log.warning(
|
||||
'Refusing to match SASL feature due to unsecured connection',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
bool canInlineFeature(List<XMLNode> features) {
|
||||
return features.firstWhereOrNull(
|
||||
(child) => child.tag == 'fast' && child.xmlns == fastXmlns,
|
||||
) !=
|
||||
null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<NegotiatorState, NegotiatorError>> negotiate(
|
||||
XMLNode nonza,
|
||||
) async {
|
||||
// TODO(Unknown): Is FAST supposed to work without SASL2?
|
||||
return const Result(NegotiatorState.done);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<bool, NegotiatorError>> onSasl2Success(XMLNode response) async {
|
||||
final token = response.firstTag('token', xmlns: fastXmlns);
|
||||
if (token != null) {
|
||||
fastToken = FASTToken.fromXml(token);
|
||||
await attributes.sendEvent(
|
||||
NewFASTTokenReceivedEvent(fastToken!),
|
||||
);
|
||||
}
|
||||
|
||||
state = NegotiatorState.done;
|
||||
return const Result(true);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onSasl2Failure(XMLNode response) async {
|
||||
fastToken = null;
|
||||
await attributes.sendEvent(
|
||||
InvalidateFASTTokenEvent(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRetrySasl() => true;
|
||||
|
||||
@override
|
||||
Future<List<XMLNode>> onSasl2FeaturesReceived(XMLNode sasl2Features) async {
|
||||
if (fastToken != null && pickedForSasl2) {
|
||||
// Specify that we are using a token
|
||||
return [
|
||||
// As we don't do TLS 0-RTT, we don't have to specify `count`.
|
||||
XMLNode.xmlns(
|
||||
tag: 'fast',
|
||||
xmlns: fastXmlns,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
// Only request a new token when we don't already have one and we are not picked
|
||||
// for SASL
|
||||
if (!pickedForSasl2) {
|
||||
return [
|
||||
XMLNode.xmlns(
|
||||
tag: 'request-token',
|
||||
xmlns: fastXmlns,
|
||||
attributes: {
|
||||
'mechanism': 'HT-SHA-256-NONE',
|
||||
},
|
||||
),
|
||||
];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> getRawStep(String input) async {
|
||||
return fastToken!.token;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> postRegisterCallback() async {
|
||||
attributes
|
||||
.getNegotiatorById<Sasl2Negotiator>(sasl2Negotiator)
|
||||
?.registerSaslNegotiator(this);
|
||||
}
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:moxxmpp/src/events.dart';
|
||||
import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
@ -10,6 +12,9 @@ import 'package:moxxmpp/src/xeps/xep_0198/nonzas.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0198/state.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0352.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0386.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
|
||||
|
||||
enum _StreamManagementNegotiatorState {
|
||||
// We have not done anything yet
|
||||
@ -23,26 +28,71 @@ enum _StreamManagementNegotiatorState {
|
||||
/// NOTE: The stream management negotiator requires that loadState has been called on the
|
||||
/// StreamManagementManager at least once before connecting, if stream resumption
|
||||
/// is wanted.
|
||||
class StreamManagementNegotiator extends XmppFeatureNegotiatorBase {
|
||||
class StreamManagementNegotiator extends Sasl2FeatureNegotiator
|
||||
implements Bind2FeatureNegotiatorInterface {
|
||||
StreamManagementNegotiator()
|
||||
: _state = _StreamManagementNegotiatorState.ready,
|
||||
_supported = false,
|
||||
_resumeFailed = false,
|
||||
_isResumed = false,
|
||||
_log = Logger('StreamManagementNegotiator'),
|
||||
super(10, false, smXmlns, streamManagementNegotiator);
|
||||
_StreamManagementNegotiatorState _state;
|
||||
bool _resumeFailed;
|
||||
bool _isResumed;
|
||||
: super(10, false, smXmlns, streamManagementNegotiator);
|
||||
|
||||
final Logger _log;
|
||||
/// Stream Management negotiation state.
|
||||
_StreamManagementNegotiatorState _state =
|
||||
_StreamManagementNegotiatorState.ready;
|
||||
|
||||
/// Flag indicating whether the resume failed (true) or succeeded (false).
|
||||
bool _resumeFailed = false;
|
||||
bool get resumeFailed => _resumeFailed;
|
||||
|
||||
/// Flag indicating whether the current stream is resumed (true) or not (false).
|
||||
bool _isResumed = false;
|
||||
bool get isResumed => _isResumed;
|
||||
|
||||
/// Flag indicating that stream enablement failed
|
||||
bool _streamEnablementFailed = false;
|
||||
bool get streamEnablementFailed => _streamEnablementFailed;
|
||||
|
||||
/// Logger
|
||||
final Logger _log = Logger('StreamManagementNegotiator');
|
||||
|
||||
/// True if Stream Management is supported on this stream.
|
||||
bool _supported;
|
||||
bool _supported = false;
|
||||
bool get isSupported => _supported;
|
||||
|
||||
/// True if the current stream is resumed. False if not.
|
||||
bool get isResumed => _isResumed;
|
||||
/// True if we requested stream enablement inline
|
||||
bool _inlineStreamEnablementRequested = false;
|
||||
|
||||
/// Cached resource for stream resumption
|
||||
String _resource = '';
|
||||
@visibleForTesting
|
||||
void setResource(String resource) {
|
||||
_resource = resource;
|
||||
}
|
||||
|
||||
@override
|
||||
bool canInlineFeature(List<XMLNode> features) {
|
||||
final sm = attributes.getManagerById<StreamManagementManager>(smManager)!;
|
||||
|
||||
// We do not check here for authentication as enabling/resuming happens inline
|
||||
// with the authentication.
|
||||
if (sm.state.streamResumptionId != null && !_resumeFailed) {
|
||||
// We can try to resume the stream or enable the stream
|
||||
return features.firstWhereOrNull(
|
||||
(child) => child.xmlns == smXmlns,
|
||||
) !=
|
||||
null;
|
||||
} else {
|
||||
// We can try to enable SM
|
||||
return features.firstWhereOrNull(
|
||||
(child) => child.tag == 'enable' && child.xmlns == smXmlns,
|
||||
) !=
|
||||
null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onXmppEvent(XmppEvent event) async {
|
||||
if (event is ResourceBoundEvent) {
|
||||
_resource = event.resource;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool matchesFeature(List<XMLNode> features) {
|
||||
@ -53,13 +103,66 @@ class StreamManagementNegotiator extends XmppFeatureNegotiatorBase {
|
||||
return super.matchesFeature(features) && attributes.isAuthenticated();
|
||||
} else {
|
||||
// We cannot do a stream resumption
|
||||
final br = attributes.getNegotiatorById(resourceBindingNegotiator);
|
||||
return super.matchesFeature(features) &&
|
||||
br?.state == NegotiatorState.done &&
|
||||
attributes.getConnection().resource.isNotEmpty &&
|
||||
attributes.isAuthenticated();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onStreamResumptionFailed() async {
|
||||
await attributes.sendEvent(StreamResumeFailedEvent());
|
||||
final sm = attributes.getManagerById<StreamManagementManager>(smManager)!;
|
||||
|
||||
// We have to do this because we otherwise get a stanza stuck in the queue,
|
||||
// thus spamming the server on every <a /> nonza we receive.
|
||||
// ignore: cascade_invocations
|
||||
await sm.setState(StreamManagementState(0, 0));
|
||||
await sm.commitState();
|
||||
|
||||
_resumeFailed = true;
|
||||
_isResumed = false;
|
||||
_state = _StreamManagementNegotiatorState.ready;
|
||||
}
|
||||
|
||||
Future<void> _onStreamResumptionSuccessful(XMLNode resumed) async {
|
||||
assert(resumed.tag == 'resumed', 'The correct element must be passed');
|
||||
|
||||
final h = int.parse(resumed.attributes['h']! as String);
|
||||
await attributes.sendEvent(StreamResumedEvent(h: h));
|
||||
|
||||
_resumeFailed = false;
|
||||
_isResumed = true;
|
||||
|
||||
if (attributes.getConnection().resource.isEmpty && _resource.isNotEmpty) {
|
||||
attributes.setResource(_resource);
|
||||
} else if (attributes.getConnection().resource.isNotEmpty &&
|
||||
_resource.isEmpty) {
|
||||
_resource = attributes.getConnection().resource;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onStreamEnablementSuccessful(XMLNode enabled) async {
|
||||
assert(enabled.tag == 'enabled', 'The correct element must be used');
|
||||
assert(enabled.xmlns == smXmlns, 'The correct element must be used');
|
||||
|
||||
final id = enabled.attributes['id'] as String?;
|
||||
if (id != null && ['true', '1'].contains(enabled.attributes['resume'])) {
|
||||
_log.info('Stream Resumption available');
|
||||
}
|
||||
|
||||
await attributes.sendEvent(
|
||||
StreamManagementEnabledEvent(
|
||||
resource: attributes.getFullJID().resource,
|
||||
id: id,
|
||||
location: enabled.attributes['location'] as String?,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onStreamEnablementFailed() {
|
||||
_streamEnablementFailed = true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<NegotiatorState, NegotiatorError>> negotiate(
|
||||
XMLNode nonza,
|
||||
@ -103,54 +206,26 @@ class StreamManagementNegotiator extends XmppFeatureNegotiatorBase {
|
||||
csi.restoreCSIState();
|
||||
}
|
||||
|
||||
final h = int.parse(nonza.attributes['h']! as String);
|
||||
await attributes.sendEvent(StreamResumedEvent(h: h));
|
||||
|
||||
_resumeFailed = false;
|
||||
_isResumed = true;
|
||||
await _onStreamResumptionSuccessful(nonza);
|
||||
return const Result(NegotiatorState.skipRest);
|
||||
} else {
|
||||
// We assume it is <failed />
|
||||
_log.info(
|
||||
'Stream resumption failed. Expected <resumed />, got ${nonza.tag}, Proceeding with new stream...',
|
||||
);
|
||||
await attributes.sendEvent(StreamResumeFailedEvent());
|
||||
final sm =
|
||||
attributes.getManagerById<StreamManagementManager>(smManager)!;
|
||||
|
||||
// We have to do this because we otherwise get a stanza stuck in the queue,
|
||||
// thus spamming the server on every <a /> nonza we receive.
|
||||
// ignore: cascade_invocations
|
||||
await sm.setState(StreamManagementState(0, 0));
|
||||
await sm.commitState();
|
||||
|
||||
_resumeFailed = true;
|
||||
_isResumed = false;
|
||||
_state = _StreamManagementNegotiatorState.ready;
|
||||
await _onStreamResumptionFailed();
|
||||
return const Result(NegotiatorState.retryLater);
|
||||
}
|
||||
case _StreamManagementNegotiatorState.enableRequested:
|
||||
if (nonza.tag == 'enabled') {
|
||||
_log.finest('Stream Management enabled');
|
||||
|
||||
final id = nonza.attributes['id'] as String?;
|
||||
if (id != null &&
|
||||
['true', '1'].contains(nonza.attributes['resume'])) {
|
||||
_log.info('Stream Resumption available');
|
||||
}
|
||||
|
||||
await attributes.sendEvent(
|
||||
StreamManagementEnabledEvent(
|
||||
resource: attributes.getFullJID().resource,
|
||||
id: id,
|
||||
location: nonza.attributes['location'] as String?,
|
||||
),
|
||||
);
|
||||
await _onStreamEnablementSuccessful(nonza);
|
||||
|
||||
return const Result(NegotiatorState.done);
|
||||
} else {
|
||||
// We assume a <failed />
|
||||
_log.warning('Stream Management enablement failed');
|
||||
_onStreamEnablementFailed();
|
||||
return const Result(NegotiatorState.done);
|
||||
}
|
||||
}
|
||||
@ -162,7 +237,97 @@ class StreamManagementNegotiator extends XmppFeatureNegotiatorBase {
|
||||
_supported = false;
|
||||
_resumeFailed = false;
|
||||
_isResumed = false;
|
||||
_inlineStreamEnablementRequested = false;
|
||||
_streamEnablementFailed = false;
|
||||
|
||||
super.reset();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<XMLNode>> onBind2FeaturesReceived(
|
||||
List<String> bind2Features,
|
||||
) async {
|
||||
if (!bind2Features.contains(smXmlns)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
_inlineStreamEnablementRequested = true;
|
||||
return [
|
||||
StreamManagementEnableNonza(),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onBind2Success(XMLNode response) async {}
|
||||
|
||||
@override
|
||||
Future<List<XMLNode>> onSasl2FeaturesReceived(XMLNode sasl2Features) async {
|
||||
final inline = sasl2Features.firstTag('inline')!;
|
||||
final resume = inline.firstTag('resume', xmlns: smXmlns);
|
||||
|
||||
if (resume == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final sm = attributes.getManagerById<StreamManagementManager>(smManager)!;
|
||||
final srid = sm.state.streamResumptionId;
|
||||
final h = sm.state.s2c;
|
||||
if (srid == null) {
|
||||
_log.finest('No srid');
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
StreamManagementResumeNonza(
|
||||
srid,
|
||||
h,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<bool, NegotiatorError>> onSasl2Success(XMLNode response) async {
|
||||
final enabled = response
|
||||
.firstTag('bound', xmlns: bind2Xmlns)
|
||||
?.firstTag('enabled', xmlns: smXmlns);
|
||||
final resumed = response.firstTag('resumed', xmlns: smXmlns);
|
||||
// We can only enable or resume->fail->enable. Thus, we check for enablement first
|
||||
// and then exit.
|
||||
if (_inlineStreamEnablementRequested) {
|
||||
if (enabled != null) {
|
||||
_log.finest('Inline stream enablement successful');
|
||||
await _onStreamEnablementSuccessful(enabled);
|
||||
return const Result(true);
|
||||
} else {
|
||||
_log.warning('Inline stream enablement failed');
|
||||
_onStreamEnablementFailed();
|
||||
}
|
||||
}
|
||||
|
||||
if (resumed == null) {
|
||||
_log.warning('Inline stream resumption failed');
|
||||
await _onStreamResumptionFailed();
|
||||
state = NegotiatorState.done;
|
||||
return const Result(true);
|
||||
}
|
||||
|
||||
_log.finest('Inline stream resumption successful');
|
||||
await _onStreamResumptionSuccessful(resumed);
|
||||
state = NegotiatorState.skipRest;
|
||||
|
||||
attributes.removeNegotiatingFeature(smXmlns);
|
||||
attributes.removeNegotiatingFeature(bindXmlns);
|
||||
|
||||
return const Result(true);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> postRegisterCallback() async {
|
||||
attributes
|
||||
.getNegotiatorById<Sasl2Negotiator>(sasl2Negotiator)
|
||||
?.registerNegotiator(this);
|
||||
attributes
|
||||
.getNegotiatorById<Bind2Negotiator>(bind2Negotiator)
|
||||
?.registerNegotiator(this);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:moxxmpp/src/connection.dart';
|
||||
import 'package:moxxmpp/src/events.dart';
|
||||
@ -7,10 +8,12 @@ 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/stanza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.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 {
|
||||
@ -173,6 +176,16 @@ class CarbonsManager extends XmppManagerBase {
|
||||
_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.
|
||||
///
|
||||
@ -185,3 +198,55 @@ class CarbonsManager extends XmppManagerBase {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CarbonsNegotiator extends Bind2FeatureNegotiator {
|
||||
CarbonsNegotiator() : super(0, 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
|
||||
Future<void> onBind2Success(XMLNode response) async {
|
||||
if (!_requestedEnablement) {
|
||||
return;
|
||||
}
|
||||
|
||||
final enabled = response.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();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<XMLNode>> onBind2FeaturesReceived(
|
||||
List<String> bind2Features,
|
||||
) async {
|
||||
if (!bind2Features.contains(carbonsXmlns)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
_requestedEnablement = true;
|
||||
return [
|
||||
XMLNode.xmlns(
|
||||
tag: 'enable',
|
||||
xmlns: carbonsXmlns,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
void reset() {
|
||||
_requestedEnablement = false;
|
||||
|
||||
super.reset();
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import 'package:moxxmpp/src/negotiators/namespaces.dart';
|
||||
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/types/result.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0386.dart';
|
||||
|
||||
class CSIActiveNonza extends XMLNode {
|
||||
CSIActiveNonza()
|
||||
@ -23,7 +24,8 @@ class CSIInactiveNonza extends XMLNode {
|
||||
}
|
||||
|
||||
/// A Stub negotiator that is just for "intercepting" the stream feature.
|
||||
class CSINegotiator extends XmppFeatureNegotiatorBase {
|
||||
class CSINegotiator extends XmppFeatureNegotiatorBase
|
||||
implements Bind2FeatureNegotiatorInterface {
|
||||
CSINegotiator() : super(11, false, csiXmlns, csiNegotiator);
|
||||
|
||||
/// True if CSI is supported. False otherwise.
|
||||
@ -40,19 +42,47 @@ class CSINegotiator extends XmppFeatureNegotiatorBase {
|
||||
return const Result(NegotiatorState.done);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<XMLNode>> onBind2FeaturesReceived(
|
||||
List<String> bind2Features,
|
||||
) async {
|
||||
if (!bind2Features.contains(csiXmlns)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
_supported = true;
|
||||
final active = attributes.getManagerById<CSIManager>(csiManager)!.isActive;
|
||||
return [
|
||||
if (active) CSIActiveNonza() else CSIInactiveNonza(),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onBind2Success(XMLNode response) async {}
|
||||
|
||||
@override
|
||||
void reset() {
|
||||
_supported = false;
|
||||
|
||||
super.reset();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> postRegisterCallback() async {
|
||||
attributes
|
||||
.getNegotiatorById<Bind2Negotiator>(bind2Negotiator)
|
||||
?.registerNegotiator(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// The manager requires a CSINegotiator to be registered as a feature negotiator.
|
||||
class CSIManager extends XmppManagerBase {
|
||||
CSIManager() : super(csiManager);
|
||||
|
||||
/// Flag indicating whether the application is currently active and the CSI
|
||||
/// traffic optimisation should be disabled (true).
|
||||
bool _isActive = true;
|
||||
bool get isActive => _isActive;
|
||||
|
||||
@override
|
||||
Future<bool> isSupported() async {
|
||||
@ -71,23 +101,31 @@ class CSIManager extends XmppManagerBase {
|
||||
}
|
||||
}
|
||||
|
||||
/// Tells the server to top optimizing traffic
|
||||
Future<void> setActive() async {
|
||||
/// Tells the server to stop optimizing traffic.
|
||||
/// If [sendNonza] is false, then no nonza is sent. This is useful
|
||||
/// for setting up the CSI manager for Bind2.
|
||||
Future<void> setActive({bool sendNonza = true}) async {
|
||||
_isActive = true;
|
||||
|
||||
final attrs = getAttributes();
|
||||
if (await isSupported()) {
|
||||
attrs.sendNonza(CSIActiveNonza());
|
||||
if (sendNonza) {
|
||||
final attrs = getAttributes();
|
||||
if (await isSupported()) {
|
||||
attrs.sendNonza(CSIActiveNonza());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tells the server to optimize traffic following XEP-0352
|
||||
Future<void> setInactive() async {
|
||||
/// If [sendNonza] is false, then no nonza is sent. This is useful
|
||||
/// for setting up the CSI manager for Bind2.
|
||||
Future<void> setInactive({bool sendNonza = true}) async {
|
||||
_isActive = false;
|
||||
|
||||
final attrs = getAttributes();
|
||||
if (await isSupported()) {
|
||||
attrs.sendNonza(CSIInactiveNonza());
|
||||
if (sendNonza) {
|
||||
final attrs = getAttributes();
|
||||
if (await isSupported()) {
|
||||
attrs.sendNonza(CSIInactiveNonza());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
142
packages/moxxmpp/lib/src/xeps/xep_0386.dart
Normal file
142
packages/moxxmpp/lib/src/xeps/xep_0386.dart
Normal file
@ -0,0 +1,142 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:meta/meta.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/stringxml.dart';
|
||||
import 'package:moxxmpp/src/types/result.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
|
||||
|
||||
/// An interface that allows registering against Bind2's feature list in order to
|
||||
/// negotiate features inline with Bind2.
|
||||
// ignore: one_member_abstracts
|
||||
abstract class Bind2FeatureNegotiatorInterface {
|
||||
/// Called by the Bind2 negotiator when Bind2 features are received. The returned
|
||||
/// [XMLNode]s are added to Bind2's bind request.
|
||||
Future<List<XMLNode>> onBind2FeaturesReceived(List<String> bind2Features);
|
||||
|
||||
/// Called by the Bind2 negotiator when Bind2 results are received.
|
||||
Future<void> onBind2Success(XMLNode result);
|
||||
}
|
||||
|
||||
/// A class that allows for simple negotiators that only registers itself against
|
||||
/// the Bind2 negotiator. You only have to implement the functions required by
|
||||
/// [Bind2FeatureNegotiatorInterface].
|
||||
abstract class Bind2FeatureNegotiator extends XmppFeatureNegotiatorBase
|
||||
implements Bind2FeatureNegotiatorInterface {
|
||||
Bind2FeatureNegotiator(
|
||||
int priority,
|
||||
String negotiatingXmlns,
|
||||
String id,
|
||||
) : super(priority, false, negotiatingXmlns, id);
|
||||
|
||||
@override
|
||||
bool matchesFeature(List<XMLNode> features) => false;
|
||||
|
||||
@override
|
||||
Future<Result<NegotiatorState, NegotiatorError>> negotiate(
|
||||
XMLNode nonza,
|
||||
) async {
|
||||
return const Result(NegotiatorState.done);
|
||||
}
|
||||
|
||||
@mustCallSuper
|
||||
@override
|
||||
Future<void> postRegisterCallback() async {
|
||||
attributes
|
||||
.getNegotiatorById<Bind2Negotiator>(bind2Negotiator)!
|
||||
.registerNegotiator(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// A negotiator implementing XEP-0386. This negotiator is useless on its own
|
||||
/// and requires a [Sasl2Negotiator] to be registered.
|
||||
class Bind2Negotiator extends Sasl2FeatureNegotiator {
|
||||
Bind2Negotiator() : super(0, false, bind2Xmlns, bind2Negotiator);
|
||||
|
||||
/// A list of negotiators that can work with Bind2.
|
||||
final List<Bind2FeatureNegotiatorInterface> _negotiators =
|
||||
List<Bind2FeatureNegotiatorInterface>.empty(growable: true);
|
||||
|
||||
/// A tag to sent to the server when requesting Bind2.
|
||||
String? tag;
|
||||
|
||||
/// Register [negotiator] against the Bind2 negotiator to append data to the Bind2
|
||||
/// negotiation.
|
||||
void registerNegotiator(Bind2FeatureNegotiatorInterface negotiator) {
|
||||
_negotiators.add(negotiator);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<NegotiatorState, NegotiatorError>> negotiate(
|
||||
XMLNode nonza,
|
||||
) async {
|
||||
return const Result(NegotiatorState.done);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<XMLNode>> onSasl2FeaturesReceived(XMLNode sasl2Features) async {
|
||||
final children = List<XMLNode>.empty(growable: true);
|
||||
if (_negotiators.isNotEmpty) {
|
||||
final inline = sasl2Features
|
||||
.firstTag('inline')!
|
||||
.firstTag('bind', xmlns: bind2Xmlns)!
|
||||
.firstTag('inline');
|
||||
if (inline != null) {
|
||||
final features = inline.children
|
||||
.where((child) => child.tag == 'feature')
|
||||
.map((child) => child.attributes['var']! as String)
|
||||
.toList();
|
||||
|
||||
// Only call the negotiators if Bind2 allows doing stuff inline
|
||||
for (final negotiator in _negotiators) {
|
||||
children.addAll(await negotiator.onBind2FeaturesReceived(features));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
XMLNode.xmlns(
|
||||
tag: 'bind',
|
||||
xmlns: bind2Xmlns,
|
||||
children: [
|
||||
if (tag != null)
|
||||
XMLNode(
|
||||
tag: 'tag',
|
||||
text: tag,
|
||||
),
|
||||
...children,
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
bool canInlineFeature(List<XMLNode> features) {
|
||||
return features.firstWhereOrNull(
|
||||
(child) => child.tag == 'bind' && child.xmlns == bind2Xmlns,
|
||||
) !=
|
||||
null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<bool, NegotiatorError>> onSasl2Success(XMLNode response) async {
|
||||
final bound = response.firstTag('bound', xmlns: bind2Xmlns);
|
||||
if (bound != null) {
|
||||
for (final negotiator in _negotiators) {
|
||||
await negotiator.onBind2Success(bound);
|
||||
}
|
||||
}
|
||||
|
||||
attributes.removeNegotiatingFeature(bindXmlns);
|
||||
return const Result(true);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> postRegisterCallback() async {
|
||||
attributes
|
||||
.getNegotiatorById<Sasl2Negotiator>(sasl2Negotiator)!
|
||||
.registerNegotiator(this);
|
||||
}
|
||||
}
|
8
packages/moxxmpp/lib/src/xeps/xep_0388/errors.dart
Normal file
8
packages/moxxmpp/lib/src/xeps/xep_0388/errors.dart
Normal file
@ -0,0 +1,8 @@
|
||||
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||
|
||||
/// Triggered by the SASL2 negotiator when no SASL mechanism was chosen during
|
||||
/// negotiation.
|
||||
class NoSASLMechanismSelectedError extends NegotiatorError {
|
||||
@override
|
||||
bool isRecoverable() => false;
|
||||
}
|
72
packages/moxxmpp/lib/src/xeps/xep_0388/negotiators.dart
Normal file
72
packages/moxxmpp/lib/src/xeps/xep_0388/negotiators.dart
Normal file
@ -0,0 +1,72 @@
|
||||
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/negotiator.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/types/result.dart';
|
||||
|
||||
/// A special type of [XmppFeatureNegotiatorBase] that is aware of SASL2.
|
||||
abstract class Sasl2FeatureNegotiator extends XmppFeatureNegotiatorBase {
|
||||
Sasl2FeatureNegotiator(
|
||||
super.priority,
|
||||
super.sendStreamHeaderWhenDone,
|
||||
super.negotiatingXmlns,
|
||||
super.id,
|
||||
);
|
||||
|
||||
/// Called by the SASL2 negotiator when we received the SASL2 stream features
|
||||
/// [sasl2Features]. The return value is a list of XML elements that should be
|
||||
/// added to the SASL2 <authenticate /> nonza.
|
||||
/// This method is only called when the <inline /> element contains an item with
|
||||
/// xmlns equal to [negotiatingXmlns].
|
||||
Future<List<XMLNode>> onSasl2FeaturesReceived(XMLNode sasl2Features);
|
||||
|
||||
/// Called by the SASL2 negotiator when the SASL2 negotiations are done. [response]
|
||||
/// is the entire response nonza.
|
||||
/// This method is only called when the previous <inline /> element contains an
|
||||
/// item with xmlns equal to [negotiatingXmlns].
|
||||
Future<Result<bool, NegotiatorError>> onSasl2Success(XMLNode response);
|
||||
|
||||
/// Called by the SASL2 negotiator when the SASL2 negotiations have failed. [response]
|
||||
/// is the entire response nonza.
|
||||
Future<void> onSasl2Failure(XMLNode response) async {}
|
||||
|
||||
/// Called by the SASL2 negotiator to find out whether the negotiator is willing
|
||||
/// to inline a feature. [features] is the list of elements inside the <inline />
|
||||
/// element.
|
||||
bool canInlineFeature(List<XMLNode> features);
|
||||
}
|
||||
|
||||
/// A special type of [SaslNegotiator] that is aware of SASL2.
|
||||
abstract class Sasl2AuthenticationNegotiator extends SaslNegotiator
|
||||
implements Sasl2FeatureNegotiator {
|
||||
Sasl2AuthenticationNegotiator(super.priority, super.id, super.mechanismName);
|
||||
|
||||
/// Flag indicating whether this negotiator was chosen during SASL2 as the SASL
|
||||
/// negotiator to use.
|
||||
bool _pickedForSasl2 = false;
|
||||
bool get pickedForSasl2 => _pickedForSasl2;
|
||||
|
||||
/// Perform a SASL step with [input] as the already parsed input data. Returns
|
||||
/// the base64-encoded response data.
|
||||
Future<String> getRawStep(String input);
|
||||
|
||||
/// Tells the negotiator that it has been selected as the SASL negotiator for SASL2.
|
||||
void pickForSasl2() {
|
||||
_pickedForSasl2 = true;
|
||||
}
|
||||
|
||||
/// When SASL2 fails, should we retry (true) or just fail (false).
|
||||
/// Defaults to just returning false.
|
||||
bool shouldRetrySasl() => false;
|
||||
|
||||
@override
|
||||
void reset() {
|
||||
_pickedForSasl2 = false;
|
||||
|
||||
super.reset();
|
||||
}
|
||||
|
||||
@override
|
||||
bool canInlineFeature(List<XMLNode> features) {
|
||||
return true;
|
||||
}
|
||||
}
|
46
packages/moxxmpp/lib/src/xeps/xep_0388/user_agent.dart
Normal file
46
packages/moxxmpp/lib/src/xeps/xep_0388/user_agent.dart
Normal file
@ -0,0 +1,46 @@
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
|
||||
/// A data class describing the user agent. See https://dyn.eightysoft.de/final/xep-0388.html#initiation
|
||||
class UserAgent {
|
||||
const UserAgent({
|
||||
this.id,
|
||||
this.software,
|
||||
this.device,
|
||||
});
|
||||
|
||||
/// The identifier of the software/device combo connecting. SHOULD be a UUIDv4.
|
||||
final String? id;
|
||||
|
||||
/// The software's name that's connecting at the moment.
|
||||
final String? software;
|
||||
|
||||
/// The name of the device.
|
||||
final String? device;
|
||||
|
||||
XMLNode toXml() {
|
||||
assert(
|
||||
id != null || software != null || device != null,
|
||||
'A completely empty user agent makes no sense',
|
||||
);
|
||||
return XMLNode(
|
||||
tag: 'user-agent',
|
||||
attributes: id != null
|
||||
? {
|
||||
'id': id,
|
||||
}
|
||||
: {},
|
||||
children: [
|
||||
if (software != null)
|
||||
XMLNode(
|
||||
tag: 'software',
|
||||
text: software,
|
||||
),
|
||||
if (device != null)
|
||||
XMLNode(
|
||||
tag: 'device',
|
||||
text: device,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
209
packages/moxxmpp/lib/src/xeps/xep_0388/xep_0388.dart
Normal file
209
packages/moxxmpp/lib/src/xeps/xep_0388/xep_0388.dart
Normal file
@ -0,0 +1,209 @@
|
||||
import 'package:moxxmpp/src/jid.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/rfcs/rfc_6120/sasl/errors.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/types/result.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0388/errors.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0388/user_agent.dart';
|
||||
|
||||
/// The state of the SASL2 negotiation
|
||||
enum Sasl2State {
|
||||
// No request has been sent yet.
|
||||
idle,
|
||||
// We have sent the <authenticate /> nonza.
|
||||
authenticateSent,
|
||||
}
|
||||
|
||||
/// A negotiator that implements XEP-0388 SASL2. Alone, it does nothing. Has to be
|
||||
/// registered with other negotiators that register themselves against this one.
|
||||
class Sasl2Negotiator extends XmppFeatureNegotiatorBase {
|
||||
Sasl2Negotiator({
|
||||
this.userAgent,
|
||||
}) : super(100, false, sasl2Xmlns, sasl2Negotiator);
|
||||
|
||||
/// The user agent data that will be sent to the server when authenticating.
|
||||
final UserAgent? userAgent;
|
||||
|
||||
/// List of callbacks that are registered against us. Will be called once we get
|
||||
/// SASL2 features.
|
||||
final List<Sasl2FeatureNegotiator> _featureNegotiators =
|
||||
List<Sasl2FeatureNegotiator>.empty(growable: true);
|
||||
|
||||
/// List of SASL negotiators, sorted by their priority. The higher the priority, the
|
||||
/// lower its index.
|
||||
final List<Sasl2AuthenticationNegotiator> _saslNegotiators =
|
||||
List<Sasl2AuthenticationNegotiator>.empty(growable: true);
|
||||
|
||||
/// The state the SASL2 negotiator is currently in.
|
||||
Sasl2State _sasl2State = Sasl2State.idle;
|
||||
|
||||
/// The SASL negotiator that will negotiate authentication.
|
||||
Sasl2AuthenticationNegotiator? _currentSaslNegotiator;
|
||||
|
||||
/// The SASL2 <authentication /> element we received with the stream features.
|
||||
XMLNode? _sasl2Data;
|
||||
final List<String> _activeSasl2Negotiators =
|
||||
List<String>.empty(growable: true);
|
||||
|
||||
/// Register a SASL negotiator so that we can use that SASL implementation during
|
||||
/// SASL2.
|
||||
void registerSaslNegotiator(Sasl2AuthenticationNegotiator negotiator) {
|
||||
_featureNegotiators.add(negotiator);
|
||||
_saslNegotiators
|
||||
..add(negotiator)
|
||||
..sort((a, b) => b.priority.compareTo(a.priority));
|
||||
}
|
||||
|
||||
/// Register a feature negotiator so that we can negotitate that feature inline with
|
||||
/// the SASL authentication.
|
||||
void registerNegotiator(Sasl2FeatureNegotiator negotiator) {
|
||||
_featureNegotiators.add(negotiator);
|
||||
}
|
||||
|
||||
@override
|
||||
bool matchesFeature(List<XMLNode> features) {
|
||||
// Only do SASL2 when the socket is secure
|
||||
return attributes.getSocket().isSecure() && super.matchesFeature(features);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<NegotiatorState, NegotiatorError>> negotiate(
|
||||
XMLNode nonza,
|
||||
) async {
|
||||
switch (_sasl2State) {
|
||||
case Sasl2State.idle:
|
||||
_sasl2Data = nonza.firstTag('authentication', xmlns: sasl2Xmlns);
|
||||
final mechanisms = XMLNode.xmlns(
|
||||
tag: 'mechanisms',
|
||||
xmlns: saslXmlns,
|
||||
children:
|
||||
_sasl2Data!.children.where((c) => c.tag == 'mechanism').toList(),
|
||||
);
|
||||
for (final negotiator in _saslNegotiators) {
|
||||
if (negotiator.matchesFeature([mechanisms])) {
|
||||
_currentSaslNegotiator = negotiator;
|
||||
_currentSaslNegotiator!.pickForSasl2();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// We must have a SASL negotiator by now
|
||||
if (_currentSaslNegotiator == null) {
|
||||
return Result(NoSASLMechanismSelectedError());
|
||||
}
|
||||
|
||||
// Collect additional data by interested negotiators
|
||||
final inline = _sasl2Data!.firstTag('inline');
|
||||
final children = List<XMLNode>.empty(growable: true);
|
||||
if (inline != null && inline.children.isNotEmpty) {
|
||||
for (final negotiator in _featureNegotiators) {
|
||||
if (negotiator.canInlineFeature(inline.children)) {
|
||||
_activeSasl2Negotiators.add(negotiator.id);
|
||||
children.addAll(
|
||||
await negotiator.onSasl2FeaturesReceived(_sasl2Data!),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build the authenticate nonza
|
||||
final authenticate = XMLNode.xmlns(
|
||||
tag: 'authenticate',
|
||||
xmlns: sasl2Xmlns,
|
||||
attributes: {
|
||||
'mechanism': _currentSaslNegotiator!.mechanismName,
|
||||
},
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'initial-response',
|
||||
text: await _currentSaslNegotiator!.getRawStep(''),
|
||||
),
|
||||
if (userAgent != null) userAgent!.toXml(),
|
||||
...children,
|
||||
],
|
||||
);
|
||||
|
||||
_sasl2State = Sasl2State.authenticateSent;
|
||||
attributes.sendNonza(authenticate);
|
||||
return const Result(NegotiatorState.ready);
|
||||
case Sasl2State.authenticateSent:
|
||||
if (nonza.tag == 'success') {
|
||||
// Tell the dependent negotiators about the result
|
||||
final negotiators = _featureNegotiators
|
||||
.where(
|
||||
(negotiator) => _activeSasl2Negotiators.contains(negotiator.id),
|
||||
)
|
||||
.toList()
|
||||
..add(_currentSaslNegotiator!);
|
||||
for (final negotiator in negotiators) {
|
||||
final result = await negotiator.onSasl2Success(nonza);
|
||||
if (!result.isType<bool>()) {
|
||||
return Result(result.get<NegotiatorError>());
|
||||
}
|
||||
}
|
||||
|
||||
// We're done
|
||||
attributes.setAuthenticated();
|
||||
attributes.removeNegotiatingFeature(saslXmlns);
|
||||
|
||||
// Check if we also received a resource with the SASL2 success
|
||||
final jid = JID.fromString(
|
||||
nonza.firstTag('authorization-identifier')!.innerText(),
|
||||
);
|
||||
if (!jid.isBare()) {
|
||||
attributes.setResource(jid.resource);
|
||||
}
|
||||
|
||||
return const Result(NegotiatorState.done);
|
||||
} else if (nonza.tag == 'challenge') {
|
||||
// We still have to negotiate
|
||||
final challenge = nonza.innerText();
|
||||
final response = XMLNode.xmlns(
|
||||
tag: 'response',
|
||||
xmlns: sasl2Xmlns,
|
||||
text: await _currentSaslNegotiator!.getRawStep(challenge),
|
||||
);
|
||||
attributes.sendNonza(response);
|
||||
} else if (nonza.tag == 'failure') {
|
||||
final negotiators = _featureNegotiators
|
||||
.where(
|
||||
(negotiator) => _activeSasl2Negotiators.contains(negotiator.id),
|
||||
)
|
||||
.toList()
|
||||
..add(_currentSaslNegotiator!);
|
||||
for (final negotiator in negotiators) {
|
||||
await negotiator.onSasl2Failure(nonza);
|
||||
}
|
||||
|
||||
// Check if we should retry and, if we should, reset the current
|
||||
// negotiator, this negotiator, and retry.
|
||||
if (_currentSaslNegotiator!.shouldRetrySasl()) {
|
||||
_currentSaslNegotiator!.reset();
|
||||
reset();
|
||||
return const Result(
|
||||
NegotiatorState.retryLater,
|
||||
);
|
||||
}
|
||||
|
||||
return Result(
|
||||
SaslError.fromFailure(nonza),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return const Result(NegotiatorState.ready);
|
||||
}
|
||||
|
||||
@override
|
||||
void reset() {
|
||||
_currentSaslNegotiator = null;
|
||||
_sasl2State = Sasl2State.idle;
|
||||
_sasl2Data = null;
|
||||
_activeSasl2Negotiators.clear();
|
||||
|
||||
super.reset();
|
||||
}
|
||||
}
|
@ -56,7 +56,6 @@ class TestingManagerHolder {
|
||||
sendNonza: (_) {},
|
||||
sendEvent: (_) {},
|
||||
getSocket: () => _socket,
|
||||
isFeatureSupported: (_) => false,
|
||||
getNegotiatorById: getNegotiatorNullStub,
|
||||
getFullJID: () => jid,
|
||||
getManagerById: _getManagerById,
|
||||
|
@ -58,7 +58,7 @@ List<ExpectationBase> buildAuthenticatedPlay(ConnectionSettings settings) {
|
||||
);
|
||||
return [
|
||||
StringExpectation(
|
||||
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='${settings.jid.domain}' xml:lang='en'>",
|
||||
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='${settings.jid.domain}' from='${settings.jid.toBare().toString()}' xml:lang='en'>",
|
||||
'''
|
||||
<stream:stream
|
||||
xmlns="jabber:client"
|
||||
@ -77,7 +77,7 @@ List<ExpectationBase> buildAuthenticatedPlay(ConnectionSettings settings) {
|
||||
'<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='${settings.jid.domain}' xml:lang='en'>",
|
||||
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='${settings.jid.domain}' from='${settings.jid.toBare().toString()}' xml:lang='en'>",
|
||||
'''
|
||||
<stream:stream
|
||||
xmlns="jabber:client"
|
||||
|
@ -46,7 +46,7 @@ void main() {
|
||||
final stubSocket = StubTCPSocket(
|
||||
[
|
||||
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' to='test.server' from='user@test.server' xml:lang='en'>",
|
||||
'''
|
||||
<stream:stream
|
||||
xmlns="jabber:client"
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:moxxmpp/src/negotiators/sasl/kv.dart';
|
||||
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/kv.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
|
@ -46,6 +46,11 @@ void main() {
|
||||
)..register(
|
||||
NegotiatorAttributes(
|
||||
(XMLNode _, {String? redact}) {},
|
||||
() => XmppConnection(
|
||||
TestingReconnectionPolicy(),
|
||||
AlwaysConnectedConnectivityManager(),
|
||||
fakeSocket,
|
||||
),
|
||||
() => ConnectionSettings(
|
||||
jid: JID.fromString('user@server'),
|
||||
password: 'pencil',
|
||||
@ -57,6 +62,9 @@ void main() {
|
||||
() => JID.fromString('user@server'),
|
||||
() => fakeSocket,
|
||||
() => false,
|
||||
() {},
|
||||
(_, {bool triggerEvent = true}) {},
|
||||
(_) {},
|
||||
),
|
||||
);
|
||||
|
||||
@ -139,6 +147,11 @@ void main() {
|
||||
)..register(
|
||||
NegotiatorAttributes(
|
||||
(XMLNode n, {String? redact}) => lastMessage = n.innerText(),
|
||||
() => XmppConnection(
|
||||
TestingReconnectionPolicy(),
|
||||
AlwaysConnectedConnectivityManager(),
|
||||
StubTCPSocket([]),
|
||||
),
|
||||
() => ConnectionSettings(
|
||||
jid: JID.fromString('user@server'),
|
||||
password: 'pencil',
|
||||
@ -150,6 +163,9 @@ void main() {
|
||||
() => JID.fromString('user@server'),
|
||||
() => fakeSocket,
|
||||
() => false,
|
||||
() {},
|
||||
(_, {bool triggerEvent = true}) {},
|
||||
(_) {},
|
||||
),
|
||||
);
|
||||
|
||||
@ -187,6 +203,11 @@ void main() {
|
||||
)..register(
|
||||
NegotiatorAttributes(
|
||||
(XMLNode _, {String? redact}) {},
|
||||
() => XmppConnection(
|
||||
TestingReconnectionPolicy(),
|
||||
AlwaysConnectedConnectivityManager(),
|
||||
StubTCPSocket([]),
|
||||
),
|
||||
() => ConnectionSettings(
|
||||
jid: JID.fromString('user@server'),
|
||||
password: 'pencil',
|
||||
@ -198,6 +219,9 @@ void main() {
|
||||
() => JID.fromString('user@server'),
|
||||
() => fakeSocket,
|
||||
() => false,
|
||||
() {},
|
||||
(_, {bool triggerEvent = true}) {},
|
||||
(_) {},
|
||||
),
|
||||
);
|
||||
|
||||
@ -225,6 +249,11 @@ void main() {
|
||||
)..register(
|
||||
NegotiatorAttributes(
|
||||
(XMLNode _, {String? redact}) {},
|
||||
() => XmppConnection(
|
||||
TestingReconnectionPolicy(),
|
||||
AlwaysConnectedConnectivityManager(),
|
||||
StubTCPSocket([]),
|
||||
),
|
||||
() => ConnectionSettings(
|
||||
jid: JID.fromString('user@server'),
|
||||
password: 'pencil',
|
||||
@ -236,6 +265,9 @@ void main() {
|
||||
() => JID.fromString('user@server'),
|
||||
() => fakeSocket,
|
||||
() => false,
|
||||
() {},
|
||||
(_, {bool triggerEvent = true}) {},
|
||||
(_) {},
|
||||
),
|
||||
);
|
||||
|
||||
@ -266,6 +298,11 @@ void main() {
|
||||
)..register(
|
||||
NegotiatorAttributes(
|
||||
(XMLNode _, {String? redact}) {},
|
||||
() => XmppConnection(
|
||||
TestingReconnectionPolicy(),
|
||||
AlwaysConnectedConnectivityManager(),
|
||||
StubTCPSocket([]),
|
||||
),
|
||||
() => ConnectionSettings(
|
||||
jid: JID.fromString('user@server'),
|
||||
password: 'pencil',
|
||||
@ -277,6 +314,9 @@ void main() {
|
||||
() => JID.fromString('user@server'),
|
||||
() => fakeSocket,
|
||||
() => false,
|
||||
() {},
|
||||
(_, {bool triggerEvent = true}) {},
|
||||
(_) {},
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -23,8 +23,8 @@ void main() {
|
||||
);
|
||||
|
||||
expect(
|
||||
StreamHeaderNonza('uwu.server').toXml(),
|
||||
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='uwu.server' xml:lang='en'>",
|
||||
StreamHeaderNonza(JID.fromString('user@uwu.server')).toXml(),
|
||||
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='uwu.server' from='user@uwu.server' xml:lang='en'>",
|
||||
);
|
||||
|
||||
expect(
|
||||
|
@ -12,7 +12,7 @@ void main() {
|
||||
final fakeSocket = StubTCPSocket(
|
||||
[
|
||||
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' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
|
||||
'''
|
||||
<stream:stream
|
||||
xmlns="jabber:client"
|
||||
@ -31,7 +31,7 @@ void main() {
|
||||
'<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' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
|
||||
'''
|
||||
<stream:stream
|
||||
xmlns="jabber:client"
|
||||
@ -84,7 +84,7 @@ void main() {
|
||||
DiscoManager([]),
|
||||
EntityCapabilitiesManager('http://moxxmpp.example'),
|
||||
]);
|
||||
conn.registerFeatureNegotiators([
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
SaslScramNegotiator(10, '', '', ScramHashType.sha512),
|
||||
ResourceBindingNegotiator(),
|
||||
|
@ -169,12 +169,11 @@ void main() {
|
||||
MessageManager(),
|
||||
RosterManager(TestingRosterStateManager(null, [])),
|
||||
]);
|
||||
connection
|
||||
..registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
ResourceBindingNegotiator(),
|
||||
])
|
||||
..setConnectionSettings(TestingManagerHolder.settings);
|
||||
await connection.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
ResourceBindingNegotiator(),
|
||||
]);
|
||||
connection.setConnectionSettings(TestingManagerHolder.settings);
|
||||
await connection.connect(
|
||||
waitUntilLogin: true,
|
||||
);
|
||||
|
@ -63,7 +63,6 @@ XmppManagerAttributes mkAttributes(void Function(Stanza) callback) {
|
||||
password: 'password',
|
||||
useDirectTLS: true,
|
||||
),
|
||||
isFeatureSupported: (_) => false,
|
||||
getFullJID: () => JID.fromString('hallo@example.server/uwu'),
|
||||
getSocket: () => StubTCPSocket([]),
|
||||
getConnection: () => XmppConnection(
|
||||
@ -228,7 +227,7 @@ void main() {
|
||||
() async {
|
||||
final fakeSocket = StubTCPSocket([
|
||||
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' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
|
||||
'''
|
||||
<stream:stream
|
||||
xmlns="jabber:client"
|
||||
@ -247,7 +246,7 @@ void main() {
|
||||
'<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' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
|
||||
'''
|
||||
<stream:stream
|
||||
xmlns="jabber:client"
|
||||
@ -298,7 +297,7 @@ void main() {
|
||||
CarbonsManager()..forceEnable(),
|
||||
EntityCapabilitiesManager('http://moxxmpp.example'),
|
||||
]);
|
||||
conn.registerFeatureNegotiators([
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
ResourceBindingNegotiator(),
|
||||
StreamManagementNegotiator(),
|
||||
@ -343,7 +342,7 @@ void main() {
|
||||
test('Test counting incoming stanzas that are awaited', () async {
|
||||
final fakeSocket = StubTCPSocket([
|
||||
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' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
|
||||
'''
|
||||
<stream:stream
|
||||
xmlns="jabber:client"
|
||||
@ -362,7 +361,7 @@ void main() {
|
||||
'<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' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
|
||||
'''
|
||||
<stream:stream
|
||||
xmlns="jabber:client"
|
||||
@ -391,10 +390,10 @@ void main() {
|
||||
"<enable xmlns='urn:xmpp:sm:3' resume='true' />",
|
||||
'<enabled xmlns="urn:xmpp:sm:3" id="some-long-sm-id" resume="true" />',
|
||||
),
|
||||
// StringExpectation(
|
||||
// "<presence xmlns='jabber:client' from='polynomdivision@test.server/MU29eEZn'><show>chat</show></presence>",
|
||||
// '<iq type="result" />',
|
||||
// ),
|
||||
StringExpectation(
|
||||
"<presence xmlns='jabber:client' from='polynomdivision@test.server/MU29eEZn'><show>chat</show></presence>",
|
||||
'<iq type="result" />',
|
||||
),
|
||||
StanzaExpectation(
|
||||
"<iq to='user@example.com' type='get' id='a' xmlns='jabber:client' />",
|
||||
"<iq from='user@example.com' type='result' id='a' />",
|
||||
@ -423,7 +422,7 @@ void main() {
|
||||
CarbonsManager()..forceEnable(),
|
||||
//EntityCapabilitiesManager('http://moxxmpp.example'),
|
||||
]);
|
||||
conn.registerFeatureNegotiators([
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
ResourceBindingNegotiator(),
|
||||
StreamManagementNegotiator(),
|
||||
@ -432,7 +431,7 @@ void main() {
|
||||
await conn.connect(
|
||||
waitUntilLogin: true,
|
||||
);
|
||||
expect(fakeSocket.getState(), 5);
|
||||
expect(fakeSocket.getState(), 6);
|
||||
expect(await conn.getConnectionState(), XmppConnectionState.connected);
|
||||
expect(
|
||||
conn
|
||||
@ -450,7 +449,7 @@ void main() {
|
||||
addFrom: StanzaFromType.none,
|
||||
);
|
||||
|
||||
expect(sm.state.s2c, /*2*/ 1);
|
||||
expect(sm.state.s2c, 2);
|
||||
});
|
||||
});
|
||||
|
||||
@ -513,7 +512,7 @@ void main() {
|
||||
test('Test successful stream enablement', () async {
|
||||
final fakeSocket = StubTCPSocket([
|
||||
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' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
|
||||
'''
|
||||
<stream:stream
|
||||
xmlns="jabber:client"
|
||||
@ -532,7 +531,7 @@ void main() {
|
||||
'<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' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
|
||||
'''
|
||||
<stream:stream
|
||||
xmlns="jabber:client"
|
||||
@ -580,7 +579,7 @@ void main() {
|
||||
DiscoManager([]),
|
||||
StreamManagementManager(),
|
||||
]);
|
||||
conn.registerFeatureNegotiators([
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
ResourceBindingNegotiator(),
|
||||
StreamManagementNegotiator(),
|
||||
@ -590,7 +589,7 @@ void main() {
|
||||
waitUntilLogin: true,
|
||||
);
|
||||
|
||||
expect(fakeSocket.getState(), 5);
|
||||
expect(fakeSocket.getState(), 6);
|
||||
expect(await conn.getConnectionState(), XmppConnectionState.connected);
|
||||
expect(
|
||||
conn
|
||||
@ -603,7 +602,7 @@ void main() {
|
||||
test('Test a failed stream resumption', () async {
|
||||
final fakeSocket = StubTCPSocket([
|
||||
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' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
|
||||
'''
|
||||
<stream:stream
|
||||
xmlns="jabber:client"
|
||||
@ -622,7 +621,7 @@ void main() {
|
||||
'<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' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
|
||||
'''
|
||||
<stream:stream
|
||||
xmlns="jabber:client"
|
||||
@ -674,7 +673,7 @@ void main() {
|
||||
DiscoManager([]),
|
||||
StreamManagementManager(),
|
||||
]);
|
||||
conn.registerFeatureNegotiators([
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
ResourceBindingNegotiator(),
|
||||
StreamManagementNegotiator(),
|
||||
@ -690,7 +689,7 @@ void main() {
|
||||
await conn.connect(
|
||||
waitUntilLogin: true,
|
||||
);
|
||||
expect(fakeSocket.getState(), 6);
|
||||
expect(fakeSocket.getState(), 7);
|
||||
expect(await conn.getConnectionState(), XmppConnectionState.connected);
|
||||
expect(
|
||||
conn
|
||||
@ -703,7 +702,7 @@ void main() {
|
||||
test('Test a successful stream resumption', () async {
|
||||
final fakeSocket = StubTCPSocket([
|
||||
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' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
|
||||
'''
|
||||
<stream:stream
|
||||
xmlns="jabber:client"
|
||||
@ -722,7 +721,7 @@ void main() {
|
||||
'<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' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
|
||||
'''
|
||||
<stream:stream
|
||||
xmlns="jabber:client"
|
||||
@ -765,7 +764,7 @@ void main() {
|
||||
DiscoManager([]),
|
||||
StreamManagementManager(),
|
||||
]);
|
||||
conn.registerFeatureNegotiators([
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
ResourceBindingNegotiator(),
|
||||
StreamManagementNegotiator(),
|
||||
@ -789,4 +788,298 @@ void main() {
|
||||
expect(sm.streamResumed, true);
|
||||
});
|
||||
});
|
||||
|
||||
test('Test SASL2 inline stream resumption', () async {
|
||||
final fakeSocket = StubTCPSocket([
|
||||
StringExpectation(
|
||||
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@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>
|
||||
<authentication xmlns='urn:xmpp:sasl:2'>
|
||||
<mechanism>PLAIN</mechanism>
|
||||
<inline>
|
||||
<resume xmlns="urn:xmpp:sm:3" />
|
||||
</inline>
|
||||
</authentication>
|
||||
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
|
||||
<required/>
|
||||
</bind>
|
||||
</stream:features>''',
|
||||
),
|
||||
StanzaExpectation(
|
||||
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='PLAIN'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>AHBvbHlub21kaXZpc2lvbgBhYWFh</initial-response><resume xmlns='urn:xmpp:sm:3' previd='test-prev-id' h='2' /></authenticate>",
|
||||
'''
|
||||
<success xmlns='urn:xmpp:sasl:2'>
|
||||
<authorization-identifier>polynomdivision@test.server</authorization-identifier>
|
||||
<resumed xmlns='urn:xmpp:sm:3' h='25' previd='test-prev-id' />
|
||||
</success>
|
||||
''',
|
||||
),
|
||||
]);
|
||||
final sm = StreamManagementManager();
|
||||
await sm.setState(
|
||||
sm.state.copyWith(
|
||||
c2s: 25,
|
||||
s2c: 2,
|
||||
streamResumptionId: 'test-prev-id',
|
||||
),
|
||||
);
|
||||
|
||||
final conn = XmppConnection(
|
||||
TestingReconnectionPolicy(),
|
||||
AlwaysConnectedConnectivityManager(),
|
||||
fakeSocket,
|
||||
)
|
||||
..setConnectionSettings(
|
||||
ConnectionSettings(
|
||||
jid: JID.fromString('polynomdivision@test.server'),
|
||||
password: 'aaaa',
|
||||
useDirectTLS: true,
|
||||
),
|
||||
)
|
||||
..setResource('test-resource', triggerEvent: false);
|
||||
await conn.registerManagers([
|
||||
RosterManager(TestingRosterStateManager('', [])),
|
||||
DiscoManager([]),
|
||||
sm,
|
||||
]);
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
ResourceBindingNegotiator(),
|
||||
StreamManagementNegotiator()..setResource('test-resource'),
|
||||
Sasl2Negotiator(
|
||||
userAgent: const UserAgent(
|
||||
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
|
||||
software: 'moxxmpp',
|
||||
device: "PapaTutuWawa's awesome device",
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
final result = await conn.connect(
|
||||
waitUntilLogin: true,
|
||||
shouldReconnect: false,
|
||||
enableReconnectOnSuccess: false,
|
||||
);
|
||||
expect(result.isType<NegotiatorError>(), false);
|
||||
|
||||
expect(
|
||||
sm.state.c2s,
|
||||
25,
|
||||
);
|
||||
expect(
|
||||
sm.state.s2c,
|
||||
2,
|
||||
);
|
||||
expect(conn.resource, 'test-resource');
|
||||
});
|
||||
|
||||
test('Test SASL2 inline stream resumption with Bind2', () async {
|
||||
final fakeSocket = StubTCPSocket([
|
||||
StringExpectation(
|
||||
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@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>
|
||||
<authentication xmlns='urn:xmpp:sasl:2'>
|
||||
<mechanism>PLAIN</mechanism>
|
||||
<inline>
|
||||
<resume xmlns="urn:xmpp:sm:3" />
|
||||
<bind xmlns="urn:xmpp:bind:0">
|
||||
<inline>
|
||||
<feature var="urn:xmpp:sm:3" />
|
||||
</inline>
|
||||
</bind>
|
||||
</inline>
|
||||
</authentication>
|
||||
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
|
||||
<required/>
|
||||
</bind>
|
||||
</stream:features>''',
|
||||
),
|
||||
StanzaExpectation(
|
||||
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='PLAIN'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>AHBvbHlub21kaXZpc2lvbgBhYWFh</initial-response><bind xmlns='urn:xmpp:bind:0'><enable xmlns='urn:xmpp:sm:3' resume='true' /></bind><resume xmlns='urn:xmpp:sm:3' previd='test-prev-id' h='2' /></authenticate>",
|
||||
'''
|
||||
<success xmlns='urn:xmpp:sasl:2'>
|
||||
<authorization-identifier>polynomdivision@test.server</authorization-identifier>
|
||||
<resumed xmlns='urn:xmpp:sm:3' h='25' previd='test-prev-id' />
|
||||
</success>
|
||||
''',
|
||||
),
|
||||
]);
|
||||
final sm = StreamManagementManager();
|
||||
await sm.setState(
|
||||
sm.state.copyWith(
|
||||
c2s: 25,
|
||||
s2c: 2,
|
||||
streamResumptionId: 'test-prev-id',
|
||||
),
|
||||
);
|
||||
|
||||
final conn = XmppConnection(
|
||||
TestingReconnectionPolicy(),
|
||||
AlwaysConnectedConnectivityManager(),
|
||||
fakeSocket,
|
||||
)
|
||||
..setConnectionSettings(
|
||||
ConnectionSettings(
|
||||
jid: JID.fromString('polynomdivision@test.server'),
|
||||
password: 'aaaa',
|
||||
useDirectTLS: true,
|
||||
),
|
||||
)
|
||||
..setResource('test-resource', triggerEvent: false);
|
||||
await conn.registerManagers([
|
||||
RosterManager(TestingRosterStateManager('', [])),
|
||||
DiscoManager([]),
|
||||
sm,
|
||||
]);
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
ResourceBindingNegotiator(),
|
||||
StreamManagementNegotiator()..setResource('test-resource'),
|
||||
Bind2Negotiator(),
|
||||
Sasl2Negotiator(
|
||||
userAgent: const UserAgent(
|
||||
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
|
||||
software: 'moxxmpp',
|
||||
device: "PapaTutuWawa's awesome device",
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
final result = await conn.connect(
|
||||
waitUntilLogin: true,
|
||||
shouldReconnect: false,
|
||||
enableReconnectOnSuccess: false,
|
||||
);
|
||||
expect(result.isType<NegotiatorError>(), false);
|
||||
|
||||
expect(
|
||||
sm.state.c2s,
|
||||
25,
|
||||
);
|
||||
expect(
|
||||
sm.state.s2c,
|
||||
2,
|
||||
);
|
||||
expect(conn.resource, 'test-resource');
|
||||
});
|
||||
|
||||
test('Test failed SASL2 inline stream resumption with Bind2', () async {
|
||||
final fakeSocket = StubTCPSocket([
|
||||
StringExpectation(
|
||||
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@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>
|
||||
<authentication xmlns='urn:xmpp:sasl:2'>
|
||||
<mechanism>PLAIN</mechanism>
|
||||
<inline>
|
||||
<resume xmlns="urn:xmpp:sm:3" />
|
||||
<bind xmlns="urn:xmpp:bind:0">
|
||||
<inline>
|
||||
<feature var="urn:xmpp:sm:3" />
|
||||
</inline>
|
||||
</bind>
|
||||
</inline>
|
||||
</authentication>
|
||||
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
|
||||
<required/>
|
||||
</bind>
|
||||
</stream:features>''',
|
||||
),
|
||||
StanzaExpectation(
|
||||
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='PLAIN'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>AHBvbHlub21kaXZpc2lvbgBhYWFh</initial-response><bind xmlns='urn:xmpp:bind:0'><enable xmlns='urn:xmpp:sm:3' resume='true' /></bind><resume xmlns='urn:xmpp:sm:3' previd='test-prev-id' h='2' /></authenticate>",
|
||||
'''
|
||||
<success xmlns='urn:xmpp:sasl:2'>
|
||||
<authorization-identifier>polynomdivision@test.server/test-resource</authorization-identifier>
|
||||
<failed xmlns='urn:xmpp:sm:3' />
|
||||
<bound xmlns='urn:xmpp:bind:0'>
|
||||
<failed xmlns='urn:xmpp:sm:3' />
|
||||
</bound>
|
||||
</success>
|
||||
''',
|
||||
),
|
||||
]);
|
||||
final sm = StreamManagementManager();
|
||||
await sm.setState(
|
||||
sm.state.copyWith(
|
||||
c2s: 25,
|
||||
s2c: 2,
|
||||
streamResumptionId: 'test-prev-id',
|
||||
),
|
||||
);
|
||||
|
||||
final conn = XmppConnection(
|
||||
TestingReconnectionPolicy(),
|
||||
AlwaysConnectedConnectivityManager(),
|
||||
fakeSocket,
|
||||
)
|
||||
..setConnectionSettings(
|
||||
ConnectionSettings(
|
||||
jid: JID.fromString('polynomdivision@test.server'),
|
||||
password: 'aaaa',
|
||||
useDirectTLS: true,
|
||||
),
|
||||
)
|
||||
..setResource('test-resource', triggerEvent: false);
|
||||
await conn.registerManagers([
|
||||
RosterManager(TestingRosterStateManager('', [])),
|
||||
DiscoManager([]),
|
||||
sm,
|
||||
]);
|
||||
|
||||
final smn = StreamManagementNegotiator();
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
ResourceBindingNegotiator(),
|
||||
smn,
|
||||
Bind2Negotiator(),
|
||||
Sasl2Negotiator(
|
||||
userAgent: const UserAgent(
|
||||
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
|
||||
software: 'moxxmpp',
|
||||
device: "PapaTutuWawa's awesome device",
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
final result = await conn.connect(
|
||||
waitUntilLogin: true,
|
||||
shouldReconnect: false,
|
||||
enableReconnectOnSuccess: false,
|
||||
);
|
||||
expect(result.isType<NegotiatorError>(), false);
|
||||
|
||||
expect(smn.isResumed, false);
|
||||
expect(smn.resumeFailed, true);
|
||||
expect(smn.streamEnablementFailed, true);
|
||||
expect(conn.resource, 'test-resource');
|
||||
});
|
||||
}
|
||||
|
@ -1,8 +1,11 @@
|
||||
import 'package:moxxmpp/moxxmpp.dart';
|
||||
import 'package:test/test.dart';
|
||||
import '../helpers/logging.dart';
|
||||
import '../helpers/xmpp.dart';
|
||||
|
||||
void main() {
|
||||
initLogger();
|
||||
|
||||
test("Test if we're vulnerable against CVE-2020-26547 style vulnerabilities",
|
||||
() async {
|
||||
final attributes = XmppManagerAttributes(
|
||||
@ -27,7 +30,6 @@ void main() {
|
||||
password: 'password',
|
||||
useDirectTLS: true,
|
||||
),
|
||||
isFeatureSupported: (_) => false,
|
||||
getFullJID: () => JID.fromString('bob@xmpp.example/uwu'),
|
||||
getSocket: () => StubTCPSocket([]),
|
||||
getConnection: () => XmppConnection(
|
||||
@ -53,4 +55,94 @@ void main() {
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('Test enabling message carbons inline with Bind2', () async {
|
||||
final fakeSocket = StubTCPSocket([
|
||||
StringExpectation(
|
||||
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@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>
|
||||
<authentication xmlns='urn:xmpp:sasl:2'>
|
||||
<mechanism>PLAIN</mechanism>
|
||||
<inline>
|
||||
<resume xmlns="urn:xmpp:sm:3" />
|
||||
<bind xmlns="urn:xmpp:bind:0">
|
||||
<inline>
|
||||
<feature var="urn:xmpp:sm:3" />
|
||||
<feature var="urn:xmpp:carbons:2" />
|
||||
</inline>
|
||||
</bind>
|
||||
</inline>
|
||||
</authentication>
|
||||
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
|
||||
<required/>
|
||||
</bind>
|
||||
</stream:features>''',
|
||||
),
|
||||
StanzaExpectation(
|
||||
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='PLAIN'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>AHBvbHlub21kaXZpc2lvbgBhYWFh</initial-response><bind xmlns='urn:xmpp:bind:0'><enable xmlns='urn:xmpp:carbons:2' /></bind></authenticate>",
|
||||
'''
|
||||
<success xmlns='urn:xmpp:sasl:2'>
|
||||
<authorization-identifier>polynomdivision@test.server/test-resource</authorization-identifier>
|
||||
<bound xmlns='urn:xmpp:bind:0'>
|
||||
<enabled xmlns='urn:xmpp:carbons:2' />
|
||||
</bound>
|
||||
</success>
|
||||
''',
|
||||
),
|
||||
]);
|
||||
final conn = XmppConnection(
|
||||
TestingReconnectionPolicy(),
|
||||
AlwaysConnectedConnectivityManager(),
|
||||
fakeSocket,
|
||||
)
|
||||
..setConnectionSettings(
|
||||
ConnectionSettings(
|
||||
jid: JID.fromString('polynomdivision@test.server'),
|
||||
password: 'aaaa',
|
||||
useDirectTLS: true,
|
||||
),
|
||||
)
|
||||
..setResource('test-resource', triggerEvent: false);
|
||||
await conn.registerManagers([
|
||||
RosterManager(TestingRosterStateManager('', [])),
|
||||
DiscoManager([]),
|
||||
CarbonsManager(),
|
||||
]);
|
||||
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
ResourceBindingNegotiator(),
|
||||
CarbonsNegotiator(),
|
||||
Bind2Negotiator(),
|
||||
Sasl2Negotiator(
|
||||
userAgent: const UserAgent(
|
||||
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
|
||||
software: 'moxxmpp',
|
||||
device: "PapaTutuWawa's awesome device",
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
final result = await conn.connect(
|
||||
waitUntilLogin: true,
|
||||
shouldReconnect: false,
|
||||
enableReconnectOnSuccess: false,
|
||||
);
|
||||
expect(result.isType<NegotiatorError>(), false);
|
||||
expect(conn.resource, 'test-resource');
|
||||
expect(
|
||||
conn.getManagerById<CarbonsManager>(carbonsManager)!.isEnabled,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:moxxmpp/moxxmpp.dart';
|
||||
import 'package:test/test.dart';
|
||||
import '../helpers/logging.dart';
|
||||
import '../helpers/xmpp.dart';
|
||||
|
||||
class MockedCSINegotiator extends CSINegotiator {
|
||||
@ -28,6 +29,8 @@ T? getUnsupportedCSINegotiator<T extends XmppFeatureNegotiatorBase>(String id) {
|
||||
}
|
||||
|
||||
void main() {
|
||||
initLogger();
|
||||
|
||||
group('Test the XEP-0352 implementation', () {
|
||||
test('Test setting the CSI state when CSI is unsupported', () {
|
||||
var nonzaSent = false;
|
||||
@ -55,7 +58,6 @@ void main() {
|
||||
),
|
||||
getManagerById: getManagerNullStub,
|
||||
getNegotiatorById: getUnsupportedCSINegotiator,
|
||||
isFeatureSupported: (_) => false,
|
||||
getFullJID: () => JID.fromString('some.user@example.server/aaaaa'),
|
||||
getSocket: () => StubTCPSocket([]),
|
||||
getConnection: () => XmppConnection(
|
||||
@ -99,7 +101,6 @@ void main() {
|
||||
),
|
||||
getManagerById: getManagerNullStub,
|
||||
getNegotiatorById: getSupportedCSINegotiator,
|
||||
isFeatureSupported: (_) => false,
|
||||
getFullJID: () => JID.fromString('some.user@example.server/aaaaa'),
|
||||
getSocket: () => StubTCPSocket([]),
|
||||
getConnection: () => XmppConnection(
|
||||
@ -113,4 +114,99 @@ void main() {
|
||||
..setInactive();
|
||||
});
|
||||
});
|
||||
|
||||
test('Test CSI with Bind2', () async {
|
||||
final fakeSocket = StubTCPSocket([
|
||||
StringExpectation(
|
||||
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@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>
|
||||
<authentication xmlns='urn:xmpp:sasl:2'>
|
||||
<mechanism>PLAIN</mechanism>
|
||||
<inline>
|
||||
<bind xmlns="urn:xmpp:bind:0">
|
||||
<inline>
|
||||
<feature var="urn:xmpp:csi:0" />
|
||||
</inline>
|
||||
</bind>
|
||||
</inline>
|
||||
</authentication>
|
||||
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
|
||||
<required/>
|
||||
</bind>
|
||||
</stream:features>''',
|
||||
),
|
||||
StanzaExpectation(
|
||||
'''
|
||||
<authenticate xmlns='urn:xmpp:sasl:2' mechanism='PLAIN'>
|
||||
<user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'>
|
||||
<software>moxxmpp</software>
|
||||
<device>PapaTutuWawa's awesome device</device>
|
||||
</user-agent>
|
||||
<initial-response>AHBvbHlub21kaXZpc2lvbgBhYWFh</initial-response>
|
||||
<bind xmlns='urn:xmpp:bind:0'>
|
||||
<inactive xmlns='urn:xmpp:csi:0' />
|
||||
</bind>
|
||||
</authenticate>''',
|
||||
'''
|
||||
<success xmlns='urn:xmpp:sasl:2'>
|
||||
<authorization-identifier>polynomdivision@test.server/test-resource</authorization-identifier>
|
||||
</success>
|
||||
''',
|
||||
),
|
||||
]);
|
||||
final conn = XmppConnection(
|
||||
TestingReconnectionPolicy(),
|
||||
AlwaysConnectedConnectivityManager(),
|
||||
fakeSocket,
|
||||
)..setConnectionSettings(
|
||||
ConnectionSettings(
|
||||
jid: JID.fromString('polynomdivision@test.server'),
|
||||
password: 'aaaa',
|
||||
useDirectTLS: true,
|
||||
),
|
||||
);
|
||||
final csi = CSIManager();
|
||||
await csi.setInactive(sendNonza: false);
|
||||
await conn.registerManagers([
|
||||
RosterManager(TestingRosterStateManager('', [])),
|
||||
DiscoManager([]),
|
||||
csi,
|
||||
]);
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
ResourceBindingNegotiator(),
|
||||
FASTSaslNegotiator(),
|
||||
Bind2Negotiator(),
|
||||
CSINegotiator(),
|
||||
Sasl2Negotiator(
|
||||
userAgent: const UserAgent(
|
||||
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
|
||||
software: 'moxxmpp',
|
||||
device: "PapaTutuWawa's awesome device",
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
final result = await conn.connect(
|
||||
waitUntilLogin: true,
|
||||
shouldReconnect: false,
|
||||
enableReconnectOnSuccess: false,
|
||||
);
|
||||
expect(result.isType<NegotiatorError>(), false);
|
||||
expect(fakeSocket.getState(), 2);
|
||||
expect(
|
||||
conn.getNegotiatorById<CSINegotiator>(csiNegotiator)!.isSupported,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
153
packages/moxxmpp/test/xeps/xep_0386_test.dart
Normal file
153
packages/moxxmpp/test/xeps/xep_0386_test.dart
Normal file
@ -0,0 +1,153 @@
|
||||
import 'package:moxxmpp/moxxmpp.dart';
|
||||
import 'package:test/test.dart';
|
||||
import '../helpers/logging.dart';
|
||||
import '../helpers/xmpp.dart';
|
||||
|
||||
void main() {
|
||||
initLogger();
|
||||
|
||||
test('Test simple Bind2 negotiation', () async {
|
||||
final fakeSocket = StubTCPSocket([
|
||||
StringExpectation(
|
||||
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@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>
|
||||
<authentication xmlns='urn:xmpp:sasl:2'>
|
||||
<mechanism>PLAIN</mechanism>
|
||||
<inline>
|
||||
<bind xmlns="urn:xmpp:bind:0" />
|
||||
</inline>
|
||||
</authentication>
|
||||
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
|
||||
<required/>
|
||||
</bind>
|
||||
</stream:features>''',
|
||||
),
|
||||
StanzaExpectation(
|
||||
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='PLAIN'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>AHBvbHlub21kaXZpc2lvbgBhYWFh</initial-response><bind xmlns='urn:xmpp:bind:0' /></authenticate>",
|
||||
'''
|
||||
<success xmlns='urn:xmpp:sasl:2'>
|
||||
<authorization-identifier>polynomdivision@test.server/random.resource</authorization-identifier>
|
||||
</success>
|
||||
''',
|
||||
),
|
||||
]);
|
||||
final conn = XmppConnection(
|
||||
TestingReconnectionPolicy(),
|
||||
AlwaysConnectedConnectivityManager(),
|
||||
fakeSocket,
|
||||
)..setConnectionSettings(
|
||||
ConnectionSettings(
|
||||
jid: JID.fromString('polynomdivision@test.server'),
|
||||
password: 'aaaa',
|
||||
useDirectTLS: true,
|
||||
),
|
||||
);
|
||||
await conn.registerManagers([
|
||||
PresenceManager(),
|
||||
RosterManager(TestingRosterStateManager('', [])),
|
||||
DiscoManager([]),
|
||||
]);
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
ResourceBindingNegotiator(),
|
||||
Bind2Negotiator(),
|
||||
Sasl2Negotiator(
|
||||
userAgent: const UserAgent(
|
||||
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
|
||||
software: 'moxxmpp',
|
||||
device: "PapaTutuWawa's awesome device",
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
final result = await conn.connect(
|
||||
waitUntilLogin: true,
|
||||
shouldReconnect: false,
|
||||
enableReconnectOnSuccess: false,
|
||||
);
|
||||
expect(result.isType<NegotiatorError>(), false);
|
||||
expect(conn.resource, 'random.resource');
|
||||
});
|
||||
|
||||
test('Test simple Bind2 negotiation with a provided tag', () async {
|
||||
final fakeSocket = StubTCPSocket([
|
||||
StringExpectation(
|
||||
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@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>
|
||||
<authentication xmlns='urn:xmpp:sasl:2'>
|
||||
<mechanism>PLAIN</mechanism>
|
||||
<inline>
|
||||
<bind xmlns="urn:xmpp:bind:0" />
|
||||
</inline>
|
||||
</authentication>
|
||||
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
|
||||
<required/>
|
||||
</bind>
|
||||
</stream:features>''',
|
||||
),
|
||||
StanzaExpectation(
|
||||
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='PLAIN'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>AHBvbHlub21kaXZpc2lvbgBhYWFh</initial-response><bind xmlns='urn:xmpp:bind:0'><tag>moxxmpp</tag></bind></authenticate>",
|
||||
'''
|
||||
<success xmlns='urn:xmpp:sasl:2'>
|
||||
<authorization-identifier>polynomdivision@test.server/moxxmpp.random.resource</authorization-identifier>
|
||||
</success>
|
||||
''',
|
||||
),
|
||||
]);
|
||||
final conn = XmppConnection(
|
||||
TestingReconnectionPolicy(),
|
||||
AlwaysConnectedConnectivityManager(),
|
||||
fakeSocket,
|
||||
)..setConnectionSettings(
|
||||
ConnectionSettings(
|
||||
jid: JID.fromString('polynomdivision@test.server'),
|
||||
password: 'aaaa',
|
||||
useDirectTLS: true,
|
||||
),
|
||||
);
|
||||
await conn.registerManagers([
|
||||
RosterManager(TestingRosterStateManager('', [])),
|
||||
DiscoManager([]),
|
||||
]);
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
ResourceBindingNegotiator(),
|
||||
Bind2Negotiator()..tag = 'moxxmpp',
|
||||
Sasl2Negotiator(
|
||||
userAgent: const UserAgent(
|
||||
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
|
||||
software: 'moxxmpp',
|
||||
device: "PapaTutuWawa's awesome device",
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
final result = await conn.connect(
|
||||
waitUntilLogin: true,
|
||||
shouldReconnect: false,
|
||||
enableReconnectOnSuccess: false,
|
||||
);
|
||||
expect(result.isType<NegotiatorError>(), false);
|
||||
expect(conn.resource, 'moxxmpp.random.resource');
|
||||
});
|
||||
}
|
473
packages/moxxmpp/test/xeps/xep_0388_test.dart
Normal file
473
packages/moxxmpp/test/xeps/xep_0388_test.dart
Normal file
@ -0,0 +1,473 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:moxxmpp/moxxmpp.dart';
|
||||
import 'package:test/test.dart';
|
||||
import '../helpers/logging.dart';
|
||||
import '../helpers/xmpp.dart';
|
||||
|
||||
class ExampleNegotiator extends Sasl2FeatureNegotiator {
|
||||
ExampleNegotiator()
|
||||
: super(0, false, 'invalid:example:dont:use', 'testNegotiator');
|
||||
|
||||
String? value;
|
||||
|
||||
@override
|
||||
Future<Result<NegotiatorState, NegotiatorError>> negotiate(
|
||||
XMLNode nonza,
|
||||
) async {
|
||||
return const Result(NegotiatorState.done);
|
||||
}
|
||||
|
||||
@override
|
||||
bool canInlineFeature(List<XMLNode> features) {
|
||||
return features.firstWhereOrNull(
|
||||
(child) => child.xmlns == 'invalid:example:dont:use',
|
||||
) !=
|
||||
null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> postRegisterCallback() async {
|
||||
attributes
|
||||
.getNegotiatorById<Sasl2Negotiator>(sasl2Negotiator)!
|
||||
.registerNegotiator(this);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<XMLNode>> onSasl2FeaturesReceived(XMLNode nonza) async {
|
||||
return [
|
||||
XMLNode.xmlns(
|
||||
tag: 'test-data-request',
|
||||
xmlns: 'invalid:example:dont:use',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<bool, NegotiatorError>> onSasl2Success(XMLNode nonza) async {
|
||||
final child =
|
||||
nonza.firstTag('test-data', xmlns: 'invalid:example:dont:use');
|
||||
if (child == null) {
|
||||
return const Result(true);
|
||||
}
|
||||
|
||||
value = child.innerText();
|
||||
return const Result(true);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
initLogger();
|
||||
|
||||
test('Test simple SASL2 negotiation', () async {
|
||||
final fakeSocket = StubTCPSocket([
|
||||
StringExpectation(
|
||||
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@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>
|
||||
<authentication xmlns='urn:xmpp:sasl:2'>
|
||||
<mechanism>PLAIN</mechanism>
|
||||
</authentication>
|
||||
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
|
||||
<required/>
|
||||
</bind>
|
||||
</stream:features>''',
|
||||
),
|
||||
StanzaExpectation(
|
||||
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='PLAIN'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>AHBvbHlub21kaXZpc2lvbgBhYWFh</initial-response></authenticate>",
|
||||
'''
|
||||
<success xmlns='urn:xmpp:sasl:2'>
|
||||
<authorization-identifier>polynomdivision@test.server</authorization-identifier>
|
||||
</success>
|
||||
''',
|
||||
),
|
||||
StanzaExpectation(
|
||||
"<iq xmlns='jabber:client' type='set' id='aaaa'><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>',
|
||||
''',
|
||||
adjustId: true,
|
||||
ignoreId: true,
|
||||
),
|
||||
]);
|
||||
final conn = XmppConnection(
|
||||
TestingReconnectionPolicy(),
|
||||
AlwaysConnectedConnectivityManager(),
|
||||
fakeSocket,
|
||||
)..setConnectionSettings(
|
||||
ConnectionSettings(
|
||||
jid: JID.fromString('polynomdivision@test.server'),
|
||||
password: 'aaaa',
|
||||
useDirectTLS: true,
|
||||
),
|
||||
);
|
||||
await conn.registerManagers([
|
||||
PresenceManager(),
|
||||
RosterManager(TestingRosterStateManager('', [])),
|
||||
DiscoManager([]),
|
||||
]);
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
ResourceBindingNegotiator(),
|
||||
Sasl2Negotiator(
|
||||
userAgent: const UserAgent(
|
||||
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
|
||||
software: 'moxxmpp',
|
||||
device: "PapaTutuWawa's awesome device",
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
final result = await conn.connect(
|
||||
waitUntilLogin: true,
|
||||
shouldReconnect: false,
|
||||
enableReconnectOnSuccess: false,
|
||||
);
|
||||
expect(result.isType<NegotiatorError>(), false);
|
||||
});
|
||||
|
||||
test('Test SCRAM-SHA-1 SASL2 negotiation with a valid signature', () async {
|
||||
final fakeSocket = StubTCPSocket([
|
||||
StringExpectation(
|
||||
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='server' from='user@server' xml:lang='en'>",
|
||||
'''
|
||||
<stream:stream
|
||||
xmlns="jabber:client"
|
||||
version="1.0"
|
||||
xmlns:stream="http://etherx.jabber.org/streams"
|
||||
from="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-256</mechanism>
|
||||
</mechanisms>
|
||||
<authentication xmlns='urn:xmpp:sasl:2'>
|
||||
<mechanism>PLAIN</mechanism>
|
||||
<mechanism>SCRAM-SHA-256</mechanism>
|
||||
</authentication>
|
||||
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
|
||||
<required/>
|
||||
</bind>
|
||||
</stream:features>''',
|
||||
),
|
||||
StanzaExpectation(
|
||||
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='SCRAM-SHA-256'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>biwsbj11c2VyLHI9ck9wck5HZndFYmVSV2diTkVrcU8=</initial-response></authenticate>",
|
||||
'''
|
||||
<challenge xmlns='urn:xmpp:sasl:2'>cj1yT3ByTkdmd0ViZVJXZ2JORWtxTyVodllEcFdVYTJSYVRDQWZ1eEZJbGopaE5sRiRrMCxzPVcyMlphSjBTTlk3c29Fc1VFamI2Z1E9PSxpPTQwOTY=</challenge>
|
||||
''',
|
||||
),
|
||||
StanzaExpectation(
|
||||
'<response xmlns="urn:xmpp:sasl:2">Yz1iaXdzLHI9ck9wck5HZndFYmVSV2diTkVrcU8laHZZRHBXVWEyUmFUQ0FmdXhGSWxqKWhObEYkazAscD1kSHpiWmFwV0lrNGpVaE4rVXRlOXl0YWc5empmTUhnc3FtbWl6N0FuZFZRPQ==</response>',
|
||||
'<success xmlns="urn:xmpp:sasl:2"><additional-data>dj02cnJpVFJCaTIzV3BSUi93dHVwK21NaFVaVW4vZEI1bkxUSlJzamw5NUc0PQ==</additional-data><authorization-identifier>user@server</authorization-identifier></success>',
|
||||
),
|
||||
StanzaExpectation(
|
||||
"<iq xmlns='jabber:client' type='set' id='aaaa'><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>',
|
||||
''',
|
||||
adjustId: true,
|
||||
ignoreId: true,
|
||||
),
|
||||
]);
|
||||
final conn = XmppConnection(
|
||||
TestingReconnectionPolicy(),
|
||||
AlwaysConnectedConnectivityManager(),
|
||||
fakeSocket,
|
||||
)..setConnectionSettings(
|
||||
ConnectionSettings(
|
||||
jid: JID.fromString('user@server'),
|
||||
password: 'pencil',
|
||||
useDirectTLS: true,
|
||||
),
|
||||
);
|
||||
await conn.registerManagers([
|
||||
PresenceManager(),
|
||||
RosterManager(TestingRosterStateManager('', [])),
|
||||
DiscoManager([]),
|
||||
]);
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
SaslScramNegotiator(
|
||||
10,
|
||||
'n=user,r=rOprNGfwEbeRWgbNEkqO',
|
||||
'rOprNGfwEbeRWgbNEkqO',
|
||||
ScramHashType.sha256,
|
||||
),
|
||||
ResourceBindingNegotiator(),
|
||||
Sasl2Negotiator(
|
||||
userAgent: const UserAgent(
|
||||
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
|
||||
software: 'moxxmpp',
|
||||
device: "PapaTutuWawa's awesome device",
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
final result = await conn.connect(
|
||||
waitUntilLogin: true,
|
||||
shouldReconnect: false,
|
||||
enableReconnectOnSuccess: false,
|
||||
);
|
||||
expect(result.isType<XmppError>(), false);
|
||||
});
|
||||
|
||||
test('Test SCRAM-SHA-1 SASL2 negotiation with an invalid signature',
|
||||
() async {
|
||||
final fakeSocket = StubTCPSocket([
|
||||
StringExpectation(
|
||||
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='server' from='user@server' xml:lang='en'>",
|
||||
'''
|
||||
<stream:stream
|
||||
xmlns="jabber:client"
|
||||
version="1.0"
|
||||
xmlns:stream="http://etherx.jabber.org/streams"
|
||||
from="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-256</mechanism>
|
||||
</mechanisms>
|
||||
<authentication xmlns='urn:xmpp:sasl:2'>
|
||||
<mechanism>PLAIN</mechanism>
|
||||
<mechanism>SCRAM-SHA-256</mechanism>
|
||||
</authentication>
|
||||
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
|
||||
<required/>
|
||||
</bind>
|
||||
</stream:features>''',
|
||||
),
|
||||
StanzaExpectation(
|
||||
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='SCRAM-SHA-256'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>biwsbj11c2VyLHI9ck9wck5HZndFYmVSV2diTkVrcU8=</initial-response></authenticate>",
|
||||
'''
|
||||
<challenge xmlns='urn:xmpp:sasl:2'>cj1yT3ByTkdmd0ViZVJXZ2JORWtxTyVodllEcFdVYTJSYVRDQWZ1eEZJbGopaE5sRiRrMCxzPVcyMlphSjBTTlk3c29Fc1VFamI2Z1E9PSxpPTQwOTY=</challenge>
|
||||
''',
|
||||
),
|
||||
StanzaExpectation(
|
||||
'<response xmlns="urn:xmpp:sasl:2">Yz1iaXdzLHI9ck9wck5HZndFYmVSV2diTkVrcU8laHZZRHBXVWEyUmFUQ0FmdXhGSWxqKWhObEYkazAscD1kSHpiWmFwV0lrNGpVaE4rVXRlOXl0YWc5empmTUhnc3FtbWl6N0FuZFZRPQ==</response>',
|
||||
'<success xmlns="urn:xmpp:sasl:2"><additional-data>dj1zbUY5cHFWOFM3c3VBb1pXamE0ZEpSa0ZzS1E9</additional-data><authorization-identifier>user@server</authorization-identifier></success>',
|
||||
),
|
||||
]);
|
||||
final conn = XmppConnection(
|
||||
TestingReconnectionPolicy(),
|
||||
AlwaysConnectedConnectivityManager(),
|
||||
fakeSocket,
|
||||
)..setConnectionSettings(
|
||||
ConnectionSettings(
|
||||
jid: JID.fromString('user@server'),
|
||||
password: 'pencil',
|
||||
useDirectTLS: true,
|
||||
),
|
||||
);
|
||||
await conn.registerManagers([
|
||||
PresenceManager(),
|
||||
RosterManager(TestingRosterStateManager('', [])),
|
||||
DiscoManager([]),
|
||||
]);
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
SaslScramNegotiator(
|
||||
10,
|
||||
'n=user,r=rOprNGfwEbeRWgbNEkqO',
|
||||
'rOprNGfwEbeRWgbNEkqO',
|
||||
ScramHashType.sha256,
|
||||
),
|
||||
ResourceBindingNegotiator(),
|
||||
Sasl2Negotiator(
|
||||
userAgent: const UserAgent(
|
||||
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
|
||||
software: 'moxxmpp',
|
||||
device: "PapaTutuWawa's awesome device",
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
final result = await conn.connect(
|
||||
waitUntilLogin: true,
|
||||
shouldReconnect: false,
|
||||
enableReconnectOnSuccess: false,
|
||||
);
|
||||
expect(result.isType<NegotiatorError>(), true);
|
||||
expect(result.get<NegotiatorError>() is InvalidServerSignatureError, true);
|
||||
});
|
||||
|
||||
test('Test simple SASL2 inlining', () async {
|
||||
final fakeSocket = StubTCPSocket([
|
||||
StringExpectation(
|
||||
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@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>
|
||||
<authentication xmlns='urn:xmpp:sasl:2'>
|
||||
<mechanism>PLAIN</mechanism>
|
||||
<inline>
|
||||
<test-feature xmlns="invalid:example:dont:use" />
|
||||
</inline>
|
||||
</authentication>
|
||||
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
|
||||
<required/>
|
||||
</bind>
|
||||
</stream:features>''',
|
||||
),
|
||||
StanzaExpectation(
|
||||
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='PLAIN'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>AHBvbHlub21kaXZpc2lvbgBhYWFh</initial-response><test-data-request xmlns='invalid:example:dont:use' /></authenticate>",
|
||||
'''
|
||||
<success xmlns='urn:xmpp:sasl:2'>
|
||||
<authorization-identifier>polynomdivision@test.server</authorization-identifier>
|
||||
<test-data xmlns='invalid:example:dont:use'>Hello World</test-data>
|
||||
</success>
|
||||
''',
|
||||
),
|
||||
StanzaExpectation(
|
||||
"<iq xmlns='jabber:client' type='set' id='aaaa'><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>',
|
||||
''',
|
||||
adjustId: true,
|
||||
ignoreId: true,
|
||||
),
|
||||
]);
|
||||
final conn = XmppConnection(
|
||||
TestingReconnectionPolicy(),
|
||||
AlwaysConnectedConnectivityManager(),
|
||||
fakeSocket,
|
||||
)..setConnectionSettings(
|
||||
ConnectionSettings(
|
||||
jid: JID.fromString('polynomdivision@test.server'),
|
||||
password: 'aaaa',
|
||||
useDirectTLS: true,
|
||||
),
|
||||
);
|
||||
await conn.registerManagers([
|
||||
PresenceManager(),
|
||||
RosterManager(TestingRosterStateManager('', [])),
|
||||
DiscoManager([]),
|
||||
]);
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
ResourceBindingNegotiator(),
|
||||
ExampleNegotiator(),
|
||||
Sasl2Negotiator(
|
||||
userAgent: const UserAgent(
|
||||
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
|
||||
software: 'moxxmpp',
|
||||
device: "PapaTutuWawa's awesome device",
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
final result = await conn.connect(
|
||||
waitUntilLogin: true,
|
||||
shouldReconnect: false,
|
||||
enableReconnectOnSuccess: false,
|
||||
);
|
||||
expect(result.isType<NegotiatorError>(), false);
|
||||
|
||||
expect(
|
||||
conn.getNegotiatorById<ExampleNegotiator>('testNegotiator')!.value,
|
||||
'Hello World',
|
||||
);
|
||||
});
|
||||
|
||||
test('Test simple SASL2 inlining 2', () async {
|
||||
final fakeSocket = StubTCPSocket([
|
||||
StringExpectation(
|
||||
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@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>
|
||||
<authentication xmlns='urn:xmpp:sasl:2'>
|
||||
<mechanism>PLAIN</mechanism>
|
||||
<inline>
|
||||
<resume xmlns="urn:xmpp:sm:3" />
|
||||
</inline>
|
||||
</authentication>
|
||||
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
|
||||
<required/>
|
||||
</bind>
|
||||
</stream:features>''',
|
||||
),
|
||||
StanzaExpectation(
|
||||
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='PLAIN'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>AHBvbHlub21kaXZpc2lvbgBhYWFh</initial-response></authenticate>",
|
||||
'''
|
||||
<success xmlns='urn:xmpp:sasl:2'>
|
||||
<authorization-identifier>polynomdivision@test.server</authorization-identifier>
|
||||
</success>
|
||||
''',
|
||||
),
|
||||
StanzaExpectation(
|
||||
"<iq xmlns='jabber:client' type='set' id='aaaa'><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>',
|
||||
''',
|
||||
adjustId: true,
|
||||
ignoreId: true,
|
||||
),
|
||||
]);
|
||||
final conn = XmppConnection(
|
||||
TestingReconnectionPolicy(),
|
||||
AlwaysConnectedConnectivityManager(),
|
||||
fakeSocket,
|
||||
)..setConnectionSettings(
|
||||
ConnectionSettings(
|
||||
jid: JID.fromString('polynomdivision@test.server'),
|
||||
password: 'aaaa',
|
||||
useDirectTLS: true,
|
||||
),
|
||||
);
|
||||
await conn.registerManagers([
|
||||
PresenceManager(),
|
||||
RosterManager(TestingRosterStateManager('', [])),
|
||||
DiscoManager([]),
|
||||
]);
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
ResourceBindingNegotiator(),
|
||||
ExampleNegotiator(),
|
||||
Sasl2Negotiator(
|
||||
userAgent: const UserAgent(
|
||||
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
|
||||
software: 'moxxmpp',
|
||||
device: "PapaTutuWawa's awesome device",
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
final result = await conn.connect(
|
||||
waitUntilLogin: true,
|
||||
shouldReconnect: false,
|
||||
enableReconnectOnSuccess: false,
|
||||
);
|
||||
expect(result.isType<NegotiatorError>(), false);
|
||||
|
||||
expect(
|
||||
conn.getNegotiatorById<ExampleNegotiator>('testNegotiator')!.value,
|
||||
null,
|
||||
);
|
||||
});
|
||||
}
|
269
packages/moxxmpp/test/xeps/xep_xxxx_fast_test.dart
Normal file
269
packages/moxxmpp/test/xeps/xep_xxxx_fast_test.dart
Normal file
@ -0,0 +1,269 @@
|
||||
import 'package:moxxmpp/moxxmpp.dart';
|
||||
import 'package:test/test.dart';
|
||||
import '../helpers/logging.dart';
|
||||
import '../helpers/xmpp.dart';
|
||||
|
||||
void main() {
|
||||
initLogger();
|
||||
|
||||
test('Test FAST authentication without a token', () async {
|
||||
final fakeSocket = StubTCPSocket([
|
||||
StringExpectation(
|
||||
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@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>HT-SHA-256-NONE</mechanism>
|
||||
</mechanisms>
|
||||
<authentication xmlns='urn:xmpp:sasl:2'>
|
||||
<mechanism>PLAIN</mechanism>
|
||||
<mechanism>HT-SHA-256-NONE</mechanism>
|
||||
<mechanism>HT-SHA-256-ENDP</mechanism>
|
||||
<inline>
|
||||
<bind xmlns="urn:xmpp:bind:0" />
|
||||
<fast xmlns="urn:xmpp:fast:0">
|
||||
<mechanism>HT-SHA-256-NONE</mechanism>
|
||||
<mechanism>HT-SHA-256-ENDP</mechanism>
|
||||
</fast>
|
||||
</inline>
|
||||
</authentication>
|
||||
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
|
||||
<required/>
|
||||
</bind>
|
||||
</stream:features>''',
|
||||
),
|
||||
StanzaExpectation(
|
||||
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='PLAIN'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>AHBvbHlub21kaXZpc2lvbgBhYWFh</initial-response><request-token xmlns='urn:xmpp:fast:0' mechanism='HT-SHA-256-NONE' /></authenticate>",
|
||||
'''
|
||||
<success xmlns='urn:xmpp:sasl:2'>
|
||||
<authorization-identifier>polynomdivision@test.server</authorization-identifier>
|
||||
<token xmlns='urn:xmpp:fast:0'
|
||||
expiry='2020-03-12T14:36:15Z'
|
||||
token='WXZzciBwYmFmdmZnZiBqdmd1IGp2eXFhcmZm' />
|
||||
</success>
|
||||
''',
|
||||
),
|
||||
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,
|
||||
),
|
||||
StringExpectation(
|
||||
'',
|
||||
'',
|
||||
),
|
||||
StringExpectation(
|
||||
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@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>HT-SHA-256-NONE</mechanism>
|
||||
</mechanisms>
|
||||
<authentication xmlns='urn:xmpp:sasl:2'>
|
||||
<mechanism>PLAIN</mechanism>
|
||||
<mechanism>HT-SHA-256-NONE</mechanism>
|
||||
<mechanism>HT-SHA-256-ENDP</mechanism>
|
||||
<inline>
|
||||
<bind xmlns="urn:xmpp:bind:0" />
|
||||
<fast xmlns="urn:xmpp:fast:0">
|
||||
<mechanism>HT-SHA-256-NONE</mechanism>
|
||||
<mechanism>HT-SHA-256-ENDP</mechanism>
|
||||
</fast>
|
||||
</inline>
|
||||
</authentication>
|
||||
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
|
||||
<required/>
|
||||
</bind>
|
||||
</stream:features>''',
|
||||
),
|
||||
StanzaExpectation(
|
||||
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='HT-SHA-256-NONE'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>WXZzciBwYmFmdmZnZiBqdmd1IGp2eXFhcmZm</initial-response><fast xmlns='urn:xmpp:fast:0' /></authenticate>",
|
||||
'''
|
||||
<success xmlns='urn:xmpp:sasl:2'>
|
||||
<authorization-identifier>polynomdivision@test.server</authorization-identifier>
|
||||
</success>
|
||||
''',
|
||||
),
|
||||
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,
|
||||
),
|
||||
]);
|
||||
final conn = XmppConnection(
|
||||
TestingReconnectionPolicy(),
|
||||
AlwaysConnectedConnectivityManager(),
|
||||
fakeSocket,
|
||||
)..setConnectionSettings(
|
||||
ConnectionSettings(
|
||||
jid: JID.fromString('polynomdivision@test.server'),
|
||||
password: 'aaaa',
|
||||
useDirectTLS: true,
|
||||
),
|
||||
);
|
||||
await conn.registerManagers([
|
||||
RosterManager(TestingRosterStateManager('', [])),
|
||||
DiscoManager([]),
|
||||
]);
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
ResourceBindingNegotiator(),
|
||||
FASTSaslNegotiator(),
|
||||
Sasl2Negotiator(
|
||||
userAgent: const UserAgent(
|
||||
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
|
||||
software: 'moxxmpp',
|
||||
device: "PapaTutuWawa's awesome device",
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
final result1 = await conn.connect(
|
||||
waitUntilLogin: true,
|
||||
shouldReconnect: false,
|
||||
enableReconnectOnSuccess: false,
|
||||
);
|
||||
expect(result1.isType<NegotiatorError>(), false);
|
||||
expect(conn.resource, 'MU29eEZn');
|
||||
expect(fakeSocket.getState(), 3);
|
||||
|
||||
final token = conn
|
||||
.getNegotiatorById<FASTSaslNegotiator>(saslFASTNegotiator)!
|
||||
.fastToken;
|
||||
expect(token != null, true);
|
||||
expect(token!.token, 'WXZzciBwYmFmdmZnZiBqdmd1IGp2eXFhcmZm');
|
||||
|
||||
// Disconnect
|
||||
await conn.disconnect();
|
||||
|
||||
// Connect again, but use FAST this time
|
||||
final result2 = await conn.connect(
|
||||
waitUntilLogin: true,
|
||||
shouldReconnect: false,
|
||||
enableReconnectOnSuccess: false,
|
||||
);
|
||||
expect(result2.isType<NegotiatorError>(), false);
|
||||
expect(conn.resource, 'MU29eEZn');
|
||||
expect(fakeSocket.getState(), 7);
|
||||
});
|
||||
|
||||
test('Test failed FAST authentication with a token', () async {
|
||||
final fakeSocket = StubTCPSocket([
|
||||
StringExpectation(
|
||||
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@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>HT-SHA-256-NONE</mechanism>
|
||||
</mechanisms>
|
||||
<authentication xmlns='urn:xmpp:sasl:2'>
|
||||
<mechanism>PLAIN</mechanism>
|
||||
<mechanism>HT-SHA-256-NONE</mechanism>
|
||||
<mechanism>HT-SHA-256-ENDP</mechanism>
|
||||
<inline>
|
||||
<bind xmlns="urn:xmpp:bind:0" />
|
||||
<fast xmlns="urn:xmpp:fast:0">
|
||||
<mechanism>HT-SHA-256-NONE</mechanism>
|
||||
<mechanism>HT-SHA-256-ENDP</mechanism>
|
||||
</fast>
|
||||
</inline>
|
||||
</authentication>
|
||||
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
|
||||
<required/>
|
||||
</bind>
|
||||
</stream:features>''',
|
||||
),
|
||||
StanzaExpectation(
|
||||
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='HT-SHA-256-NONE'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>WXZzciBwYmFmdmZnZiBqdmd1IGp2eXFhcmZm</initial-response><fast xmlns='urn:xmpp:fast:0' /></authenticate>",
|
||||
'''
|
||||
<failure xmlns='urn:xmpp:sasl:2'>
|
||||
<not-authorized xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>
|
||||
</failure>
|
||||
''',
|
||||
),
|
||||
StanzaExpectation(
|
||||
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='PLAIN'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>AHBvbHlub21kaXZpc2lvbgBhYWFh</initial-response><request-token xmlns='urn:xmpp:fast:0' mechanism='HT-SHA-256-NONE' /></authenticate>",
|
||||
'''
|
||||
<success xmlns='urn:xmpp:sasl:2'>
|
||||
<authorization-identifier>polynomdivision@test.server</authorization-identifier>
|
||||
<token xmlns='urn:xmpp:fast:0'
|
||||
expiry='2020-03-12T14:36:15Z'
|
||||
token='ed00e36cb42449a365a306a413f51ffd5ea8' />
|
||||
</success>
|
||||
''',
|
||||
),
|
||||
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,
|
||||
),
|
||||
]);
|
||||
final conn = XmppConnection(
|
||||
TestingReconnectionPolicy(),
|
||||
AlwaysConnectedConnectivityManager(),
|
||||
fakeSocket,
|
||||
)..setConnectionSettings(
|
||||
ConnectionSettings(
|
||||
jid: JID.fromString('polynomdivision@test.server'),
|
||||
password: 'aaaa',
|
||||
useDirectTLS: true,
|
||||
),
|
||||
);
|
||||
await conn.registerManagers([
|
||||
RosterManager(TestingRosterStateManager('', [])),
|
||||
DiscoManager([]),
|
||||
]);
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
ResourceBindingNegotiator(),
|
||||
FASTSaslNegotiator()
|
||||
..fastToken = const FASTToken(
|
||||
'WXZzciBwYmFmdmZnZiBqdmd1IGp2eXFhcmZm',
|
||||
'2020-03-12T14:36:15Z',
|
||||
),
|
||||
Sasl2Negotiator(
|
||||
userAgent: const UserAgent(
|
||||
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
|
||||
software: 'moxxmpp',
|
||||
device: "PapaTutuWawa's awesome device",
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
final result1 = await conn.connect(
|
||||
waitUntilLogin: true,
|
||||
shouldReconnect: false,
|
||||
enableReconnectOnSuccess: false,
|
||||
);
|
||||
expect(result1.isType<NegotiatorError>(), false);
|
||||
expect(conn.resource, 'MU29eEZn');
|
||||
expect(fakeSocket.getState(), 4);
|
||||
|
||||
final token = conn
|
||||
.getNegotiatorById<FASTSaslNegotiator>(saslFASTNegotiator)!
|
||||
.fastToken;
|
||||
expect(token != null, true);
|
||||
expect(token!.token, 'ed00e36cb42449a365a306a413f51ffd5ea8');
|
||||
});
|
||||
}
|
@ -35,7 +35,6 @@ Future<bool> testRosterManager(
|
||||
),
|
||||
getManagerById: getManagerNullStub,
|
||||
getNegotiatorById: getNegotiatorNullStub,
|
||||
isFeatureSupported: (_) => false,
|
||||
getFullJID: () => JID.fromString('$bareJid/$resource'),
|
||||
getSocket: () => StubTCPSocket([]),
|
||||
getConnection: () => XmppConnection(
|
||||
@ -71,7 +70,7 @@ void main() {
|
||||
final fakeSocket = StubTCPSocket(
|
||||
[
|
||||
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' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
|
||||
'''
|
||||
<stream:stream
|
||||
xmlns="jabber:client"
|
||||
@ -90,7 +89,7 @@ void main() {
|
||||
'<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' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
|
||||
'''
|
||||
<stream:stream
|
||||
xmlns="jabber:client"
|
||||
@ -140,7 +139,7 @@ void main() {
|
||||
StreamManagementManager(),
|
||||
EntityCapabilitiesManager('http://moxxmpp.example'),
|
||||
]);
|
||||
conn.registerFeatureNegotiators([
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
SaslScramNegotiator(10, '', '', ScramHashType.sha512),
|
||||
ResourceBindingNegotiator(),
|
||||
@ -151,13 +150,14 @@ void main() {
|
||||
waitUntilLogin: true,
|
||||
);
|
||||
expect(fakeSocket.getState(), /*6*/ 5);
|
||||
expect(conn.resource, 'MU29eEZn');
|
||||
});
|
||||
|
||||
test('Test a failed SASL auth', () async {
|
||||
final fakeSocket = StubTCPSocket(
|
||||
[
|
||||
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' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
|
||||
'''
|
||||
<stream:stream
|
||||
xmlns="jabber:client"
|
||||
@ -195,7 +195,7 @@ void main() {
|
||||
DiscoManager([]),
|
||||
EntityCapabilitiesManager('http://moxxmpp.example'),
|
||||
]);
|
||||
conn.registerFeatureNegotiators([
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
]);
|
||||
|
||||
@ -216,7 +216,7 @@ void main() {
|
||||
final fakeSocket = StubTCPSocket(
|
||||
[
|
||||
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' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
|
||||
'''
|
||||
<stream:stream
|
||||
xmlns="jabber:client"
|
||||
@ -254,7 +254,7 @@ void main() {
|
||||
DiscoManager([]),
|
||||
EntityCapabilitiesManager('http://moxxmpp.example'),
|
||||
]);
|
||||
conn.registerFeatureNegotiators([SaslPlainNegotiator()]);
|
||||
await conn.registerFeatureNegotiators([SaslPlainNegotiator()]);
|
||||
|
||||
conn.asBroadcastStream().listen((event) {
|
||||
if (event is AuthenticationFailedEvent &&
|
||||
@ -296,7 +296,6 @@ void main() {
|
||||
),
|
||||
getManagerById: getManagerNullStub,
|
||||
getNegotiatorById: getNegotiatorNullStub,
|
||||
isFeatureSupported: (_) => false,
|
||||
getFullJID: () => JID.fromString('some.user@example.server/aaaaa'),
|
||||
getSocket: () => StubTCPSocket([]),
|
||||
getConnection: () => XmppConnection(
|
||||
@ -380,7 +379,7 @@ void main() {
|
||||
final fakeSocket = StubTCPSocket(
|
||||
[
|
||||
StringExpectation(
|
||||
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='example.org' xml:lang='en'>",
|
||||
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='example.org' from='testuser@example.org' xml:lang='en'>",
|
||||
'''
|
||||
<stream:stream
|
||||
xmlns="jabber:client"
|
||||
@ -407,18 +406,17 @@ void main() {
|
||||
RosterManager(TestingRosterStateManager('', [])),
|
||||
DiscoManager([]),
|
||||
]);
|
||||
conn
|
||||
..registerFeatureNegotiators([
|
||||
// SaslPlainNegotiator(),
|
||||
ResourceBindingNegotiator(),
|
||||
])
|
||||
..setConnectionSettings(
|
||||
ConnectionSettings(
|
||||
jid: JID.fromString('testuser@example.org'),
|
||||
password: 'abc123',
|
||||
useDirectTLS: false,
|
||||
),
|
||||
);
|
||||
await conn.registerFeatureNegotiators([
|
||||
// SaslPlainNegotiator(),
|
||||
ResourceBindingNegotiator(),
|
||||
]);
|
||||
conn.setConnectionSettings(
|
||||
ConnectionSettings(
|
||||
jid: JID.fromString('testuser@example.org'),
|
||||
password: 'abc123',
|
||||
useDirectTLS: false,
|
||||
),
|
||||
);
|
||||
|
||||
final result = await conn.connect(
|
||||
waitUntilLogin: true,
|
||||
|
@ -18,9 +18,10 @@ Future<void> _runTest(String domain) async {
|
||||
TestingReconnectionPolicy(),
|
||||
AlwaysConnectedConnectivityManager(),
|
||||
socket,
|
||||
)..registerFeatureNegotiators([
|
||||
StartTlsNegotiator(),
|
||||
]);
|
||||
);
|
||||
await connection.registerFeatureNegotiators([
|
||||
StartTlsNegotiator(),
|
||||
]);
|
||||
await connection.registerManagers([
|
||||
DiscoManager([]),
|
||||
RosterManager(TestingRosterStateManager('', [])),
|
||||
|
@ -19,9 +19,10 @@ void main() {
|
||||
TestingSleepReconnectionPolicy(10),
|
||||
AlwaysConnectedConnectivityManager(),
|
||||
TCPSocketWrapper(),
|
||||
)..registerFeatureNegotiators([
|
||||
StartTlsNegotiator(),
|
||||
]);
|
||||
);
|
||||
await connection.registerFeatureNegotiators([
|
||||
StartTlsNegotiator(),
|
||||
]);
|
||||
await connection.registerManagers([
|
||||
DiscoManager([]),
|
||||
RosterManager(TestingRosterStateManager('', [])),
|
||||
@ -68,9 +69,10 @@ void main() {
|
||||
TestingReconnectionPolicy(),
|
||||
AlwaysConnectedConnectivityManager(),
|
||||
TCPSocketWrapper(),
|
||||
)..registerFeatureNegotiators([
|
||||
StartTlsNegotiator(),
|
||||
]);
|
||||
);
|
||||
await connection.registerFeatureNegotiators([
|
||||
StartTlsNegotiator(),
|
||||
]);
|
||||
await connection.registerManagers([
|
||||
DiscoManager([]),
|
||||
RosterManager(TestingRosterStateManager('', [])),
|
||||
|
@ -243,6 +243,8 @@ class TCPSocketWrapper extends BaseSocketWrapper {
|
||||
if (await _hostPortConnect(host, port)) {
|
||||
_setupStreams();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -289,17 +291,13 @@ class TCPSocketWrapper extends BaseSocketWrapper {
|
||||
_eventStream.stream.asBroadcastStream();
|
||||
|
||||
@override
|
||||
void write(String data, {String? redact}) {
|
||||
void write(String data) {
|
||||
if (_socket == null) {
|
||||
_log.severe('Failed to write to socket as _socket is null');
|
||||
return;
|
||||
}
|
||||
|
||||
if (redact != null) {
|
||||
_log.finest('**> $redact');
|
||||
} else {
|
||||
_log.finest('==> $data');
|
||||
}
|
||||
_log.finest('==> $data');
|
||||
|
||||
try {
|
||||
_socket!.write(data);
|
||||
|
Loading…
Reference in New Issue
Block a user