diff --git a/packages/client-core/lib/Client.js b/packages/client-core/lib/Client.js
index 11c86c6d..bd9a18a7 100644
--- a/packages/client-core/lib/Client.js
+++ b/packages/client-core/lib/Client.js
@@ -42,8 +42,14 @@ class Client extends Connection {
return this.Transport.prototype.socketParameters(...args);
}
- header(...args) {
- return this.Transport.prototype.header(...args);
+ header(headerElement, ...args) {
+ // if the client knows the XMPP identity then it SHOULD include the 'from' attribute
+ // after the confidentiality and integrity of the stream are protected via TLS
+ // or an equivalent security layer.
+ // https://xmpp.org/rfcs/rfc6120.html#rfc.section.4.7.1
+ const from = this.socket?.isSecure() && this.jid?.bare().toString();
+ if (from) headerElement.attrs.from = from;
+ return this.Transport.prototype.header(headerElement, ...args);
}
headerElement(...args) {
diff --git a/packages/client-core/src/bind2/bind2.js b/packages/client-core/src/bind2/bind2.js
index 4fd186bd..fa1ae5c1 100644
--- a/packages/client-core/src/bind2/bind2.js
+++ b/packages/client-core/src/bind2/bind2.js
@@ -2,7 +2,7 @@ import xml from "@xmpp/xml";
const NS_BIND = "urn:xmpp:bind:0";
-export default function bind2({ sasl2 }, tag) {
+export default function bind2({ sasl2, entity }, tag) {
const features = new Map();
sasl2.use(
@@ -22,6 +22,8 @@ export default function bind2({ sasl2 }, tag) {
);
},
(element) => {
+ if (!element.is("bound")) return;
+ entity._ready(false);
for (const child of element.getChildElements()) {
const feature = features.get(child.getNS());
feature?.[1]?.(child);
diff --git a/packages/client-core/test/Client.js b/packages/client-core/test/Client.js
index 728da9e0..e510e9cb 100644
--- a/packages/client-core/test/Client.js
+++ b/packages/client-core/test/Client.js
@@ -1,4 +1,5 @@
import Client from "../lib/Client.js";
+import { JID } from "@xmpp/test";
test("_findTransport", () => {
class Transport {
@@ -21,3 +22,31 @@ test("_findTransport", () => {
expect(entity._findTransport("b")).toBe(undefined);
expect(entity._findTransport("c")).toBe(undefined);
});
+
+test("header", () => {
+ class Transport {
+ header(el) {
+ return el;
+ }
+ }
+
+ const entity = new Client();
+ entity.Transport = Transport;
+ entity.socket = {};
+
+ entity.jid = null;
+ entity.socket.isSecure = () => false;
+ expect(entity.header()).toEqual();
+
+ entity.jid = null;
+ entity.socket.isSecure = () => true;
+ expect(entity.header()).toEqual();
+
+ entity.jid = new JID("foo@bar/example");
+ entity.socket.isSecure = () => false;
+ expect(entity.header()).toEqual();
+
+ entity.jid = new JID("foo@bar/example");
+ entity.socket.isSecure = () => true;
+ expect(entity.header()).toEqual();
+});
diff --git a/packages/client/example.js b/packages/client/example.js
index 9bd5f21d..f0b342bd 100644
--- a/packages/client/example.js
+++ b/packages/client/example.js
@@ -8,6 +8,7 @@ import debug from "@xmpp/debug";
const xmpp = client({
service: "ws://localhost:5280/xmpp-websocket",
// service: "xmpps://localhost:5223",
+ // service: "xmpp://localhost:5222",
domain: "localhost",
resource: "example",
username: "username",
diff --git a/packages/client/index.js b/packages/client/index.js
index 14bcb46c..0d5c1e5b 100644
--- a/packages/client/index.js
+++ b/packages/client/index.js
@@ -15,7 +15,6 @@ import _starttls from "@xmpp/starttls";
import _sasl2 from "@xmpp/sasl2";
import _sasl from "@xmpp/sasl";
import _resourceBinding from "@xmpp/resource-binding";
-import _sessionEstablishment from "@xmpp/session-establishment";
import _streamManagement from "@xmpp/stream-management";
import _bind2 from "@xmpp/client-core/src/bind2/bind2.js";
@@ -34,6 +33,9 @@ function client(options = {}) {
}
const entity = new Client(params);
+ if (username && params.domain) {
+ entity.jid = jid(username, params.domain);
+ }
const reconnect = _reconnect({ entity });
const websocket = _websocket({ entity });
@@ -62,7 +64,7 @@ function client(options = {}) {
);
// SASL2 inline features
- const bind2 = _bind2({ sasl2 }, resource);
+ const bind2 = _bind2({ sasl2, entity }, resource);
// Stream features - order matters and define priority
const sasl = _sasl(
@@ -80,10 +82,6 @@ function client(options = {}) {
{ iqCaller, streamFeatures },
resource,
);
- const sessionEstablishment = _sessionEstablishment({
- iqCaller,
- streamFeatures,
- });
return Object.assign(entity, {
entity,
@@ -101,7 +99,6 @@ function client(options = {}) {
sasl2,
sasl,
resourceBinding,
- sessionEstablishment,
streamManagement,
mechanisms,
bind2,
diff --git a/packages/component-core/lib/Component.js b/packages/component-core/lib/Component.js
index 6f3cd284..fded6354 100644
--- a/packages/component-core/lib/Component.js
+++ b/packages/component-core/lib/Component.js
@@ -37,7 +37,7 @@ class Component extends Connection {
}
this._jid(this.options.domain);
- this._status("online", this.jid);
+ this._ready(false);
}
}
diff --git a/packages/connection-tcp/Socket.js b/packages/connection-tcp/Socket.js
new file mode 100644
index 00000000..7fd8d457
--- /dev/null
+++ b/packages/connection-tcp/Socket.js
@@ -0,0 +1,7 @@
+import { Socket as TCPSocket } from "net";
+
+export default class Socket extends TCPSocket {
+ isSecure() {
+ return false;
+ }
+}
diff --git a/packages/connection-tcp/index.js b/packages/connection-tcp/index.js
index 398a0188..9c4e4085 100644
--- a/packages/connection-tcp/index.js
+++ b/packages/connection-tcp/index.js
@@ -1,4 +1,4 @@
-import { Socket } from "net";
+import Socket from "./Socket.js";
import Connection from "@xmpp/connection";
import { Parser } from "@xmpp/xml";
import { parseURI } from "@xmpp/connection/lib/util.js";
diff --git a/packages/connection-tcp/test/Connection.js b/packages/connection-tcp/test/Connection.js
index fc95f0fe..9ffcfe46 100644
--- a/packages/connection-tcp/test/Connection.js
+++ b/packages/connection-tcp/test/Connection.js
@@ -1,7 +1,7 @@
import _Connection from "@xmpp/connection";
import Connection from "../index.js";
+import Socket from "../Socket.js";
-import net from "net";
import xml from "@xmpp/xml";
const NS_STREAM = "http://etherx.jabber.org/streams";
@@ -14,7 +14,7 @@ test("new Connection()", () => {
test("Socket", () => {
const conn = new Connection();
- expect(conn.Socket).toBe(net.Socket);
+ expect(conn.Socket).toBe(Socket);
});
test("NS", () => {
diff --git a/packages/connection-tcp/test/Socket.js b/packages/connection-tcp/test/Socket.js
new file mode 100644
index 00000000..18177858
--- /dev/null
+++ b/packages/connection-tcp/test/Socket.js
@@ -0,0 +1,12 @@
+import net from "node:net";
+import Socket from "../Socket.js";
+
+test("isSecure()", () => {
+ const socket = new Socket();
+ expect(socket.isSecure()).toBe(false);
+});
+
+test("instance of net.Socket", () => {
+ const socket = new Socket();
+ expect(socket).toBeInstanceOf(net.Socket);
+});
diff --git a/packages/connection/index.js b/packages/connection/index.js
index 5ab1a714..881ac528 100644
--- a/packages/connection/index.js
+++ b/packages/connection/index.js
@@ -22,10 +22,10 @@ class Connection extends EventEmitter {
}
_reset() {
- this.jid = null;
this.status = "offline";
this._detachSocket();
this._detachParser();
+ this.root = null;
}
async _streamError(condition, children) {
@@ -184,6 +184,14 @@ class Connection extends EventEmitter {
this.emit(status, ...args);
}
+ _ready(resumed = false) {
+ if (resumed) {
+ this.status = "online";
+ } else {
+ this._status("online", this.jid);
+ }
+ }
+
async _end() {
let el;
try {
@@ -272,6 +280,7 @@ class Connection extends EventEmitter {
*/
async stop() {
const el = await this._end();
+ this.jid = null;
if (this.status !== "offline") this._status("offline", el);
return el;
}
diff --git a/packages/connection/test/close.js b/packages/connection/test/close.js
index 34441a75..30c9b24d 100644
--- a/packages/connection/test/close.js
+++ b/packages/connection/test/close.js
@@ -1,15 +1,13 @@
import Connection from "../index.js";
import { EventEmitter, promise, timeout, TimeoutError } from "@xmpp/events";
-import xml from "@xmpp/xml";
+import { xml } from "@xmpp/test";
test("resets properties on socket close event", () => {
const conn = new Connection();
conn._attachSocket(new EventEmitter());
- conn.jid = {};
conn.status = "online";
conn.socket.emit("connect");
conn.socket.emit("close");
- expect(conn.jid).toBe(null);
expect(conn.status).toBe("disconnect");
});
diff --git a/packages/connection/test/stop.js b/packages/connection/test/stop.js
index 5057068b..59eb456d 100644
--- a/packages/connection/test/stop.js
+++ b/packages/connection/test/stop.js
@@ -1,4 +1,5 @@
import Connection from "../index.js";
+import { JID } from "@xmpp/test";
test("resolves if socket property is undefined", async () => {
const conn = new Connection();
@@ -38,3 +39,12 @@ test("does not throw if connection is not established", async () => {
await conn.stop();
expect().pass();
});
+
+test("resets jid", async () => {
+ const conn = new Connection();
+ conn.jid = new JID("foo@bar");
+
+ expect(conn.jid).not.toEqual(null);
+ await conn.stop();
+ expect(conn.jid).toEqual(null);
+});
diff --git a/packages/resource-binding/index.js b/packages/resource-binding/index.js
index 5324db22..5b9352ff 100644
--- a/packages/resource-binding/index.js
+++ b/packages/resource-binding/index.js
@@ -15,6 +15,7 @@ async function bind(entity, iqCaller, resource) {
const result = await iqCaller.set(makeBindElement(resource));
const jid = result.getChildText("jid");
entity._jid(jid);
+ entity._ready(false);
return jid;
}
diff --git a/packages/sasl-anonymous/package.json b/packages/sasl-anonymous/package.json
index 87a5b10d..00fbc57c 100644
--- a/packages/sasl-anonymous/package.json
+++ b/packages/sasl-anonymous/package.json
@@ -10,8 +10,7 @@
"main": "index.js",
"keywords": [
"XMPP",
- "sasl",
- "anonymous"
+ "sasl"
],
"dependencies": {
"sasl-anonymous": "^0.1.0"
diff --git a/packages/sasl-plain/package.json b/packages/sasl-plain/package.json
index 87c9c0cf..9b401b8f 100644
--- a/packages/sasl-plain/package.json
+++ b/packages/sasl-plain/package.json
@@ -10,8 +10,7 @@
"main": "index.js",
"keywords": [
"XMPP",
- "sasl",
- "plain"
+ "sasl"
],
"dependencies": {
"sasl-plain": "^0.1.0"
diff --git a/packages/sasl-scram-sha-1/package.json b/packages/sasl-scram-sha-1/package.json
index a5990455..17ee55c0 100644
--- a/packages/sasl-scram-sha-1/package.json
+++ b/packages/sasl-scram-sha-1/package.json
@@ -10,8 +10,7 @@
"main": "index.js",
"keywords": [
"XMPP",
- "sasl",
- "plain"
+ "sasl"
],
"dependencies": {
"sasl-scram-sha-1": "^1.3.0"
diff --git a/packages/sasl/test.js b/packages/sasl/test.js
index 5147a99d..86513b56 100644
--- a/packages/sasl/test.js
+++ b/packages/sasl/test.js
@@ -43,14 +43,13 @@ test("with object credentials", async () => {
);
entity.mockInput();
-
- await promise(entity, "online");
});
test("with function credentials", async () => {
expect.assertions(2);
const mech = "PLAIN";
+ let promise_authenticate;
async function onAuthenticate(authenticate, mechanisms) {
expect(mechanisms).toEqual([mech]);
@@ -79,7 +78,7 @@ test("with function credentials", async () => {
entity.mockInput();
- await promise(entity, "online");
+ await promise_authenticate;
});
test("Mechanism not found", async () => {
diff --git a/packages/sasl2/index.js b/packages/sasl2/index.js
index 29c237be..1b93b46f 100644
--- a/packages/sasl2/index.js
+++ b/packages/sasl2/index.js
@@ -72,9 +72,11 @@ async function authenticate({
}
// https://xmpp.org/extensions/xep-0388.html#success
- // this is a bare JID, unless resource binding has occurred, in which case it is a full JID.
+ // this is a bare JID, unless resource binding or stream resumption has occurred, in which case it is a full JID.
const aid = element.getChildText("authorization-identifier");
- if (aid) entity._jid(aid);
+ if (aid) {
+ entity._jid(aid);
+ }
for (const child of element.getChildElements()) {
const feature = features.get(child.getNS());
@@ -132,8 +134,6 @@ export default function sasl2({ streamFeatures, saslFactory }, onAuthenticate) {
}
await onAuthenticate(done, intersection);
- // Not online yet, wait for next features
- return true;
},
);
diff --git a/packages/sasl2/test.js b/packages/sasl2/test.js
index 0a7e696c..b2042769 100644
--- a/packages/sasl2/test.js
+++ b/packages/sasl2/test.js
@@ -37,10 +37,17 @@ test("with object credentials", async () => {
,
);
- entity.mockInput();
- entity.mockInput();
+ const jid = "username@localhost/example~Ln8YSSzsyK-b_3-vIFvOJNnE";
- await promise(entity, "online");
+ expect(entity.jid?.toString()).not.toBe(jid);
+
+ entity.mockInput(
+
+ {jid}
+ ,
+ );
+
+ expect(entity.jid.toString()).toBe(jid);
});
test("with function credentials", async () => {
@@ -74,10 +81,17 @@ test("with function credentials", async () => {
,
);
- entity.mockInput();
- entity.mockInput();
+ const jid = "username@localhost/example~Ln8YSSzsyK-b_3-vIFvOJNnE";
+
+ expect(entity.jid?.toString()).not.toBe(jid);
+
+ entity.mockInput(
+
+ {jid}
+ ,
+ );
- await promise(entity, "online");
+ expect(entity.jid.toString()).toBe(jid);
});
test("failure", async () => {
diff --git a/packages/session-establishment/test.js b/packages/session-establishment/test.js
index 5418ace1..5ca535f9 100644
--- a/packages/session-establishment/test.js
+++ b/packages/session-establishment/test.js
@@ -1,7 +1,9 @@
import { mockClient, promise, timeout } from "@xmpp/test";
+import sessionEstablishment from "./index.js";
test("mandatory", async () => {
const { entity } = mockClient();
+ sessionEstablishment(entity);
entity.mockInput(
@@ -15,12 +17,11 @@ test("mandatory", async () => {
expect(child).toEqual(
,
);
-
- await promise(entity, "online");
});
test("optional", async () => {
const { entity } = mockClient();
+ sessionEstablishment(entity);
entity.mockInput(
@@ -32,8 +33,6 @@ test("optional", async () => {
const promiseSend = promise(entity, "send");
- await promise(entity, "online");
-
await timeout(promiseSend, 0).catch((err) => {
expect(err.name).toBe("TimeoutError");
});
diff --git a/packages/stream-features/index.js b/packages/stream-features/index.js
index bf41a2db..0ffc79be 100644
--- a/packages/stream-features/index.js
+++ b/packages/stream-features/index.js
@@ -1,5 +1,3 @@
-import route from "./route.js";
-
/**
* References
* https://xmpp.org/rfcs/rfc6120.html#streams-negotiation Stream Negotiation
@@ -8,8 +6,6 @@ import route from "./route.js";
*/
export default function streamFeatures({ middleware }) {
- middleware.use(route());
-
function use(name, xmlns, handler) {
return middleware.use((ctx, next) => {
const { stanza } = ctx;
diff --git a/packages/stream-features/route.js b/packages/stream-features/route.js
deleted file mode 100644
index 0aed4f5c..00000000
--- a/packages/stream-features/route.js
+++ /dev/null
@@ -1,12 +0,0 @@
-export default function route() {
- return async ({ stanza, entity }, next) => {
- if (!stanza.is("features", "http://etherx.jabber.org/streams"))
- return next();
-
- // FIXME: instead of this prevent mechanism
- // emit online once all stream features have negotiated
- // and if entity.jid is set
- const prevent = await next();
- if (!prevent && entity.jid) entity._status("online", entity.jid);
- };
-}
diff --git a/packages/stream-management/index.js b/packages/stream-management/index.js
index 1cabd870..74fb74eb 100644
--- a/packages/stream-management/index.js
+++ b/packages/stream-management/index.js
@@ -63,12 +63,9 @@ export default function streamManagement({
max: null,
};
- let address = null;
-
function resumed() {
sm.enabled = true;
- if (address) entity.jid = address;
- entity.status = "online";
+ entity._ready(true);
}
function failed() {
@@ -83,8 +80,7 @@ export default function streamManagement({
sm.max = max;
}
- entity.on("online", (jid) => {
- address = jid;
+ entity.on("online", () => {
sm.outbound = 0;
sm.inbound = 0;
});
@@ -147,7 +143,7 @@ function setupStreamFeature({
try {
await resume(entity, sm);
resumed();
- return true;
+ return;
// If resumption fails, continue with session establishment
} catch {
failed();
diff --git a/packages/tls/lib/Socket.js b/packages/tls/lib/Socket.js
index 0f0c5ad4..1e3f2a64 100644
--- a/packages/tls/lib/Socket.js
+++ b/packages/tls/lib/Socket.js
@@ -8,6 +8,10 @@ class Socket extends EventEmitter {
this.timeout = null;
}
+ isSecure() {
+ return true;
+ }
+
connect(...args) {
this._attachSocket(tls.connect(...args));
}
diff --git a/packages/websocket/lib/Socket.js b/packages/websocket/lib/Socket.js
index 171e7b0a..018e09d6 100644
--- a/packages/websocket/lib/Socket.js
+++ b/packages/websocket/lib/Socket.js
@@ -1,5 +1,6 @@
import WS from "ws";
import { EventEmitter } from "@xmpp/events";
+import { parseURI } from "@xmpp/connection/lib/util.js";
// eslint-disable-next-line n/no-unsupported-features/node-builtins
const WebSocket = globalThis.WebSocket || WS;
@@ -12,6 +13,14 @@ export default class Socket extends EventEmitter {
this.listeners = Object.create(null);
}
+ isSecure() {
+ if (!this.url) return false;
+ const uri = parseURI(this.url);
+ if (uri.protocol === "wss:") return true;
+ if (["localhost", "127.0.0.1", "::1"].includes(uri.hostname)) return true;
+ return false;
+ }
+
connect(url) {
this.url = url;
this._attachSocket(new WebSocket(url, ["xmpp"]));
diff --git a/packages/websocket/test/Socket.js b/packages/websocket/test/Socket.js
new file mode 100644
index 00000000..9f8d4c5d
--- /dev/null
+++ b/packages/websocket/test/Socket.js
@@ -0,0 +1,21 @@
+import Socket from "../lib/Socket.js";
+
+test("isSecure", () => {
+ const socket = new Socket();
+ expect(socket.isSecure()).toBe(false);
+
+ socket.url = "ws://example.com/foo";
+ expect(socket.isSecure()).toBe(false);
+
+ socket.url = "ws://localhost/foo";
+ expect(socket.isSecure()).toBe(true);
+
+ socket.url = "ws://127.0.0.1/foo";
+ expect(socket.isSecure()).toBe(true);
+
+ socket.url = "ws://[::1]/foo";
+ expect(socket.isSecure()).toBe(true);
+
+ socket.url = "wss://example.com/foo";
+ expect(socket.isSecure()).toBe(true);
+});