├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package.json └── packages ├── xep-0013-fomr ├── index.js └── package.json ├── xep-0047-ibb ├── index.js └── package.json ├── xep-0065-socks5 ├── index.js └── package.json ├── xep-0085-chatstate ├── index.js └── package.json ├── xep-0096-si ├── index.js └── package.json ├── xep-0184-mdr ├── index.js └── package.json └── xep-0384-omemo ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | 3 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2019, Telldus Technologies AB 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, 6 | provided that the above copyright notice and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED “AS IS” AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED 9 | WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL 10 | DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 11 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xmppjs-client-plugins 2 | 3 | Set of plugins for [xmppjs](https://github.com/xmppjs/xmpp.js) 4 | 5 | * [XEP-0013 - Flexible Offline Message Retrieval](https://xmpp.org/extensions/xep-0013.html)(v1.2) 6 | * [XEP-0047 - In-Band Bytestreams](https://xmpp.org/extensions/xep-0047.html)(v2.0) 7 | * [XEP-0065 - SOCKS5](https://xmpp.org/extensions/xep-0065.html)(v1.8.1) 8 | * [XEP-0085 - Chat State Notification](https://xmpp.org/extensions/xep-0085.html)(v2.1) 9 | * [XEP-0096 - SI File Transfer](https://xmpp.org/extensions/xep-0096.html)(v1.3) 10 | * [XEP-0184 - Message Delivery Receipts](https://xmpp.org/extensions/xep-0184.html)(v1.4.0) 11 | * [XEP-0384 - OMEMO](https://xmpp.org/extensions/xep-0384.html)(v0.3.0) 12 | 13 | 14 | ## Usage: 15 | 16 | #### OMEMO [OUTDATED - There has been some additions and namespace changes in the protocol] 17 | 18 | ```javascript 19 | 20 | import { 21 | client, 22 | } from "@xmpp/client"; 23 | import { 24 | setupOMEMO, 25 | } from 'xmppjs-client-plugins'; 26 | 27 | 28 | // Set up the plugin 29 | const xmpp = client({service: 'wss://xmpp.example.com'}); 30 | const omemoPlugin = setupOMEMO(xmpp); 31 | 32 | // Announce OMEMO support, and publish the current/latest devices list and each device's bundle info 33 | OMEMOPlugin.announceOMEMOSupport(myDevicesArray, myBareJid) 34 | OMEMOPlugin.announceBundleInfo(payload, deviceId, bareJid).then((res: any): any => { 35 | }).catch((err: any): any => { 36 | }); 37 | 38 | // Set listener 39 | OMEMOPlugin.on('omemo.devicelist', (deviceList: Object) => { 40 | }); 41 | 42 | // Subscribe to contact's devices list update If not already subscribed 43 | OMEMOPlugin.subscribeToDeviceListUpdate(fromFullJid, toBareJid).then((stanza: Object): Object => { 44 | const child = stanza.getChild('pubsub'); 45 | if (!child || stanza.attrs.type !== 'result') { 46 | return stanza; 47 | } 48 | const subscription = child.getChild('subscription'); 49 | if (subscription && subscription.attrs.subscription) { 50 | // Cache subscription status 51 | } 52 | return stanza; 53 | }); 54 | 55 | // Subscribe to contact's device's bundle update If not already subscribed 56 | OMEMOPlugin.subscribeToBundleUpdate(fromFullJid, toBareJid, deviceId).then((stanza: Object): Object => { 57 | const child = stanza.getChild('pubsub'); 58 | if (!child || stanza.attrs.type !== 'result') { 59 | return stanza; 60 | } 61 | const subscription = child.getChild('subscription'); 62 | if (subscription && subscription.attrs.subscription && subscription.attrs.node) { 63 | // Cache subscription status 64 | } 65 | return stanza; 66 | }); 67 | 68 | // Request for contact's devices list on demand 69 | omemoPlugin.requestDeviceList(fromFullJid, toBareJid).then((deviceList: Object) => { 70 | // Cache devices 71 | }).catch((err) => { 72 | }); 73 | 74 | // Request for contact's device's bundle on demand 75 | OMEMOPlugin.requestBundle(bareFromJid, bareToJid, deviceId).then((bundle: Object): any => { 76 | // Cache bundle 77 | }); 78 | 79 | // Send encrypted message 80 | const fromJid = ""; 81 | const toJid = ""; 82 | const chatType = "chat"; 83 | const encryptionPayload = { 84 | ciphertext: "", 85 | iv: "", 86 | keys: [{ 87 | prekey: "", 88 | key: "", 89 | deviceId: "", 90 | }], 91 | }; 92 | const messageId = "unique_messgae_id"; // set as `id` on element 93 | const sid = ""; // `sid` set on
element 94 | const otherElements = [ 95 | xml('active', { 96 | xmlns: "http://jabber.org/protocol/chatstates", 97 | }) 98 | ]; // Any other supported xml elements, say chat state. 99 | 100 | OMEMOPlugin.sendMessage(fromJid, toJid, chatType, encryptionPayload, messageId, sid, otherElements).then(() => { 101 | }); 102 | 103 | // Message reception can be handled inside xmpp client's "on('stanza', async (stanza: any) => {})" listener as usual. Check for "encrypted" child element. 104 | 105 | ``` 106 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import setupIBB from './packages/xep-0047-ibb'; 2 | import setupSI from './packages/xep-0096-si'; 3 | import * as OMEMO from './packages/xep-0384-omemo'; 4 | import setupCSN from './packages/xep-0085-chatstate'; 5 | import setupSOCKS5 from './packages/xep-0065-socks5'; 6 | import * as FOMR from './packages/xep-0013-fomr'; 7 | import * as MDR from './packages/xep-0184-mdr'; 8 | 9 | module.exports = { 10 | setupIBB, 11 | setupSI, 12 | setupCSN, 13 | setupSOCKS5, 14 | ...OMEMO, 15 | ...FOMR, 16 | ...MDR, 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xmppjs-client-plugins", 3 | "description": "plugins for xmpp.js client", 4 | "version": "1.0.0", 5 | "license": "ISC", 6 | "keywords": [ 7 | "XMPP", 8 | "si file transfer", 9 | "in-band bytestream", 10 | "socks5", 11 | "chat state", 12 | "message delivery receipt", 13 | "omemo", 14 | "flexible offline message retrieval" 15 | ], 16 | "engines": { 17 | "node": ">= 10.0.0", 18 | "yarn": ">= 1.0.0" 19 | }, 20 | "publishConfig": { 21 | "access": "public" 22 | } 23 | } -------------------------------------------------------------------------------- /packages/xep-0013-fomr/index.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | const xml = require('@xmpp/xml'); 5 | 6 | const OfflineMessageRetrievalNS = 'http://jabber.org/protocol/offline'; 7 | const DiscoInfoNS = 'http://jabber.org/protocol/disco#info'; 8 | const DiscoItemsNS = 'http://jabber.org/protocol/disco#items'; 9 | 10 | class OfflineMessageRetrievalPlugin { 11 | constructor(client: Object) { 12 | this.client = client; 13 | } 14 | 15 | retrieveAll(): Promise { 16 | const { iqCaller } = this.client; 17 | const req = xml('iq', {type: 'get', id: 'fetchalloffline'}, 18 | xml('offline', {xmlns: OfflineMessageRetrievalNS}, 19 | xml('fetch'), 20 | )); 21 | return iqCaller.request(req); 22 | } 23 | 24 | removeAll(): Promise { 25 | const { iqCaller } = this.client; 26 | const req = xml('iq', {type: 'set', id: 'removealloffline'}, 27 | xml('offline', {xmlns: OfflineMessageRetrievalNS}, 28 | xml('purge'), 29 | )); 30 | return iqCaller.request(req); 31 | } 32 | 33 | requestMessagesCount(): Promise { 34 | const { iqCaller } = this.client; 35 | const req = xml('iq', {type: 'get'}, 36 | xml('query', { 37 | xmlns: DiscoInfoNS, 38 | node: OfflineMessageRetrievalNS} 39 | )); 40 | return iqCaller.request(req); 41 | } 42 | 43 | requestMessageHeader(node: string): Promise { 44 | const { iqCaller } = this.client; 45 | const req = xml('iq', {type: 'get'}, 46 | xml('query', { 47 | xmlns: DiscoItemsNS, 48 | node: OfflineMessageRetrievalNS} 49 | )); 50 | return iqCaller.request(req); 51 | } 52 | 53 | retrieveMessages(node: Array, id: string): Promise { 54 | const items = node.map((n: string): any => { 55 | return xml('item', {action: 'view', node}); 56 | }); 57 | const { iqCaller } = this.client; 58 | const req = xml('iq', {type: 'get', id}, 59 | xml('offline', {xmlns: OfflineMessageRetrievalNS}, 60 | ...items, 61 | )); 62 | return iqCaller.request(req); 63 | } 64 | 65 | removeMessages(node: Array, id: string): Promise { 66 | const items = node.map((n: string): any => { 67 | return xml('item', {action: 'remove', node}); 68 | }); 69 | const { iqCaller } = this.client; 70 | const req = xml('iq', {type: 'set', id}, 71 | xml('offline', {xmlns: OfflineMessageRetrievalNS}, 72 | ...items, 73 | )); 74 | return iqCaller.request(req); 75 | } 76 | } 77 | 78 | /** 79 | * Register a fomr plugin. 80 | * 81 | * @param {Client} client XMPP client instance 82 | * @returns {OfflineMessageRetrievalPlugin} Plugin instance 83 | */ 84 | 85 | function setupFOMR(client: Object): Object { 86 | const plugin = new OfflineMessageRetrievalPlugin(client); 87 | return plugin; 88 | } 89 | 90 | module.exports = { 91 | setupFOMR, 92 | }; 93 | -------------------------------------------------------------------------------- /packages/xep-0013-fomr/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xmppjs-client-plugins", 3 | "description": "offline message retrieval plugin for xmpp.js client", 4 | "version": "1.0.0", 5 | "license": "ISC", 6 | "keywords": [ 7 | "XMPP", 8 | "OMEMO" 9 | ], 10 | "engines": { 11 | "node": ">= 10.0.0", 12 | "yarn": ">= 1.0.0" 13 | }, 14 | "dependencies": { 15 | "@xmpp/events": "^0.7.0", 16 | "@xmpp/xml": "^0.7.0" 17 | }, 18 | "devDependencies": { 19 | "@xmpp/events": "^0.7.0" 20 | }, 21 | "publishConfig": { 22 | "access": "public" 23 | } 24 | } -------------------------------------------------------------------------------- /packages/xep-0047-ibb/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {EventEmitter} = require('@xmpp/events'); 4 | const xml = require('@xmpp/xml'); 5 | 6 | const IBBNS = 'http://jabber.org/protocol/ibb'; 7 | 8 | class IBBPlugin extends EventEmitter { 9 | constructor(client) { 10 | super(); 11 | this.iqCallee = client.iqCallee; 12 | this.iqCaller = client.iqCaller; 13 | this.init(); 14 | } 15 | 16 | init() { 17 | this.iqCallee.set(IBBNS, 'open', ctx => { 18 | return true; 19 | }); 20 | this.iqCallee.set(IBBNS, 'data', ctx => { 21 | this.handleIBBData(ctx); 22 | return true; 23 | }); 24 | } 25 | 26 | sendSessionRequest(from, to, id, sid, blockSize) { 27 | const ibbreq = xml( 28 | 'iq', 29 | { type: 'set', to, id, from }, 30 | xml('open', { 31 | 'xmlns': IBBNS, 32 | 'block-size': blockSize, 33 | sid, 34 | 'stanza': 'iq', 35 | }), 36 | ); 37 | return this.iqCaller 38 | .request(ibbreq); 39 | } 40 | 41 | sendByteStream(from, to, id, sid, rid, blockSize, data, messageGroup, comment) { 42 | return this.sendSessionRequest(from, to, rid, sid, blockSize).then((res) => { 43 | const { from: From, id: ID, type } = res.attrs; 44 | if (From === to && rid === ID && type === 'result') { 45 | return this.sendData(from, to, id, sid, data, messageGroup, comment); 46 | } 47 | throw res; 48 | 49 | }); 50 | } 51 | 52 | sendData(from, to, id, sid, data, messageGroup, comment) { 53 | return this.iqCaller 54 | .request( 55 | xml( 56 | 'iq', 57 | { type: 'set', to, id, from }, 58 | xml('data', { 59 | 'xmlns': IBBNS, 60 | 'seq': '0', 61 | sid, 62 | 'imgcomment': comment, // custom atribute 63 | 'imggroupid': messageGroup, // custom atribute 64 | // TODO: See if there is a better way to pass custom data 65 | // custom xml element is the suggested way, but does not work with iq, unlike message 66 | }, data), 67 | ) 68 | ); 69 | } 70 | // TODO: Implement Closing the Bytestream 71 | 72 | handleIBBData({stanza}) { 73 | const { from } = stanza.attrs; 74 | const data = stanza.getChild('data'); 75 | const { seq, imgcomment, imggroupid } = data.attrs; 76 | this.emit('IBBSuccess', { 77 | data: data.text(), 78 | from, 79 | seq, 80 | imgcomment, 81 | imggroupid, 82 | }); 83 | } 84 | 85 | sendClose() { 86 | } 87 | 88 | receiveClose() { 89 | } 90 | } 91 | 92 | /** 93 | * Register a ibb plugin. 94 | * 95 | * @param {Client} client XMPP client instance 96 | * @returns {IBBPlugin} Plugin instance 97 | */ 98 | 99 | function setupIBB(client) { 100 | return new IBBPlugin(client); 101 | } 102 | 103 | module.exports = setupIBB; 104 | -------------------------------------------------------------------------------- /packages/xep-0047-ibb/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ibb", 3 | "description": "In-band bytestream plugin for xmpp.js", 4 | "version": "1.0.0", 5 | "license": "ISC", 6 | "keywords": [ 7 | "XMPP", 8 | "in-band bytestream" 9 | ], 10 | "engines": { 11 | "node": ">= 10.0.0", 12 | "yarn": ">= 1.0.0" 13 | }, 14 | "dependencies": { 15 | "@xmpp/events": "^0.7.0", 16 | "@xmpp/jid": "^0.7.0", 17 | "@xmpp/xml": "^0.7.0" 18 | }, 19 | "devDependencies": { 20 | "@xmpp/events": "^0.7.0", 21 | "@xmpp/iq": "^0.7.0", 22 | "@xmpp/middleware": "^0.7.0", 23 | "@xmpp/test": "^0.7.2" 24 | }, 25 | "publishConfig": { 26 | "access": "public" 27 | } 28 | } -------------------------------------------------------------------------------- /packages/xep-0065-socks5/index.js: -------------------------------------------------------------------------------- 1 | 2 | // @flow 3 | 'use strict'; 4 | 5 | const xml = require('@xmpp/xml'); 6 | 7 | const SOCKS5NS = 'http://jabber.org/protocol/bytestreams'; 8 | const DiscoItemsNS = 'http://jabber.org/protocol/disco#items'; 9 | const DiscoInfoNS = 'http://jabber.org/protocol/disco#info'; 10 | 11 | class SOCKS5Plugin { 12 | iqCaller: Object; 13 | constructor(client: Object) { 14 | this.iqCaller = client.iqCaller; 15 | } 16 | 17 | serviceDiscoveryRequest(to: string): Promise { 18 | return this.discoInfoRequest(to).then((stanza: Object) => { 19 | this.checkIfSOCKS5IsSupported(stanza); 20 | }); 21 | } 22 | 23 | checkIfSOCKS5IsSupported(stanza: Object) { 24 | const child = stanza.getChild('query'); 25 | if (stanza.attrs.type === 'result' && child) { 26 | const features = child.getChildren('feature'); 27 | features.map((f: Object) => { 28 | if (f.attrs.var === SOCKS5NS) { 29 | // SOCKS5 supported. 30 | } 31 | }); 32 | } 33 | } 34 | 35 | serviceDiscoveryItemsRequest(domain: string): Promise { 36 | return this.discoItemsRequest(domain).then((stanza: Object) => { 37 | const child = stanza.getChild('query'); 38 | if (stanza.attrs.type === 'result' && child) { 39 | const items = child.getChildren('item'); 40 | items.map((item: Object) => { 41 | this.discoInfoRequest(item.attrs.jid).then((stanzaOne: Object) => { 42 | if (this.checkIfProx(stanzaOne)) { 43 | this.getAddressFromProxy(item.attrs.jid).then((stanzaTwo: Object) => { 44 | const { host, port } = this.getHostAndPort(stanzaTwo); 45 | if (host && port) { 46 | // Initiate S5B request. 47 | this.initiateS5BRequest(); 48 | } 49 | }); 50 | } 51 | }); 52 | }); 53 | } 54 | }); 55 | } 56 | 57 | initiateS5BRequest() { 58 | } 59 | 60 | getHostAndPort(stanza: Object): Object { 61 | const child = stanza.getChild('query'); 62 | if (!child) { 63 | return {}; 64 | } 65 | const streamhost = child.getChild('streamhost'); 66 | if (!streamhost) { 67 | return {}; 68 | } 69 | const host = streamhost.attrs.host; 70 | const port = streamhost.attrs.post; 71 | return { host, port }; 72 | } 73 | 74 | checkIfProx(stanza: Object): boolean { 75 | const child = stanza.getChild('query'); 76 | if (stanza.attrs.type === 'result' && child) { 77 | const identity = child.getChild('identity'); 78 | return identity && identity.attrs.category === 'proxy'; 79 | } 80 | return false; 81 | } 82 | 83 | getAddressFromProxy(to: string): Promise { 84 | 85 | const req = xml('iq', 86 | { 87 | type: 'get', 88 | to, 89 | }, 90 | xml('query', { xmlns: SOCKS5NS })); 91 | return this.iqCaller 92 | .request(req); 93 | } 94 | 95 | discoItemsRequest(to: string): Promise { 96 | 97 | const req = xml('iq', 98 | { 99 | type: 'get', 100 | to, 101 | }, 102 | xml('query', { xmlns: DiscoItemsNS })); 103 | return this.iqCaller 104 | .request(req); 105 | } 106 | 107 | discoInfoRequest(to: string): Promise { 108 | 109 | const req = xml('iq', 110 | { 111 | type: 'get', 112 | to, 113 | }, 114 | xml('query', { xmlns: DiscoInfoNS })); 115 | return this.iqCaller 116 | .request(req); 117 | } 118 | } 119 | 120 | /** 121 | * Register a SOCKS5 plugin. 122 | * 123 | * @param {Client} client XMPP client instance 124 | * @returns {SOCKS5Plugin} Plugin instance 125 | */ 126 | function setupSOCKS5(client: Object): Object { 127 | return new SOCKS5Plugin(client); 128 | } 129 | 130 | module.exports = setupSOCKS5; 131 | -------------------------------------------------------------------------------- /packages/xep-0065-socks5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xmppjs-client-plugins", 3 | "description": "SOCKS5 plugin for xmpp.js client", 4 | "version": "1.0.0", 5 | "license": "ISC", 6 | "keywords": [ 7 | "XMPP", 8 | "SOCKS5" 9 | ], 10 | "engines": { 11 | "node": ">= 10.0.0", 12 | "yarn": ">= 1.0.0" 13 | }, 14 | "dependencies": { 15 | "@xmpp/events": "^0.7.0", 16 | "@xmpp/xml": "^0.7.0" 17 | }, 18 | "devDependencies": { 19 | "@xmpp/events": "^0.7.0" 20 | }, 21 | "publishConfig": { 22 | "access": "public" 23 | } 24 | } -------------------------------------------------------------------------------- /packages/xep-0085-chatstate/index.js: -------------------------------------------------------------------------------- 1 | 2 | // @flow 3 | 'use strict'; 4 | const {EventEmitter} = require('@xmpp/events'); 5 | const xml = require('@xmpp/xml'); 6 | 7 | const ChatStateNS = 'http://jabber.org/protocol/chatstates'; 8 | 9 | class ChatStateNotificationsPlugin extends EventEmitter { 10 | constructor(client: Object) { 11 | super(); 12 | this.client = client; 13 | } 14 | 15 | handleChatStateNotification(stanza: Object) { 16 | const { from: jid, type } = stanza.attrs; 17 | const payload = { 18 | jid, 19 | type, 20 | }; 21 | if (type !== 'error') { 22 | if (stanza.getChild('composing')) { 23 | this.emit('chatstate.composing', payload); 24 | } 25 | if (stanza.getChild('paused')) { 26 | this.emit('chatstate.paused', payload); 27 | } 28 | if (stanza.getChild('active')) { 29 | this.emit('chatstate.active', payload); 30 | } 31 | if (stanza.getChild('inactive')) { 32 | this.emit('chatstate.inactive', payload); 33 | } 34 | if (stanza.getChild('gone')) { 35 | this.emit('chatstate.gone', payload); 36 | } 37 | } 38 | } 39 | 40 | sendComposing(jid: string, type: string = 'chat') { 41 | this.sendStateNotification(jid, type, 'composing'); 42 | } 43 | 44 | sendPaused(jid: string, type: string = 'chat') { 45 | this.sendStateNotification(jid, type, 'paused'); 46 | } 47 | 48 | sendActive(jid: string, type: string = 'chat') { 49 | this.sendStateNotification(jid, type, 'active'); 50 | } 51 | 52 | sendInactive(jid: string, type: string = 'chat') { 53 | this.sendStateNotification(jid, type, 'inactive'); 54 | } 55 | 56 | sendGone(jid: string, type: string = 'chat') { 57 | this.sendStateNotification(jid, type, 'gone'); 58 | } 59 | 60 | sendStateNotification(to: string, type: string, state: string) { 61 | const msg = xml('message', { 62 | type, 63 | to, 64 | }, 65 | xml(state, { 66 | 'xmlns': ChatStateNS, 67 | })); 68 | this.client.send(msg); 69 | } 70 | 71 | } 72 | 73 | /** 74 | * Register a ChatStateNotifications plugin. 75 | * 76 | * @param {Client} client XMPP client instance 77 | * @returns {ChatStateNotificationsPlugin} Plugin instance 78 | */ 79 | 80 | function setupCSN(client: Object): Object { 81 | const {middleware} = client; 82 | const plugin = new ChatStateNotificationsPlugin(client); 83 | 84 | middleware.use(({stanza}: Object, next: Function): Function => { 85 | if (stanza.is('message')) { 86 | return plugin.handleChatStateNotification(stanza); 87 | } 88 | 89 | return next(); 90 | }); 91 | 92 | return plugin; 93 | } 94 | 95 | module.exports = setupCSN; 96 | -------------------------------------------------------------------------------- /packages/xep-0085-chatstate/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xmppjs-client-plugins", 3 | "description": "chat state plugin for xmpp.js client", 4 | "version": "1.0.0", 5 | "license": "ISC", 6 | "keywords": [ 7 | "XMPP", 8 | "chat state" 9 | ], 10 | "engines": { 11 | "node": ">= 10.0.0", 12 | "yarn": ">= 1.0.0" 13 | }, 14 | "dependencies": { 15 | "@xmpp/events": "^0.7.0", 16 | "@xmpp/xml": "^0.7.0" 17 | }, 18 | "devDependencies": { 19 | "@xmpp/events": "^0.7.0" 20 | }, 21 | "publishConfig": { 22 | "access": "public" 23 | } 24 | } -------------------------------------------------------------------------------- /packages/xep-0096-si/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {EventEmitter} = require('@xmpp/events'); 4 | const xml = require('@xmpp/xml'); 5 | 6 | const SINS = 'http://jabber.org/protocol/si'; 7 | const SIPNS = 'http://jabber.org/protocol/si/profile/file-transfer'; 8 | const SIFNNS = 'http://jabber.org/protocol/feature-neg'; 9 | const DNS = 'jabber:x:data'; 10 | 11 | const IBBNS = 'http://jabber.org/protocol/ibb'; 12 | 13 | const isSIStreamInitiationRequest = ({stanza}) => { 14 | const child = stanza.getChild('si'); 15 | const feature = child.getChild('feature'); 16 | const check1 = child.attrs.profile === SIPNS && feature.attrs.xmlns === SIFNNS; 17 | 18 | const x = feature.getChild('x'); 19 | if (!x) { 20 | return false; 21 | } 22 | const field = x.getChild('field'); 23 | if (!field) { 24 | return false; 25 | } 26 | const option = field.getChild('option'); 27 | if (!option) { 28 | return false; 29 | } 30 | 31 | const check2 = x.attrs.type === 'form'; 32 | 33 | return check1 && check2; 34 | }; 35 | 36 | const parseValues = stanza => { 37 | const c1 = stanza.getChild('si'); 38 | if (c1 && c1.getChild('feature')) { 39 | return c1.getChild('feature').getChild('x').getChild('field').getChildren('value'); 40 | } 41 | return null; 42 | }; 43 | 44 | class SIPlugin extends EventEmitter { 45 | constructor(client) { 46 | super(); 47 | this.iqCallee = client.iqCallee; 48 | this.iqCaller = client.iqCaller; 49 | this._supportedMethods = []; 50 | this.init(); 51 | } 52 | 53 | init() { 54 | this.iqCallee.set(SINS, 'si', ctx => { 55 | if (isSIStreamInitiationRequest(ctx)) { 56 | return this.onSIStreamInitiationRequest(ctx); 57 | } 58 | return false; 59 | }); 60 | } 61 | 62 | addMethod(method) { 63 | this._supportedMethods.push(method); 64 | } 65 | 66 | send(id, sid, to, fileSize, fileName, mimeType, date, hash) { 67 | 68 | const preferredMethod = this._supportedMethods[0] || IBBNS; 69 | 70 | const req = xml( 71 | 'iq', 72 | { type: 'set', id, to }, 73 | xml('si', 74 | { 75 | 'xmlns': SINS, 76 | 'id': sid, 77 | 'mime-type': mimeType, 78 | 'profile': SIPNS, 79 | }, 80 | xml('file', 81 | { 82 | 'xmlns': SIPNS, 83 | 'name': fileName, 84 | 'size': fileSize, 85 | date, 86 | hash, 87 | }), 88 | xml('feature', 89 | { 90 | 'xmlns': SIFNNS, 91 | }, 92 | xml('x', 93 | { 94 | 'xmlns': DNS, 95 | 'type': 'form', 96 | }, 97 | xml('field', 98 | { 99 | 'var': 'stream-method', 100 | 'type': 'list-single', 101 | }, 102 | xml('option', null, 103 | xml('value', null, preferredMethod), 104 | ), 105 | ), 106 | ), 107 | ), 108 | ), 109 | ); 110 | 111 | return this.iqCaller 112 | .request(req).then(res => { 113 | const values = parseValues(res); 114 | if (values[0] && values[0].text()) { 115 | return res; 116 | } 117 | throw res; 118 | }); 119 | } 120 | 121 | onSIStreamInitiationRequest({stanza}) { 122 | const { from } = stanza.attrs; 123 | this.emit('incomingRequest', {from}); 124 | const options = stanza.getChild('si').getChild('feature').getChild('x').getChild('field').getChildren('option'); 125 | 126 | let preferredMethod = IBBNS; 127 | options.map((option) => { 128 | const value = option.getChildText('value'); 129 | const index = this._supportedMethods.indexOf(value); 130 | if (index !== -1) { 131 | preferredMethod = this._supportedMethods[index]; 132 | } 133 | }); 134 | 135 | const res = xml('si', 136 | { 137 | 'xmlns': SINS, 138 | }, 139 | xml('feature', 140 | { 141 | 'xmlns': SIFNNS, 142 | }, 143 | xml('x', 144 | { 145 | 'xmlns': DNS, 146 | 'type': 'submit', 147 | }, 148 | xml('field', 149 | { 150 | 'var': 'stream-method', 151 | }, 152 | xml('value', null, preferredMethod), 153 | ), 154 | ), 155 | ), 156 | ); 157 | return res; 158 | } 159 | } 160 | 161 | /** 162 | * Register a si plugin. 163 | * 164 | * @param {Client} client XMPP client instance 165 | * @returns {SIPlugin} Plugin instance 166 | */ 167 | function setupSI(client) { 168 | return new SIPlugin(client); 169 | } 170 | 171 | module.exports = setupSI; 172 | -------------------------------------------------------------------------------- /packages/xep-0096-si/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xmppjs-client-plugins", 3 | "description": "si file transfer plugin for xmpp.js client", 4 | "version": "1.0.0", 5 | "license": "ISC", 6 | "keywords": [ 7 | "XMPP", 8 | "si file transfer" 9 | ], 10 | "engines": { 11 | "node": ">= 10.0.0", 12 | "yarn": ">= 1.0.0" 13 | }, 14 | "dependencies": { 15 | "@xmpp/events": "^0.7.0", 16 | "@xmpp/jid": "^0.7.0", 17 | "@xmpp/xml": "^0.7.0" 18 | }, 19 | "devDependencies": { 20 | "@xmpp/events": "^0.7.0", 21 | "@xmpp/iq": "^0.7.0", 22 | "@xmpp/middleware": "^0.7.0", 23 | "@xmpp/test": "^0.7.2" 24 | }, 25 | "publishConfig": { 26 | "access": "public" 27 | } 28 | } -------------------------------------------------------------------------------- /packages/xep-0184-mdr/index.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | const {EventEmitter} = require('@xmpp/events'); 5 | const xml = require('@xmpp/xml'); 6 | 7 | const MDRNS = 'urn:xmpp:receipts'; 8 | 9 | const doesRequestReceipt = (stanza: Object): boolean => { 10 | if (!stanza.is('message')) { 11 | return false; 12 | } 13 | const child = stanza.getChild('request'); 14 | return child && child.attrs.xmlns === MDRNS; 15 | }; 16 | 17 | const isDeliveryReceipt = (stanza: Object): boolean => { 18 | if (!stanza.is('message')) { 19 | return false; 20 | } 21 | const child = stanza.getChild('received'); 22 | return child && child.attrs.xmlns === MDRNS && child.attrs.id; 23 | }; 24 | 25 | class MDRPlugin extends EventEmitter { 26 | constructor(client: Object) { 27 | super(); 28 | this.client = client; 29 | } 30 | 31 | acknowledge(stanza: Object) { 32 | const { id, from } = stanza.attrs; 33 | const message = xml('message', {to: from, id: 'mdr'}, null, 34 | xml('received', {id, xmlns: MDRNS}), 35 | xml('store', {xmlns: 'urn:xmpp:hints'}) 36 | ); 37 | this.client.send(message); 38 | } 39 | 40 | handleReceipt(stanza: Object) { 41 | const child = stanza.getChild('received'); 42 | const { from } = stanza.attrs; 43 | const { id } = child.attrs; 44 | this.emit('mdr.receipt', { 45 | from, 46 | id, 47 | }); 48 | } 49 | } 50 | 51 | /** 52 | * Register a mdr plugin. 53 | * 54 | * @param {Client} client XMPP client instance 55 | * @returns {MDRPlugin} Plugin instance 56 | */ 57 | 58 | function setupMDR(client: Object): Object { 59 | const {middleware} = client; 60 | const plugin = new MDRPlugin(client); 61 | 62 | middleware.use(({stanza}: Object, next: Function): Function => { 63 | if (doesRequestReceipt(stanza)) { 64 | return plugin.acknowledge(stanza); 65 | } 66 | if (isDeliveryReceipt(stanza)) { 67 | return plugin.handleReceipt(stanza); 68 | } 69 | 70 | return next(); 71 | }); 72 | 73 | return plugin; 74 | } 75 | 76 | module.exports = { 77 | setupMDR, 78 | doesRequestReceipt, 79 | isDeliveryReceipt, 80 | }; 81 | -------------------------------------------------------------------------------- /packages/xep-0184-mdr/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xmppjs-client-plugins", 3 | "description": "message delivery receipt plugin for xmpp.js client", 4 | "version": "1.0.0", 5 | "license": "ISC", 6 | "keywords": [ 7 | "XMPP", 8 | "OMEMO" 9 | ], 10 | "engines": { 11 | "node": ">= 10.0.0", 12 | "yarn": ">= 1.0.0" 13 | }, 14 | "dependencies": { 15 | "@xmpp/events": "^0.7.0", 16 | "@xmpp/xml": "^0.7.0" 17 | }, 18 | "devDependencies": { 19 | "@xmpp/events": "^0.7.0" 20 | }, 21 | "publishConfig": { 22 | "access": "public" 23 | } 24 | } -------------------------------------------------------------------------------- /packages/xep-0384-omemo/index.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | const {EventEmitter} = require('@xmpp/events'); 4 | const xml = require('@xmpp/xml'); 5 | 6 | const PubSubEventNS = 'http://jabber.org/protocol/pubsub#event'; 7 | const PubSubNS = 'http://jabber.org/protocol/pubsub'; 8 | const OMEMODeviceListNodeNS = 'eu.siacs.conversations.axolotl.devicelist'; 9 | const OMEMODeviceListNS = 'eu.siacs.conversations.axolotl'; 10 | const ExplicitEncryptionNS = 'urn:xmpp:eme:0'; 11 | 12 | const BundleInfoNode = 'eu.siacs.conversations.axolotl.bundles:'; 13 | 14 | const isOMEMODeviceList = (stanza: Object): boolean => { 15 | const child = stanza.getChild('event') || stanza.getChild('pubsub'); 16 | if (!child) { 17 | return false; 18 | } 19 | const items = child.getChild('items'); 20 | if (!items) { 21 | return false; 22 | } 23 | const item = items.getChild('item'); 24 | if (!item) { 25 | return false; 26 | } 27 | const list = item.getChild('list'); 28 | 29 | return list && (list.attrs.xmlns === OMEMODeviceListNS) && 30 | (child.attrs.xmlns === PubSubEventNS || child.attrs.xmlns === PubSubNS) && 31 | (items.attrs.node === OMEMODeviceListNodeNS); 32 | }; 33 | 34 | const preparePayloadDeviceList = (stanza: Object): Object => { 35 | const { from: jid, type } = stanza.attrs; 36 | const child = stanza.getChild('event') || stanza.getChild('pubsub'); 37 | const items = child.getChild('items'); 38 | const list = items.getChild('item').getChild('list'); 39 | const devices = list.getChildren('device') || []; 40 | 41 | const payload = { 42 | jid, 43 | type, 44 | devices: devices.map((d: Object): number => { 45 | const { id } = d.attrs; 46 | return id; 47 | }), 48 | }; 49 | return payload; 50 | }; 51 | 52 | const isOMEMOBundle = (stanza: Object): boolean => { 53 | const child = stanza.getChild('event') || stanza.getChild('pubsub'); 54 | if (!child) { 55 | return false; 56 | } 57 | const items = child.getChild('items'); 58 | if (!items) { 59 | return false; 60 | } 61 | const item = items.getChild('item'); 62 | if (!item) { 63 | return false; 64 | } 65 | const bundle = item.getChild('bundle'); 66 | 67 | return bundle && bundle.attrs.xmlns === OMEMODeviceListNS && 68 | (child.attrs.xmlns === PubSubEventNS || child.attrs.xmlns === PubSubNS); 69 | }; 70 | 71 | const preparePayloadBundle = (stanza: Object, deviceId?: string): Object => { 72 | const { from: jid, type } = stanza.attrs; 73 | const child = stanza.getChild('event') || stanza.getChild('pubsub'); 74 | const items = child.getChild('items'); 75 | const bundle = items.getChild('item').getChild('bundle'); 76 | 77 | const signedPreKeyPublic = bundle.getChild('signedPreKeyPublic'); 78 | const signedPreKeySignature = bundle.getChild('signedPreKeySignature'); 79 | const identityKey = bundle.getChild('identityKey'); 80 | 81 | const prekeys = bundle.getChild('prekeys'); 82 | const preKeyPub = prekeys.getChildren('preKeyPublic'); 83 | 84 | deviceId = deviceId ? deviceId : items.attrs.node.split(':')[1]; 85 | 86 | const payload = { 87 | jid, 88 | type, 89 | prekeys: preKeyPub.map((pkp: Object): Object => { 90 | return { 91 | preKeyPublic: pkp.text(), 92 | preKeyId: pkp.attrs.preKeyId, 93 | }; 94 | }), 95 | identityKey: identityKey.text(), 96 | signedPreKeySignature: signedPreKeySignature.text(), 97 | signedPreKeyPublic: signedPreKeyPublic.text(), 98 | signedPreKeyId: signedPreKeyPublic.attrs.signedPreKeyId, 99 | deviceId, 100 | }; 101 | return payload; 102 | }; 103 | 104 | export type KeysRecord = { 105 | prekey: string, 106 | key: string, 107 | deviceId: string, 108 | }; 109 | 110 | export type KeysType = Array; 111 | 112 | export type EncryptionPayloadType = { 113 | ciphertext: string, 114 | iv: string, 115 | keys: KeysType, 116 | }; 117 | 118 | class OMEMOPlugin extends EventEmitter { 119 | iqCaller: Object; 120 | 121 | constructor(client: Object) { 122 | super(); 123 | this.client = client; 124 | } 125 | 126 | sendMessage(from: string, to: string, type: 'chat' | 'groupchat' | 'normal', encryptionPayload: EncryptionPayloadType, id: number, sid: number, otherElements: Array): Promise { 127 | let encryptionXML = xml('encryption', { 128 | xmlns: ExplicitEncryptionNS, 129 | name: 'OMEMO', 130 | namespace: OMEMODeviceListNS, 131 | }); 132 | const { ciphertext, iv, keys } = encryptionPayload; 133 | const keysXML = keys.map((kInfo: Object): any => { 134 | const { prekey, key, deviceId: rid } = kInfo; 135 | return xml('key', {prekey, rid}, key); 136 | }); 137 | let ivXML = xml('iv', null, iv); 138 | 139 | const req = xml('message', 140 | { 141 | to, 142 | from, 143 | id, 144 | type, 145 | }, 146 | xml('encrypted', { xmlns: OMEMODeviceListNS }, 147 | xml('header', { sid }, 148 | keysXML, 149 | ivXML 150 | ), 151 | xml('payload', null, ciphertext) 152 | ), 153 | encryptionXML, 154 | ...otherElements, 155 | ); 156 | return this.client.send(req); 157 | } 158 | 159 | handleDeviceList(stanza: Object) { 160 | const payload = preparePayloadDeviceList(stanza); 161 | this.emit('omemo.devicelist', payload); 162 | } 163 | 164 | /** 165 | * 166 | * @param {string} from : full JID of local user. 167 | * @param {string} to : Bare JID of remote contact. 168 | */ 169 | async subscribeToDeviceListUpdate(from: string, to: string): Promise { 170 | const { iqCaller } = this.client; 171 | const message = xml('iq', 172 | { 173 | xmlns: 'jabber:client', 174 | type: 'set', 175 | from, 176 | to, 177 | }, 178 | xml('pubsub', { xmlns: PubSubNS }, 179 | xml('subscribe', { node: OMEMODeviceListNodeNS, jid: from.split('/')[0] }, 180 | ))); 181 | return await iqCaller.request(message); 182 | } 183 | 184 | /** 185 | * 186 | * @param {string} from : full JID of local user. 187 | * @param {string} to : Bare JID of remote contact. 188 | */ 189 | async subscribeToBundleUpdate(from: string, to: string, deviceId: string): Promise { 190 | const { iqCaller } = this.client; 191 | const message = xml('iq', 192 | { 193 | xmlns: 'jabber:client', 194 | type: 'set', 195 | from, 196 | to, 197 | }, 198 | xml('pubsub', { xmlns: PubSubNS }, 199 | xml('subscribe', { node: BundleInfoNode + deviceId, jid: from.split('/')[0] }, 200 | ))); 201 | return await iqCaller.request(message); 202 | } 203 | 204 | requestDeviceList(from: string, to: string): any { 205 | const req = xml('iq', 206 | { 207 | xmlns: 'jabber:client', 208 | type: 'get', 209 | from, 210 | to, 211 | }, 212 | xml('pubsub', { xmlns: PubSubNS }, 213 | xml('items', { node: OMEMODeviceListNodeNS}, 214 | ))); 215 | 216 | return this.client.iqCaller 217 | .request(req).then((res: Object): any => { 218 | if (isOMEMODeviceList(res)) { 219 | return preparePayloadDeviceList(res); 220 | } 221 | return res; 222 | }); 223 | } 224 | 225 | requestBundle(from: string, to: string, deviceId: string): any { 226 | const req = xml('iq', 227 | { 228 | type: 'get', 229 | from, 230 | to, 231 | }, 232 | xml('pubsub', { xmlns: PubSubNS }, 233 | xml('items', { node: BundleInfoNode + deviceId}, 234 | ))); 235 | return this.client.iqCaller 236 | .request(req).then((res: Object): any => { 237 | if (isOMEMOBundle(res)) { 238 | return preparePayloadBundle(res, deviceId); 239 | } 240 | return res; 241 | }); 242 | } 243 | 244 | announceOMEMOSupport(deviceList: Array, from: string): Promise { 245 | const devices = deviceList.map((id: string): Object => { 246 | return xml('device', {id}); 247 | }); 248 | const req = xml('iq', 249 | { 250 | type: 'set', 251 | from, 252 | }, 253 | xml('pubsub', { xmlns: PubSubNS }, 254 | xml('publish', { node: OMEMODeviceListNodeNS}, 255 | xml('item', { id: 'current' }, 256 | xml('list', { xmlns: OMEMODeviceListNS}, 257 | ...devices, 258 | ))))); 259 | return this.client.iqCaller 260 | .request(req); 261 | } 262 | 263 | announceBundleInfo(data: Object, deviceId: string, from: string): Promise { 264 | const { 265 | signedPreKeyPublic, 266 | signedPreKeyId, 267 | signedPreKeySignature, 268 | identityKey, 269 | preKeysPublic, 270 | } = data; 271 | const prekeys = preKeysPublic.map(({preKeyPublic, preKeyId}: Object): Object => { 272 | return xml('preKeyPublic', {preKeyId}, preKeyPublic); 273 | }); 274 | const req = xml('iq', 275 | { 276 | type: 'set', 277 | from, 278 | }, 279 | xml('pubsub', { xmlns: PubSubNS }, 280 | xml('publish', { node: BundleInfoNode + deviceId}, 281 | xml('item', { id: 'current'}, 282 | xml('bundle', { xmlns: OMEMODeviceListNS}, 283 | xml('signedPreKeyPublic', {signedPreKeyId}, signedPreKeyPublic), 284 | xml('signedPreKeySignature', null, signedPreKeySignature), 285 | xml('identityKey', null, identityKey), 286 | xml('prekeys', null, 287 | ...prekeys, 288 | )))))); 289 | return this.client.iqCaller 290 | .request(req); 291 | } 292 | } 293 | 294 | /** 295 | * Register a OMEMOPlugin Encryption plugin. 296 | * 297 | * @param {Client} client XMPP client instance 298 | * @returns {OMEMOPlugin} Plugin instance 299 | */ 300 | 301 | function setupOMEMO(client: Object): Object { 302 | const {middleware} = client; 303 | const plugin = new OMEMOPlugin(client); 304 | 305 | middleware.use(({stanza}: Object, next: Function): Function => { 306 | // TODO 101 : Investigate, Middleware part not called for device list updation 'message' stanza 307 | // Hence event 'omemo.devicelist' will not be fired. 308 | if (isOMEMODeviceList(stanza)) { 309 | return plugin.handleDeviceList(stanza); 310 | } 311 | 312 | return next(); 313 | }); 314 | 315 | return plugin; 316 | } 317 | 318 | module.exports = { 319 | setupOMEMO, 320 | isOMEMODeviceList, 321 | preparePayloadDeviceList, 322 | isOMEMOBundle, 323 | preparePayloadBundle, 324 | }; 325 | -------------------------------------------------------------------------------- /packages/xep-0384-omemo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xmppjs-client-plugins", 3 | "description": "omemo plugin for xmpp.js client", 4 | "version": "1.0.0", 5 | "license": "ISC", 6 | "keywords": [ 7 | "XMPP", 8 | "OMEMO" 9 | ], 10 | "engines": { 11 | "node": ">= 10.0.0", 12 | "yarn": ">= 1.0.0" 13 | }, 14 | "dependencies": { 15 | "@xmpp/events": "^0.7.0", 16 | "@xmpp/xml": "^0.7.0" 17 | }, 18 | "devDependencies": { 19 | "@xmpp/events": "^0.7.0" 20 | }, 21 | "publishConfig": { 22 | "access": "public" 23 | } 24 | } --------------------------------------------------------------------------------