diff --git a/packages/client/README.md b/packages/client/README.md index 0c4fef356..dd688b6b1 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -2,17 +2,33 @@ An XMPP client is an entity that connects to an XMPP server. -`@xmpp/client` package includes a minimal set of features to connect /authenticate securely and reliably. +`@xmpp/client` package includes a minimal set of features to connect and authenticate securely and reliably. + +It supports Node.js, browser and React Native. See [Connection Method](#connection-methods) for differences. ## Install `npm install @xmpp/client` or `yarn add @xmpp/client` -## Example +## Setup ```js const {client, xml, jid} = require('@xmpp/client') +``` + +or + +```html + +``` +```js +const {client, xml, jid} = window.XMPP +``` + +## Example + +```js const xmpp = client({ service: 'ws://localhost:5280/xmpp-websocket', domain: 'localhost', @@ -63,7 +79,7 @@ See [jid package](/packages/jid) - `service` `` The service to connect to, accepts an URI or a domain. - `domain` lookup and connect to the most secure endpoint using [@xmpp/resolve](/packages/resolve) - - `xmpp://hostname:port` plain TCP, can be upgraded to TLS using [@xmpp/starttls](/packages/starttls) + - `xmpp://hostname:port` plain TCP, may be upgraded to TLS by [@xmpp/starttls](/packages/starttls) - `xmpps://hostname:port` direct TLS - `ws://hostname:port/path` plain WebSocket - `wss://hostname:port/path` secure WebSocket @@ -163,3 +179,28 @@ xmpp.send(xml('presence')) ### xmpp.reconnect See [@xmpp/reconnect](/packages/reconnect). + +## Connection methods + +XMPP supports multiple transports, this table list `@xmpp/client` supported and unsupported transport for each environment. + +| transport | protocols | Node.js | Browser | React Native | +| :------------------------------: | :-----------: | :-----: | :-----: | :----------: | +| [WebSocket](/packages/websocket) | ws://, wss:// | ✔ | ✔ | ✔ | +| [TCP](/packages/tcp) | xmpp:// | ✔ | ✗ | ✗ | +| [TLS](/packages/tls) | xmpps:// | ✔ | ✗ | ✗ | + +## Authentication methods + +Multiple authentication mechanisms are supported. +PLAIN should only be used over secure WebSocket (`wss://)`, direct TLS (`xmpps:`) or a TCP (`xmpp:`) connection upgraded to TLS via [STARTTLS](/starttls) + +| SASL | Node.js | Browser | React Native | +| :---------------------------------------: | :-----: | :-----: | :----------: | +| [ANONYMOUS](/packages/sasl-anonymous) | ✔ | ✔ | ✔ | +| [PLAIN](/packages/sasl-plain) | ✔ | ✔ | ✔ | +| [SCRAM-SHA-1](/packages/sasl-scram-sha-1) | ✔ | ☐ | ✗ | + +- ☐ : Optional +- ✗ : Unavailable +- ✔ : Included diff --git a/packages/client/browser.js b/packages/client/browser.js new file mode 100644 index 000000000..18d9d1338 --- /dev/null +++ b/packages/client/browser.js @@ -0,0 +1,67 @@ +'use strict' + +const {xml, jid, Client} = require('@xmpp/client-core') +const getDomain = require('./lib/getDomain') + +const _reconnect = require('@xmpp/reconnect') +const _websocket = require('@xmpp/websocket') +const _middleware = require('@xmpp/middleware') +const _streamFeatures = require('@xmpp/stream-features') +const _iqCaller = require('@xmpp/iq/caller') +const _iqCallee = require('@xmpp/iq/callee') +const _resolve = require('@xmpp/resolve') + +// Stream features - order matters and define priority +const _sasl = require('@xmpp/sasl') +const _resourceBinding = require('@xmpp/resource-binding') +const _sessionEstablishment = require('@xmpp/session-establishment') + +// SASL mechanisms - order matters and define priority +const anonymous = require('@xmpp/sasl-anonymous') +const plain = require('@xmpp/sasl-plain') + +function client(options = {}) { + const {resource, credentials, username, password, ...params} = options + + const {domain, service} = params + if (!domain && service) { + params.domain = getDomain(service) + } + + const entity = new Client(params) + + const reconnect = _reconnect({entity}) + const websocket = _websocket({entity}) + + const middleware = _middleware({entity}) + const streamFeatures = _streamFeatures({middleware}) + const iqCaller = _iqCaller({middleware, entity}) + const iqCallee = _iqCallee({middleware, entity}) + const resolve = _resolve({entity}) + // Stream features - order matters and define priority + const sasl = _sasl({streamFeatures}, credentials || {username, password}) + const resourceBinding = _resourceBinding({iqCaller, streamFeatures}, resource) + const sessionEstablishment = _sessionEstablishment({iqCaller, streamFeatures}) + // SASL mechanisms - order matters and define priority + const mechanisms = Object.entries({anonymous, plain}) + .map(([k, v]) => ({[k]: v(sasl)})) + + return Object.assign(entity, { + entity, + reconnect, + websocket, + middleware, + streamFeatures, + iqCaller, + iqCallee, + resolve, + sasl, + resourceBinding, + sessionEstablishment, + mechanisms, + }) +} + +module.exports.xml = xml +module.exports.jid = jid +module.exports.client = client diff --git a/packages/client/index.js b/packages/client/index.js index b0e4875ce..2cf0c94ea 100644 --- a/packages/client/index.js +++ b/packages/client/index.js @@ -1,6 +1,7 @@ 'use strict' const {xml, jid, Client} = require('@xmpp/client-core') +const getDomain = require('./lib/getDomain') const _reconnect = require('@xmpp/reconnect') const _websocket = require('@xmpp/websocket') @@ -23,19 +24,6 @@ const anonymous = require('@xmpp/sasl-anonymous') const scramsha1 = require('@xmpp/sasl-scram-sha-1') const plain = require('@xmpp/sasl-plain') -const URL = global.URL || require('url').URL // eslint-disable-line node/no-unsupported-features/node-builtins - -function getDomain(service) { - // WHATWG URL parser requires a protocol - if (!service.includes('://')) { - service = 'http://' + service - } - const url = new URL(service) - // WHATWG URL parser doesn't support non Web protocols in browser - url.protocol = 'http:' - return url.hostname -} - function client(options = {}) { const {resource, credentials, username, password, ...params} = options @@ -48,8 +36,8 @@ function client(options = {}) { const reconnect = _reconnect({entity}) const websocket = _websocket({entity}) - const tcp = typeof _tcp === 'function' ? _tcp({entity}) : undefined - const tls = typeof _tls === 'function' ? _tls({entity}) : undefined + const tcp = _tcp({entity}) + const tls = _tls({entity}) const middleware = _middleware({entity}) const streamFeatures = _streamFeatures({middleware}) @@ -57,15 +45,12 @@ function client(options = {}) { const iqCallee = _iqCallee({middleware, entity}) const resolve = _resolve({entity}) // Stream features - order matters and define priority - const starttls = - typeof _starttls === 'function' ? _starttls({streamFeatures}) : undefined + const starttls = _starttls({streamFeatures}) const sasl = _sasl({streamFeatures}, credentials || {username, password}) const resourceBinding = _resourceBinding({iqCaller, streamFeatures}, resource) const sessionEstablishment = _sessionEstablishment({iqCaller, streamFeatures}) // SASL mechanisms - order matters and define priority const mechanisms = Object.entries({anonymous, scramsha1, plain}) - // Ignore browserify stubs - .filter(([, v]) => typeof v === 'function') .map(([k, v]) => ({[k]: v(sasl)})) return Object.assign(entity, { @@ -90,4 +75,3 @@ function client(options = {}) { module.exports.xml = xml module.exports.jid = jid module.exports.client = client -module.exports.getDomain = getDomain diff --git a/packages/client/lib/getDomain.js b/packages/client/lib/getDomain.js new file mode 100644 index 000000000..c41c1a4a0 --- /dev/null +++ b/packages/client/lib/getDomain.js @@ -0,0 +1,6 @@ +'use strict' + +module.exports = function getDomain(service) { + const domain = service.split('://')[1] || service + return domain.split(':')[0].split('/')[0] +} diff --git a/packages/client/package.json b/packages/client/package.json index 092cf6409..bd93e96c7 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -23,13 +23,8 @@ "@xmpp/tls": "^0.5.0", "@xmpp/websocket": "^0.5.0" }, - "browser": { - "url": false, - "@xmpp/tcp": false, - "@xmpp/tls": false, - "@xmpp/starttls": false, - "@xmpp/sasl-scram-sha-1": false - }, + "browser": "./browser.js", + "react-native": "./browser.js", "engines": { "node": ">= 10.0.0", "yarn": ">= 1.0.0" diff --git a/packages/client/test/getDomain.js b/packages/client/test/getDomain.js index 939b7d471..b0cff01f7 100644 --- a/packages/client/test/getDomain.js +++ b/packages/client/test/getDomain.js @@ -1,7 +1,7 @@ 'use strict' const test = require('ava') -const {getDomain} = require('..') +const getDomain = require('../lib/getDomain') test('getDomain', t => { t.is(getDomain('ws://foo:123/foobar'), 'foo') diff --git a/packages/events/package.json b/packages/events/package.json index 51b1feda5..05f9bf32e 100644 --- a/packages/events/package.json +++ b/packages/events/package.json @@ -18,5 +18,8 @@ }, "publishConfig": { "access": "public" + }, + "dependencies": { + "events": "^3.0.0" } } diff --git a/packages/reconnect/index.js b/packages/reconnect/index.js index 4db2a38ea..a533b19c5 100644 --- a/packages/reconnect/index.js +++ b/packages/reconnect/index.js @@ -1,6 +1,6 @@ 'use strict' -const EventEmitter = require('events') +const {EventEmitter} = require('@xmpp/events') class Reconnect extends EventEmitter { constructor(entity) { diff --git a/packages/reconnect/package.json b/packages/reconnect/package.json index 529305639..e6ac17419 100644 --- a/packages/reconnect/package.json +++ b/packages/reconnect/package.json @@ -10,6 +10,9 @@ "XMPP", "reconnect" ], + "dependencies": { + "@xmpp/events": "^0.5.0" + }, "engines": { "node": ">= 10.0.0", "yarn": ">= 1.0.0" diff --git a/packages/sasl/lib/b64.js b/packages/sasl/lib/b64.js index 00ebac742..8bc911e83 100644 --- a/packages/sasl/lib/b64.js +++ b/packages/sasl/lib/b64.js @@ -1,15 +1,27 @@ 'use strict' +const {Base64} = require('js-base64') + module.exports.encode = function encode(string) { - if (!global.Buffer) { + if (global.btoa) { return global.btoa(string) } - return Buffer.from(string, 'utf8').toString('base64') + + if (global.Buffer) { + return Buffer.from(string, 'utf8').toString('base64') + } + + return Base64.btoa(string) } module.exports.decode = function decode(string) { - if (!global.Buffer) { + if (global.atob) { return global.atob(string) } - return Buffer.from(string, 'base64').toString('utf8') + + if (global.Buffer) { + return Buffer.from(string, 'base64').toString('utf8') + } + + return Base64.btoa(string) } diff --git a/packages/sasl/package.json b/packages/sasl/package.json index 49b8da91d..70098dc3d 100644 --- a/packages/sasl/package.json +++ b/packages/sasl/package.json @@ -13,10 +13,12 @@ "dependencies": { "@xmpp/error": "^0.5.0", "@xmpp/xml": "^0.5.0", + "js-base64": "^2.4.9", "saslmechanisms": "^0.1.1" }, "browser": { - "buffer": false + "buffer": false, + "js-base64": false }, "engines": { "node": ">= 10.0.0", diff --git a/packages/starttls/README.md b/packages/starttls/README.md index 00add7e84..e4bea4bc6 100644 --- a/packages/starttls/README.md +++ b/packages/starttls/README.md @@ -4,7 +4,7 @@ STARTTLS negotiation for `@xmpp/client`. Included and enabled in `@xmpp/client` for Node.js -STARTTLS will automatically be negotiated upon TCP connection. +STARTTLS will automatically upgrade the TCP connection to TLS upon connecton if the server supports it. ## References diff --git a/packages/tcp/README.md b/packages/tcp/README.md new file mode 100644 index 000000000..17dae0459 --- /dev/null +++ b/packages/tcp/README.md @@ -0,0 +1,5 @@ +# TCP + +TCP transport for `@xmpp/client`. + +Included and enabled in `@xmpp/client` for Node.js. diff --git a/packages/tls/README.md b/packages/tls/README.md new file mode 100644 index 000000000..5063b6abc --- /dev/null +++ b/packages/tls/README.md @@ -0,0 +1,5 @@ +# TLS + +TLS transport for `@xmpp/client`. + +Included and enabled in `@xmpp/client` for Node.js. diff --git a/packages/websocket/README.md b/packages/websocket/README.md new file mode 100644 index 000000000..42842ee96 --- /dev/null +++ b/packages/websocket/README.md @@ -0,0 +1,5 @@ +# WebSocket + +WebSocket transport for `@xmpp/client`. + +Included and enabled in `@xmpp/client`. diff --git a/yarn.lock b/yarn.lock index c1f22e6b1..635d0c13a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3675,6 +3675,11 @@ events@^2.0.0: resolved "https://registry.yarnpkg.com/events/-/events-2.1.0.tgz#2a9a1e18e6106e0e812aa9ebd4a819b3c29c0ba5" integrity sha512-3Zmiobend8P9DjmKAty0Era4jV8oJ0yGYe2nJJAxgymF9+N8F2m0hhZiMoWtcfepExzNKZumFU3ksdQbInGWCg== +events@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.0.0.tgz#9a0a0dfaf62893d92b875b8f2698ca4114973e88" + integrity sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA== + evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" @@ -5006,6 +5011,11 @@ isstream@~0.1.2: resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= +js-base64@^2.4.9: + version "2.4.9" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.9.tgz#748911fb04f48a60c4771b375cac45a80df11c03" + integrity sha512-xcinL3AuDJk7VSzsHgb9DvvIXayBbadtMZ4HFPx8rUszbW1MuNMlwYVC4zzCZ6e1sqZpnNS5ZFYOhXqA39T7LQ== + js-levenshtein@^1.1.3: version "1.1.4" resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.4.tgz#3a56e3cbf589ca0081eb22cd9ba0b1290a16d26e"