git » sdk » commit 42fff24

SASL2, FAST, and SCRAM-SHA-1 in browser

author Stephen Paul Weber
2023-11-27 19:13:18 UTC
committer Stephen Paul Weber
2023-11-27 19:13:18 UTC
parent fd63db6faf60f595f670dc56ffcd3e90b0af1b4c

SASL2, FAST, and SCRAM-SHA-1 in browser

xmpp/Client.hx +25 -11
xmpp/GenericStream.hx +1 -0
xmpp/Persistence.hx +1 -1
xmpp/persistence/browser.js +11 -3
xmpp/streams/XmppJsStream.hx +80 -52

diff --git a/xmpp/Client.hx b/xmpp/Client.hx
index 185b14f..621aeb2 100644
--- a/xmpp/Client.hx
+++ b/xmpp/Client.hx
@@ -47,6 +47,7 @@ class Client extends xmpp.EventEmitter {
 		]
 	);
 	private var _displayName: String;
+	private var fastMechanism: Null<String> = null;
 
 	public function new(jid: String, persistence: Persistence) {
 		super();
@@ -55,6 +56,12 @@ class Client extends xmpp.EventEmitter {
 		this.persistence = persistence;
 		stream = new Stream();
 		stream.on("status/online", this.onConnected);
+
+		stream.on("fast-token", (data) -> {
+			persistence.storeLogin(this.jid.asBare().asString(), stream.clientId ?? this.jid.resource, displayName(), data.token);
+			return EventHandled;
+		});
+
 		stream.on("sm/update", (data) -> {
 			persistence.storeStreamManagement(accountId(), data.id, data.outbound, data.inbound, data.outbound_q);
 			return EventHandled;
@@ -366,13 +373,14 @@ class Client extends xmpp.EventEmitter {
 	}
 
 	public function setDisplayName(fn: String) {
-		if (fn == null || fn == "" || fn == displayName()) return;
+		if (fn == null || fn == "" || fn == displayName()) return false;
 		_displayName = fn;
-		persistence.storeLogin(jid.asBare().asString(), jid.resource, fn, null);
+		persistence.storeLogin(jid.asBare().asString(), stream.clientId ?? jid.resource, fn, null);
 		for (chat in getChats()) {
 			if (Std.isOfType(chat, Channel)) Std.downcast(chat, Channel)?.selfPing(false);
 		}
 		// TODO: should this path publish to server too? But we use it for notifications from server right now...
+		return true;
 	}
 
 	public function start() {
@@ -392,14 +400,20 @@ class Client extends xmpp.EventEmitter {
 				this.trigger("chats/update", chats);
 
 				persistence.getStreamManagement(accountId(), (smId, smOut, smIn, smOutQ) -> {
-					persistence.getLogin(accountId(), (clientId, token, displayName) -> {
-						setDisplayName(displayName);
-						if (clientId != null) jid = jid.withResource(clientId);
-						if (token == null) {
-							stream.on("auth/password-needed", (data)->this.trigger("auth/password-needed", { accountId: accountId() }));
-						} else {
-							stream.on("auth/password-needed", (data)->this.stream.trigger("auth/password", { password: token }));
+					persistence.getLogin(accountId(), (clientId, token, fastCount, displayName) -> {
+						stream.clientId = clientId ?? ID.long();
+						jid = jid.withResource(stream.clientId);
+						if (!setDisplayName(displayName) && clientId == null) {
+							persistence.storeLogin(jid.asBare().asString(), stream.clientId, this.displayName(), null);
 						}
+						stream.on("auth/password-needed", (data) -> {
+							fastMechanism = data.mechanisms.find((mech) -> mech.canFast)?.name;
+							if (token == null || fastMechanism == null) {
+								this.trigger("auth/password-needed", { accountId: accountId() });
+							} else {
+								this.stream.trigger("auth/password", { password: token, mechanism: fastMechanism, fastCount: fastCount });
+							}
+						});
 						stream.connect(jid.asString(), smId == null || smId == "" ? null : { id: smId, outbound: smOut, inbound: smIn, outbound_q: smOutQ });
 					});
 				});
@@ -414,7 +428,7 @@ class Client extends xmpp.EventEmitter {
 	private function onConnected(data) { // Fired on connect or reconnect
 		if (data != null && data.jid != null) {
 			jid = JID.parse(data.jid);
-			if (!jid.isBare()) persistence.storeLogin(jid.asBare().asString(), jid.resource, displayName(), null);
+			if (stream.clientId == null && !jid.isBare()) persistence.storeLogin(jid.asBare().asString(), jid.resource, displayName(), null);
 		}
 
 		if (data.resumed) return EventHandled;
@@ -452,7 +466,7 @@ class Client extends xmpp.EventEmitter {
 	}
 
 	public function usePassword(password: String):Void {
-		this.stream.trigger("auth/password", { password: password });
+		this.stream.trigger("auth/password", { password: password, requestToken: fastMechanism });
 	}
 
 	/* Return array of chats, sorted by last activity */
diff --git a/xmpp/GenericStream.hx b/xmpp/GenericStream.hx
index edfa3e3..cb2cbbc 100644
--- a/xmpp/GenericStream.hx
+++ b/xmpp/GenericStream.hx
@@ -10,6 +10,7 @@ enum IqResult {
 }
 
 abstract class GenericStream extends EventEmitter {
+	public var clientId: Null<String> = null;
 
 	public function new() {
 		super();
diff --git a/xmpp/Persistence.hx b/xmpp/Persistence.hx
index 5115c19..428f796 100644
--- a/xmpp/Persistence.hx
+++ b/xmpp/Persistence.hx
@@ -18,7 +18,7 @@ abstract class Persistence {
 	abstract public function storeCaps(caps:Caps):Void;
 	abstract public function getCaps(ver:String, callback: (Caps)->Void):Void;
 	abstract public function storeLogin(login:String, clientId:String, displayName:String, token:Null<String>):Void;
-	abstract public function getLogin(login:String, callback:(clientId:String, token:Null<String>, displayName:String)->Void):Void;
+	abstract public function getLogin(login:String, callback:(clientId:String, token:Null<String>, fastCount: Int, displayName:String)->Void):Void;
 	abstract public function storeStreamManagement(accountId:String, smId:String, outboundCount:Int, inboundCount:Int, outboundQueue:Array<String>):Void;
 	abstract public function getStreamManagement(accountId:String, callback: (smId:String, outboundCount:Int, inboundCount:Int, outboundQueue:Array<String>)->Void):Void;
 }
diff --git a/xmpp/persistence/browser.js b/xmpp/persistence/browser.js
index 7e24598..c255264 100644
--- a/xmpp/persistence/browser.js
+++ b/xmpp/persistence/browser.js
@@ -361,7 +361,10 @@ exports.xmpp.persistence = {
 				const store = tx.objectStore("keyvaluepairs");
 				store.put(clientId, "login:clientId:" + login).onerror = console.error;
 				store.put(displayName, "fn:" + login).onerror = console.error;
-				if (token != null) store.put(token, "login:token:" + login).onerror = console.error;
+				if (token != null) {
+					store.put(token, "login:token:" + login).onerror = console.error;
+					store.put(0, "login:fastCount:" + login).onerror = console.error;
+				}
 			},
 
 			storeStreamManagement: function(account, id, outbound, inbound, outbound_q) {
@@ -385,15 +388,20 @@ exports.xmpp.persistence = {
 			},
 
 			getLogin: function(login, callback) {
-				const tx = db.transaction(["keyvaluepairs"], "readonly");
+				const tx = db.transaction(["keyvaluepairs"], "readwrite");
 				const store = tx.objectStore("keyvaluepairs");
 				Promise.all([
 					promisifyRequest(store.get("login:clientId:" + login)),
 					promisifyRequest(store.get("login:token:" + login)),
+					promisifyRequest(store.get("login:fastCount:" + login)),
 					promisifyRequest(store.get("fn:" + login)),
 				]).then((result) => {
-					callback(result[0], result[1], result[2]);
+					if (result[1]) {
+						store.put((result[2] || 0) + 1, "login:fastCount:" + login).onerror = console.error;
+					}
+					callback(result[0], result[1], result[2] || 0, result[3]);
 				}).catch((e) => {
+					console.error(e);
 					callback(null, null, null);
 				});
 			}
diff --git a/xmpp/streams/XmppJsStream.hx b/xmpp/streams/XmppJsStream.hx
index ea7e0c8..e1cc8de 100644
--- a/xmpp/streams/XmppJsStream.hx
+++ b/xmpp/streams/XmppJsStream.hx
@@ -3,11 +3,18 @@ package xmpp.streams;
 import js.lib.Promise;
 import haxe.Http;
 import haxe.Json;
+using Lambda;
 
 import xmpp.FSM;
 import xmpp.GenericStream;
 import xmpp.Stanza;
 
+@:jsRequire("@xmpp/sasl-scram-sha-1")
+extern class XmppJsScramSha1 {
+	@:selfCall
+	function new(sasl: Dynamic);
+}
+
 @:jsRequire("@xmpp/client", "client")
 extern class XmppJsClient {
 	function new(options:Dynamic);
@@ -15,12 +22,14 @@ extern class XmppJsClient {
 	function on(eventName:String, callback:(Dynamic)->Void):Void;
 	function send(stanza:XmppJsXml):Void;
 	var jid:XmppJsJID;
+	var streamFrom:Null<XmppJsJID>;
 	var status: String;
 	var iqCallee:{
 		get: (String, String, ({stanza: XmppJsXml})->Any)->Void,
 		set: (String, String, ({stanza: XmppJsXml})->Any)->Void,
 	};
 	var streamManagement: { id:String, outbound: Int, inbound: Int, outbound_q: Array<XmppJsXml>, enabled: Bool, allowResume: Bool };
+	var sasl2: Dynamic;
 }
 
 @:jsRequire("@xmpp/jid", "jid")
@@ -133,71 +142,90 @@ class XmppJsStream extends GenericStream {
 		}
 		connectionURI = uri;
 
-		this.on("auth/password", function (event) {
-			var xmpp = new XmppJsClient({
-				service: connectionURI,
-				domain: jid.domain,
-				username: jid.local,
-				resource: jid.resource,
-				password: event.password,
+		final waitForCreds = new js.lib.Promise((resolve, reject) -> {
+			this.on("auth/password", (event: Dynamic) -> {
+				if (event.username == null) event.username = jid.local;
+				resolve(event);
+				return EventHandled;
 			});
+		});
 
-			if(this.debug) {
-				new XmppJsDebug(xmpp, true);
+		final clientId = jid.resource;
+		final xmpp = new XmppJsClient({
+			service: connectionURI,
+			domain: jid.domain,
+			resource: jid.resource,
+			clientId: clientId,
+			credentials: (callback, mechanisms: Dynamic) -> {
+				this.clientId = Std.is(mechanisms, Array) ? clientId : null;
+				final mechs: Array<{name: String, canFast: Bool, canOther: Bool}> = Std.is(mechanisms, Array) ? mechanisms : [{ name: mechanisms, canFast: false, canOther: true }];
+				final mech = mechs.find((m) -> m.canOther)?.name;
+				this.trigger("auth/password-needed", { mechanisms: mechs });
+				return waitForCreds.then((creds) -> {
+					return callback(creds, creds.mechanism ?? mech);
+				});
 			}
+		});
+		new XmppJsScramSha1(xmpp.sasl2);
+		xmpp.streamFrom = this.jid;
 
-			if (initialSM != null) {
-				xmpp.streamManagement.id = initialSM.id;
-				xmpp.streamManagement.outbound = initialSM.outbound;
-				xmpp.streamManagement.inbound = initialSM.inbound;
-				xmpp.streamManagement.outbound_q = (initialSM.outbound_q ?? []).map(XmppJsLtx.parse);
-				initialSM = null;
-			}
+		if(this.debug) {
+			new XmppJsDebug(xmpp, true);
+		}
+
+		if (initialSM != null) {
+			xmpp.streamManagement.id = initialSM.id;
+			xmpp.streamManagement.outbound = initialSM.outbound;
+			xmpp.streamManagement.inbound = initialSM.inbound;
+			xmpp.streamManagement.outbound_q = (initialSM.outbound_q ?? []).map(XmppJsLtx.parse);
+			initialSM = null;
+		}
 
-			this.client = xmpp;
-			processPendingOnIq();
+		this.client = xmpp;
+		processPendingOnIq();
 
-			xmpp.on("online", function (jid) {
-				resumed = false;
-				this.jid = jid;
-				this.state.event("connection-success");
-			});
+		xmpp.on("online", function (jid) {
+			resumed = false;
+			this.jid = jid;
+			this.state.event("connection-success");
+		});
 
-			xmpp.on("offline", function (data) {
-				this.state.event("connection-closed");
-			});
+		xmpp.on("offline", function (data) {
+			this.state.event("connection-closed");
+		});
 
-			xmpp.on("stanza", function (stanza) {
-				if (xmpp.status == "online" && this.state.can("connection-success")) {
-					resumed = xmpp.streamManagement.enabled && xmpp.streamManagement.id != null && xmpp.streamManagement.id != "";
-					if (xmpp.jid == null) {
-						xmpp.jid = this.jid;
-					} else {
-						this.jid = xmpp.jid;
-					}
-					this.state.event("connection-success");
+		xmpp.on("stanza", function (stanza) {
+			if (xmpp.status == "online" && this.state.can("connection-success")) {
+				resumed = xmpp.streamManagement.enabled && xmpp.streamManagement.id != null && xmpp.streamManagement.id != "";
+				if (xmpp.jid == null) {
+					xmpp.jid = this.jid;
+				} else {
+					this.jid = xmpp.jid;
 				}
-				this.onStanza(convertToStanza(stanza));
-				triggerSMupdate();
-			});
+				this.state.event("connection-success");
+			}
+			this.onStanza(convertToStanza(stanza));
+			triggerSMupdate();
+		});
 
-			xmpp.on("stream-management/ack", (stanza) -> {
-				if (stanza.name == "message" && stanza.attrs.id != null) this.trigger("sm/ack", { id: stanza.attrs.id });
-				triggerSMupdate();
-			});
+		xmpp.on("stream-management/ack", (stanza) -> {
+			if (stanza.name == "message" && stanza.attrs.id != null) this.trigger("sm/ack", { id: stanza.attrs.id });
+			triggerSMupdate();
+		});
 
-			xmpp.on("stream-management/fail", (stanza) -> {
-				if (stanza.name == "message" && stanza.attrs.id != null) this.trigger("sm/fail", { id: stanza.attrs.id });
-				triggerSMupdate();
-			});
+		xmpp.on("stream-management/fail", (stanza) -> {
+			if (stanza.name == "message" && stanza.attrs.id != null) this.trigger("sm/fail", { id: stanza.attrs.id });
+			triggerSMupdate();
+		});
 
-			resumed = false;
-			xmpp.start().catchError(function (err) {
-				trace(err);
-			});
-			return EventHandled;
+		xmpp.on("fast-token", (tokenEl) -> {
+			this.trigger("fast-token", tokenEl.attrs);
+		});
+
+		resumed = false;
+		xmpp.start().catchError(function (err) {
+			trace(err);
 		});
-		this.trigger("auth/password-needed", {});
 	}
 
 	public function connect(jid:String, sm:Null<{id:String,outbound:Int,inbound:Int,outbound_q:Array<String>}>) {