├── .nvmrc ├── examples ├── attach │ ├── __init__.py │ ├── attacher │ │ ├── __init__.py │ │ └── views.py │ ├── manage.py │ ├── urls.py │ ├── README │ ├── templates │ │ └── attacher │ │ │ └── index.html │ ├── settings.py │ └── boshclient.py ├── amd.html ├── restore.html ├── basic.html ├── echobot.html ├── prebind.html ├── main.js ├── basic.js ├── restore.js ├── echobot.js └── prebind.js ├── .gitattributes ├── jsdoc.json ├── .travis.yml ├── .github ├── dependabot.yml └── workflows │ └── karma-tests.yml ├── .prettierrc ├── src ├── types │ ├── errors.d.ts │ ├── sasl-anon.d.ts │ ├── sasl-plain.d.ts │ ├── sasl-external.d.ts │ ├── sasl-oauthbearer.d.ts │ ├── sasl-xoauth2.d.ts │ ├── sasl-sha256.d.ts │ ├── sasl-sha384.d.ts │ ├── sasl-sha512.d.ts │ ├── sasl-sha1.d.ts │ ├── scram.d.ts │ ├── worker-websocket.d.ts │ ├── shared-connection-worker.d.ts │ ├── timed-handler.d.ts │ ├── request.d.ts │ ├── stanza.d.ts │ ├── log.d.ts │ ├── handler.d.ts │ ├── sasl.d.ts │ ├── constants.d.ts │ ├── websocket.d.ts │ ├── builder.d.ts │ ├── utils.d.ts │ ├── index.d.ts │ └── bosh.d.ts ├── errors.js ├── sasl-anon.js ├── sasl-xoauth2.js ├── sasl-sha256.js ├── sasl-sha384.js ├── sasl-sha512.js ├── sasl-sha1.js ├── sasl-oauthbearer.js ├── sasl-external.js ├── sasl-plain.js ├── timed-handler.js ├── log.js ├── shared-connection-worker.js ├── request.js ├── sasl.js ├── stanza.js ├── handler.js ├── worker-websocket.js ├── constants.js ├── index.js ├── scram.js └── builder.js ├── .gitignore ├── babel.config.json ├── tests └── node.js ├── tsconfig.json ├── RELEASE_CHECKLIST.md ├── LICENSE.txt ├── karma.conf.js ├── README.md ├── Makefile ├── rollup.config.js ├── package.json └── eslint.config.mjs /.nvmrc: -------------------------------------------------------------------------------- 1 | v22.14.0 2 | -------------------------------------------------------------------------------- /examples/attach/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.min.js binary -------------------------------------------------------------------------------- /examples/attach/attacher/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["plugins/markdown"] 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: bionic 2 | language: node_js 3 | addons: 4 | chrome: stable 5 | node_js: 6 | - "13" 7 | before_script: make serve_bg 8 | script: make check 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 120, 4 | "quoteProps": "preserve", 5 | "spaceBeforeFunctionParen": true, 6 | "tabWidth": 4, 7 | "useTabs": false 8 | } 9 | -------------------------------------------------------------------------------- /src/types/errors.d.ts: -------------------------------------------------------------------------------- 1 | export class SessionError extends Error { 2 | /** 3 | * @param {string} message 4 | */ 5 | constructor(message: string); 6 | } 7 | //# sourceMappingURL=errors.d.ts.map -------------------------------------------------------------------------------- /src/types/sasl-anon.d.ts: -------------------------------------------------------------------------------- 1 | export default SASLAnonymous; 2 | export type Connection = import("./connection.js").default; 3 | declare class SASLAnonymous extends SASLMechanism { 4 | } 5 | import SASLMechanism from './sasl.js'; 6 | //# sourceMappingURL=sasl-anon.d.ts.map -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | stamp-npm 2 | stamp-bower 3 | version.txt 4 | node_modules 5 | bower_components 6 | dist 7 | strophejs-*/ 8 | ndproj 9 | doc 10 | *.map 11 | .taperc 12 | *.tar.gz 13 | *.zip 14 | *~ 15 | .*.sw? 16 | doc-tmp 17 | tags.lock 18 | tags.temp 19 | .aider* 20 | -------------------------------------------------------------------------------- /src/errors.js: -------------------------------------------------------------------------------- 1 | class SessionError extends Error { 2 | /** 3 | * @param {string} message 4 | */ 5 | constructor(message) { 6 | super(message); 7 | this.name = 'StropheSessionError'; 8 | } 9 | } 10 | 11 | export { SessionError }; 12 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "targets": { 5 | "browsers": [">1%", "not dead"] 6 | } 7 | }] 8 | ], 9 | "plugins": ["@babel/plugin-proposal-optional-chaining", "@babel/plugin-proposal-nullish-coalescing-operator"] 10 | } 11 | -------------------------------------------------------------------------------- /tests/node.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | const { Strophe, $iq, $msg, $build, $pres } = require('../dist/strophe.common.js'); 4 | 5 | global.sinon = require('sinon'); 6 | global.XMLHttpRequest = require('xhr2'); 7 | 8 | global.Strophe = Strophe; 9 | global.$iq = $iq; 10 | global.$msg = $msg; 11 | global.$build = $build; 12 | global.$pres = $pres; 13 | -------------------------------------------------------------------------------- /src/types/sasl-plain.d.ts: -------------------------------------------------------------------------------- 1 | export default SASLPlain; 2 | export type Connection = import("./connection.js").default; 3 | declare class SASLPlain extends SASLMechanism { 4 | /** 5 | * @param {Connection} connection 6 | */ 7 | onChallenge(connection: Connection): string; 8 | } 9 | import SASLMechanism from './sasl.js'; 10 | //# sourceMappingURL=sasl-plain.d.ts.map -------------------------------------------------------------------------------- /src/types/sasl-external.d.ts: -------------------------------------------------------------------------------- 1 | export default SASLExternal; 2 | export type Connection = import("./connection.js").default; 3 | declare class SASLExternal extends SASLMechanism { 4 | /** 5 | * @param {Connection} connection 6 | */ 7 | onChallenge(connection: Connection): string; 8 | } 9 | import SASLMechanism from './sasl.js'; 10 | //# sourceMappingURL=sasl-external.d.ts.map -------------------------------------------------------------------------------- /src/types/sasl-oauthbearer.d.ts: -------------------------------------------------------------------------------- 1 | export default SASLOAuthBearer; 2 | export type Connection = import("./connection.js").default; 3 | declare class SASLOAuthBearer extends SASLMechanism { 4 | /** 5 | * @param {Connection} connection 6 | */ 7 | onChallenge(connection: Connection): string; 8 | } 9 | import SASLMechanism from './sasl.js'; 10 | //# sourceMappingURL=sasl-oauthbearer.d.ts.map -------------------------------------------------------------------------------- /src/types/sasl-xoauth2.d.ts: -------------------------------------------------------------------------------- 1 | export default SASLXOAuth2; 2 | export type Connection = import("./connection.js").default; 3 | /** 4 | * @typedef {import("./connection.js").default} Connection 5 | */ 6 | declare class SASLXOAuth2 extends SASLMechanism { 7 | /** 8 | * @param {Connection} connection 9 | */ 10 | onChallenge(connection: Connection): string; 11 | } 12 | import SASLMechanism from './sasl.js'; 13 | //# sourceMappingURL=sasl-xoauth2.d.ts.map -------------------------------------------------------------------------------- /examples/attach/attacher/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.template import Context, loader 3 | 4 | from attach.settings import BOSH_SERVICE, JABBERID, PASSWORD 5 | from attach.boshclient import BOSHClient 6 | 7 | def index(request): 8 | bc = BOSHClient(JABBERID, PASSWORD, BOSH_SERVICE) 9 | bc.startSessionAndAuth() 10 | 11 | t = loader.get_template("attacher/index.html") 12 | c = Context({ 13 | 'jid': bc.jabberid.full(), 14 | 'sid': bc.sid, 15 | 'rid': bc.rid, 16 | }) 17 | 18 | return HttpResponse(t.render(c)) 19 | -------------------------------------------------------------------------------- /examples/attach/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | try: 4 | import settings # Assumed to be in the same directory. 5 | except ImportError: 6 | import sys 7 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 8 | sys.exit(1) 9 | 10 | if __name__ == "__main__": 11 | execute_manager(settings) 12 | -------------------------------------------------------------------------------- /src/sasl-anon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import("./connection.js").default} Connection 3 | */ 4 | import SASLMechanism from './sasl.js'; 5 | 6 | class SASLAnonymous extends SASLMechanism { 7 | /** 8 | * SASL ANONYMOUS authentication. 9 | */ 10 | constructor(mechname = 'ANONYMOUS', isClientFirst = false, priority = 20) { 11 | super(mechname, isClientFirst, priority); 12 | } 13 | 14 | /** 15 | * @param {Connection} connection 16 | */ 17 | test(connection) { 18 | return connection.authcid === null; 19 | } 20 | } 21 | 22 | export default SASLAnonymous; 23 | -------------------------------------------------------------------------------- /src/types/sasl-sha256.d.ts: -------------------------------------------------------------------------------- 1 | export default SASLSHA256; 2 | export type Connection = import("./connection.js").default; 3 | declare class SASLSHA256 extends SASLMechanism { 4 | /** 5 | * @param {Connection} connection 6 | * @param {string} [challenge] 7 | */ 8 | onChallenge(connection: Connection, challenge?: string): Promise; 9 | /** 10 | * @param {Connection} connection 11 | * @param {string} [test_cnonce] 12 | */ 13 | clientChallenge(connection: Connection, test_cnonce?: string): string; 14 | } 15 | import SASLMechanism from './sasl.js'; 16 | //# sourceMappingURL=sasl-sha256.d.ts.map -------------------------------------------------------------------------------- /src/types/sasl-sha384.d.ts: -------------------------------------------------------------------------------- 1 | export default SASLSHA384; 2 | export type Connection = import("./connection.js").default; 3 | declare class SASLSHA384 extends SASLMechanism { 4 | /** 5 | * @param {Connection} connection 6 | * @param {string} [challenge] 7 | */ 8 | onChallenge(connection: Connection, challenge?: string): Promise; 9 | /** 10 | * @param {Connection} connection 11 | * @param {string} [test_cnonce] 12 | */ 13 | clientChallenge(connection: Connection, test_cnonce?: string): string; 14 | } 15 | import SASLMechanism from './sasl.js'; 16 | //# sourceMappingURL=sasl-sha384.d.ts.map -------------------------------------------------------------------------------- /src/types/sasl-sha512.d.ts: -------------------------------------------------------------------------------- 1 | export default SASLSHA512; 2 | export type Connection = import("./connection.js").default; 3 | declare class SASLSHA512 extends SASLMechanism { 4 | /** 5 | * @param {Connection} connection 6 | * @param {string} [challenge] 7 | */ 8 | onChallenge(connection: Connection, challenge?: string): Promise; 9 | /** 10 | * @param {Connection} connection 11 | * @param {string} [test_cnonce] 12 | */ 13 | clientChallenge(connection: Connection, test_cnonce?: string): string; 14 | } 15 | import SASLMechanism from './sasl.js'; 16 | //# sourceMappingURL=sasl-sha512.d.ts.map -------------------------------------------------------------------------------- /examples/attach/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | # Uncomment the next two lines to enable the admin: 4 | # from django.contrib import admin 5 | # admin.autodiscover() 6 | 7 | urlpatterns = patterns('', 8 | # Example: 9 | # (r'^attach/', include('attach.foo.urls')), 10 | 11 | # Uncomment the admin/doc line below and add 'django.contrib.admindocs' 12 | # to INSTALLED_APPS to enable admin documentation: 13 | # (r'^admin/doc/', include('django.contrib.admindocs.urls')), 14 | 15 | # Uncomment the next line to enable the admin: 16 | # (r'^admin/(.*)', admin.site.root), 17 | 18 | (r'^$', 'attach.attacher.views.index'), 19 | ) 20 | -------------------------------------------------------------------------------- /examples/amd.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Strophe.js AMD Example 5 | 6 | 7 | 8 | 9 |
10 |
11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |
19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /src/types/sasl-sha1.d.ts: -------------------------------------------------------------------------------- 1 | export default SASLSHA1; 2 | export type Connection = import("./connection.js").default; 3 | declare class SASLSHA1 extends SASLMechanism { 4 | /** 5 | * @param {Connection} connection 6 | * @param {string} [challenge] 7 | * @return {Promise} Mechanism response. 8 | */ 9 | onChallenge(connection: Connection, challenge?: string): Promise; 10 | /** 11 | * @param {Connection} connection 12 | * @param {string} [test_cnonce] 13 | */ 14 | clientChallenge(connection: Connection, test_cnonce?: string): string; 15 | } 16 | import SASLMechanism from './sasl.js'; 17 | //# sourceMappingURL=sasl-sha1.d.ts.map -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/**/*" 4 | ], 5 | "compilerOptions": { 6 | "target": "es2016", 7 | "module": "esnext", 8 | 9 | "allowJs": true, 10 | "checkJs": true, 11 | 12 | // Generate d.ts files 13 | "declaration": true, 14 | "emitDeclarationOnly": true, 15 | "declarationMap": true, 16 | 17 | "rootDir": "./src", 18 | "outDir": "./src/types/", 19 | "baseUrl": "./src/", 20 | 21 | "esModuleInterop": true, 22 | "forceConsistentCasingInFileNames": true, 23 | 24 | "strict": false, 25 | "noImplicitAny": true, 26 | 27 | "skipLibCheck": true, 28 | 29 | "moduleResolution": "node", 30 | "resolveJsonModule": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/restore.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Strophe.js Persistent Session Example 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Strophe.js Basic Example 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /RELEASE_CHECKLIST.md: -------------------------------------------------------------------------------- 1 | # Release Checklist 2 | 3 | ## Note: it's assumed that you have Strophe.js and Strophe.im checked out in sibling directories. 4 | 5 | 1. Make sure all tests pass (run 'make check') 6 | 2. Update CHANGELOG.md 7 | 3. Run `make release VERSION=3.0.1` (on Mac, prefix with "SED=gsed" so that GNU-sed is used). 8 | 4. Run `make doc` 9 | 5. Run `cp -r doc ../strophe.im/strophejs/doc/3.0.1` 10 | 5. Update links in `../strophe.im/strophejs/index.markdown` in Strophe.im 11 | 6. `cd ../strophe.im && git commit -am "Docs for Strophe.js 3.0.1" && git push` 12 | 7. Update link to documentation in README (of strophe.js) 13 | 8. `cd ../strophe.js && git commit -am "Release 3.0.1"` 14 | 9. `git tag -s v3.0.1 -m "Release 3.0.1"` 15 | 10. Run `git push && git push origin v3.0.1` 16 | 11. Publish on NPM: `npm publish` 17 | 12. Update the release notes on https://github.com/strophe/strophejs/releases 18 | 13. Run `npm pack` and upload the tgz file to the releases page. 19 | -------------------------------------------------------------------------------- /examples/echobot.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Strophe.js Echobot Example 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /src/sasl-xoauth2.js: -------------------------------------------------------------------------------- 1 | import SASLMechanism from './sasl.js'; 2 | import utils from './utils'; 3 | 4 | /** 5 | * @typedef {import("./connection.js").default} Connection 6 | */ 7 | 8 | class SASLXOAuth2 extends SASLMechanism { 9 | /** 10 | * SASL X-OAuth2 authentication. 11 | */ 12 | constructor(mechname = 'X-OAUTH2', isClientFirst = true, priority = 30) { 13 | super(mechname, isClientFirst, priority); 14 | } 15 | 16 | /** 17 | * @param {Connection} connection 18 | */ 19 | test(connection) { 20 | return connection.pass !== null; 21 | } 22 | 23 | /** 24 | * @param {Connection} connection 25 | */ 26 | onChallenge(connection) { 27 | let auth_str = '\u0000'; 28 | if (connection.authcid !== null) { 29 | auth_str = auth_str + connection.authzid; 30 | } 31 | auth_str = auth_str + '\u0000'; 32 | auth_str = auth_str + connection.pass; 33 | return utils.utf16to8(auth_str); 34 | } 35 | } 36 | 37 | export default SASLXOAuth2; 38 | -------------------------------------------------------------------------------- /.github/workflows/karma-tests.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Strophe CI Tests 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | env: 16 | DISPLAY: :99.0 17 | 18 | strategy: 19 | matrix: 20 | node-version: [19.x] 21 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | cache: 'npm' 30 | - name: Run Karma tests 31 | uses: GabrielBB/xvfb-action@v1 32 | with: 33 | run: make check ARGS=--single-run 34 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006-2009 Collecta, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/sasl-sha256.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import("./connection.js").default} Connection 3 | */ 4 | import SASLMechanism from './sasl.js'; 5 | import scram from './scram.js'; 6 | 7 | class SASLSHA256 extends SASLMechanism { 8 | /** 9 | * SASL SCRAM SHA 256 authentication. 10 | */ 11 | constructor(mechname = 'SCRAM-SHA-256', isClientFirst = true, priority = 70) { 12 | super(mechname, isClientFirst, priority); 13 | } 14 | 15 | /** 16 | * @param {Connection} connection 17 | */ 18 | test(connection) { 19 | return connection.authcid !== null; 20 | } 21 | 22 | /** 23 | * @param {Connection} connection 24 | * @param {string} [challenge] 25 | */ 26 | async onChallenge(connection, challenge) { 27 | return await scram.scramResponse(connection, challenge, 'SHA-256', 256); 28 | } 29 | 30 | /** 31 | * @param {Connection} connection 32 | * @param {string} [test_cnonce] 33 | */ 34 | clientChallenge(connection, test_cnonce) { 35 | return scram.clientChallenge(connection, test_cnonce); 36 | } 37 | } 38 | 39 | export default SASLSHA256; 40 | -------------------------------------------------------------------------------- /src/sasl-sha384.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import("./connection.js").default} Connection 3 | */ 4 | import SASLMechanism from './sasl.js'; 5 | import scram from './scram.js'; 6 | 7 | class SASLSHA384 extends SASLMechanism { 8 | /** 9 | * SASL SCRAM SHA 384 authentication. 10 | */ 11 | constructor(mechname = 'SCRAM-SHA-384', isClientFirst = true, priority = 71) { 12 | super(mechname, isClientFirst, priority); 13 | } 14 | 15 | /** 16 | * @param {Connection} connection 17 | */ 18 | test(connection) { 19 | return connection.authcid !== null; 20 | } 21 | 22 | /** 23 | * @param {Connection} connection 24 | * @param {string} [challenge] 25 | */ 26 | async onChallenge(connection, challenge) { 27 | return await scram.scramResponse(connection, challenge, 'SHA-384', 384); 28 | } 29 | 30 | /** 31 | * @param {Connection} connection 32 | * @param {string} [test_cnonce] 33 | */ 34 | clientChallenge(connection, test_cnonce) { 35 | return scram.clientChallenge(connection, test_cnonce); 36 | } 37 | } 38 | 39 | export default SASLSHA384; 40 | -------------------------------------------------------------------------------- /src/sasl-sha512.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import("./connection.js").default} Connection 3 | */ 4 | import SASLMechanism from './sasl.js'; 5 | import scram from './scram.js'; 6 | 7 | class SASLSHA512 extends SASLMechanism { 8 | /** 9 | * SASL SCRAM SHA 512 authentication. 10 | */ 11 | constructor(mechname = 'SCRAM-SHA-512', isClientFirst = true, priority = 72) { 12 | super(mechname, isClientFirst, priority); 13 | } 14 | 15 | /** 16 | * @param {Connection} connection 17 | */ 18 | test(connection) { 19 | return connection.authcid !== null; 20 | } 21 | 22 | /** 23 | * @param {Connection} connection 24 | * @param {string} [challenge] 25 | */ 26 | async onChallenge(connection, challenge) { 27 | return await scram.scramResponse(connection, challenge, 'SHA-512', 512); 28 | } 29 | 30 | /** 31 | * @param {Connection} connection 32 | * @param {string} [test_cnonce] 33 | */ 34 | clientChallenge(connection, test_cnonce) { 35 | return scram.clientChallenge(connection, test_cnonce); 36 | } 37 | } 38 | 39 | export default SASLSHA512; 40 | -------------------------------------------------------------------------------- /src/sasl-sha1.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import("./connection.js").default} Connection 3 | */ 4 | import SASLMechanism from './sasl.js'; 5 | import scram from './scram.js'; 6 | 7 | class SASLSHA1 extends SASLMechanism { 8 | /** 9 | * SASL SCRAM SHA 1 authentication. 10 | */ 11 | constructor(mechname = 'SCRAM-SHA-1', isClientFirst = true, priority = 60) { 12 | super(mechname, isClientFirst, priority); 13 | } 14 | 15 | /** 16 | * @param {Connection} connection 17 | */ 18 | test(connection) { 19 | return connection.authcid !== null; 20 | } 21 | 22 | /** 23 | * @param {Connection} connection 24 | * @param {string} [challenge] 25 | * @return {Promise} Mechanism response. 26 | */ 27 | async onChallenge(connection, challenge) { 28 | return await scram.scramResponse(connection, challenge, 'SHA-1', 160); 29 | } 30 | 31 | /** 32 | * @param {Connection} connection 33 | * @param {string} [test_cnonce] 34 | */ 35 | clientChallenge(connection, test_cnonce) { 36 | return scram.clientChallenge(connection, test_cnonce); 37 | } 38 | } 39 | 40 | export default SASLSHA1; 41 | -------------------------------------------------------------------------------- /src/types/scram.d.ts: -------------------------------------------------------------------------------- 1 | export { scram as default }; 2 | export type Connection = import("./connection.js").default; 3 | export type Password = { 4 | name: string; 5 | ck: string; 6 | sk: string; 7 | iter: number; 8 | salt: string; 9 | }; 10 | declare namespace scram { 11 | /** 12 | * On success, sets 13 | * connection_sasl_data["server-signature"] 14 | * and 15 | * connection._sasl_data.keys 16 | * 17 | * The server signature should be verified after this function completes.. 18 | * 19 | * On failure, returns connection._sasl_failure_cb(); 20 | * @param {Connection} connection 21 | * @param {string} challenge 22 | * @param {string} hashName 23 | * @param {number} hashBits 24 | */ 25 | function scramResponse(connection: Connection, challenge: string, hashName: string, hashBits: number): Promise; 26 | /** 27 | * Returns a string containing the client first message 28 | * @param {Connection} connection 29 | * @param {string} test_cnonce 30 | */ 31 | function clientChallenge(connection: Connection, test_cnonce: string): string; 32 | } 33 | //# sourceMappingURL=scram.d.ts.map -------------------------------------------------------------------------------- /src/types/worker-websocket.d.ts: -------------------------------------------------------------------------------- 1 | export default WorkerWebsocket; 2 | /** 3 | * Helper class that handles a websocket connection inside a shared worker. 4 | */ 5 | declare class WorkerWebsocket extends Websocket { 6 | worker: SharedWorker; 7 | /** 8 | * @private 9 | */ 10 | private _setSocket; 11 | /** @param {MessageEvent} m */ 12 | _messageHandler: ((m: MessageEvent) => void) | ((m: MessageEvent) => void) | ((m: MessageEvent) => void); 13 | /** 14 | * @param {Function} callback 15 | */ 16 | _attach(callback: Function): void; 17 | /** 18 | * @param {number} status 19 | * @param {string} jid 20 | */ 21 | _attachCallback(status: number, jid: string): void; 22 | /** 23 | * @param {Element|Builder} pres - This stanza will be sent before disconnecting. 24 | */ 25 | _disconnect(pres: Element | Builder): void; 26 | /** 27 | * function that handles messages received from the service worker 28 | * @private 29 | * @param {MessageEvent} ev 30 | */ 31 | private _onWorkerMessage; 32 | } 33 | import Websocket from './websocket.js'; 34 | import Builder from './builder.js'; 35 | //# sourceMappingURL=worker-websocket.d.ts.map -------------------------------------------------------------------------------- /src/sasl-oauthbearer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import("./connection.js").default} Connection 3 | */ 4 | import SASLMechanism from './sasl.js'; 5 | import utils from './utils'; 6 | 7 | class SASLOAuthBearer extends SASLMechanism { 8 | /** 9 | * SASL OAuth Bearer authentication. 10 | */ 11 | constructor(mechname = 'OAUTHBEARER', isClientFirst = true, priority = 40) { 12 | super(mechname, isClientFirst, priority); 13 | } 14 | 15 | /** 16 | * @param {Connection} connection 17 | */ 18 | test(connection) { 19 | return connection.pass !== null; 20 | } 21 | 22 | /** 23 | * @param {Connection} connection 24 | */ 25 | onChallenge(connection) { 26 | let auth_str = 'n,'; 27 | if (connection.authcid !== null) { 28 | auth_str = auth_str + 'a=' + connection.authzid; 29 | } 30 | auth_str = auth_str + ','; 31 | auth_str = auth_str + '\u0001'; 32 | auth_str = auth_str + 'auth=Bearer '; 33 | auth_str = auth_str + connection.pass; 34 | auth_str = auth_str + '\u0001'; 35 | auth_str = auth_str + '\u0001'; 36 | return utils.utf16to8(auth_str); 37 | } 38 | } 39 | 40 | export default SASLOAuthBearer; 41 | -------------------------------------------------------------------------------- /examples/prebind.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 18 | Strophe.js Pre-Bind Example 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 |
27 | 28 | 29 | 30 | 31 | 32 |
33 |
34 | 35 |
36 | 37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /src/sasl-external.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import("./connection.js").default} Connection 3 | */ 4 | 5 | import SASLMechanism from './sasl.js'; 6 | 7 | class SASLExternal extends SASLMechanism { 8 | /** 9 | * SASL EXTERNAL authentication. 10 | * 11 | * The EXTERNAL mechanism allows a client to request the server to use 12 | * credentials established by means external to the mechanism to 13 | * authenticate the client. The external means may be, for instance, 14 | * TLS services. 15 | */ 16 | constructor(mechname = 'EXTERNAL', isClientFirst = true, priority = 10) { 17 | super(mechname, isClientFirst, priority); 18 | } 19 | 20 | /** 21 | * @param {Connection} connection 22 | */ 23 | onChallenge(connection) { 24 | /* According to XEP-178, an authzid SHOULD NOT be presented when the 25 | * authcid contained or implied in the client certificate is the JID (i.e. 26 | * authzid) with which the user wants to log in as. 27 | * 28 | * To NOT send the authzid, the user should therefore set the authcid equal 29 | * to the JID when instantiating a new Strophe.Connection object. 30 | */ 31 | return connection.authcid === connection.authzid ? '' : connection.authzid; 32 | } 33 | } 34 | 35 | export default SASLExternal; 36 | -------------------------------------------------------------------------------- /src/sasl-plain.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import("./connection.js").default} Connection 3 | */ 4 | import SASLMechanism from './sasl.js'; 5 | import utils from './utils'; 6 | 7 | class SASLPlain extends SASLMechanism { 8 | /** 9 | * SASL PLAIN authentication. 10 | */ 11 | constructor(mechname = 'PLAIN', isClientFirst = true, priority = 50) { 12 | super(mechname, isClientFirst, priority); 13 | } 14 | 15 | /** 16 | * @param {Connection} connection 17 | */ 18 | test(connection) { 19 | return connection.authcid !== null; 20 | } 21 | 22 | /** 23 | * @param {Connection} connection 24 | */ 25 | onChallenge(connection) { 26 | const { authcid, authzid, domain, pass } = connection; 27 | if (!domain) { 28 | throw new Error('SASLPlain onChallenge: domain is not defined!'); 29 | } 30 | // Only include authzid if it differs from authcid. 31 | // See: https://tools.ietf.org/html/rfc6120#section-6.3.8 32 | let auth_str = authzid !== `${authcid}@${domain}` ? authzid : ''; 33 | auth_str = auth_str + '\u0000'; 34 | auth_str = auth_str + authcid; 35 | auth_str = auth_str + '\u0000'; 36 | auth_str = auth_str + pass; 37 | return utils.utf16to8(auth_str); 38 | } 39 | } 40 | 41 | export default SASLPlain; 42 | -------------------------------------------------------------------------------- /examples/attach/README: -------------------------------------------------------------------------------- 1 | This is an example of Strophe attaching to a pre-existing BOSH session 2 | that is created externally. This example requires a bit more than 3 | HTML and JavaScript. Specifically it contains a very simple Web 4 | application written in Django which creates a BOSH session before 5 | rendering the page. 6 | 7 | Requirements: 8 | 9 | * Django 1.0 (http://www.djangoproject.com) 10 | * Twisted 8.1.x (http://twistedmatrix.com) 11 | * Punjab 0.3 (http://code.stanziq.com/punjab) 12 | 13 | Note that Twisted and Punjab are only used for small functions related 14 | to JID and BOSH parsing. 15 | 16 | How It Works: 17 | 18 | The Django app contains one view which is tied to the root URL. This 19 | view uses the BOSHClient class to start a BOSH session using the 20 | settings from settings.py. 21 | 22 | Once the connection is established, Django passes the JID, SID, and 23 | RID for the BOSH session into the template engine and renders the 24 | page. 25 | 26 | The template assigns the JID, SID, and RID to global vars like so: 27 | 28 | var BOSH_JID = {{ jid }}; 29 | var BOSH_SID = {{ sid }}; 30 | var BOSH_RID = {{ rid }}; 31 | 32 | The connection is attached to Strophe by calling 33 | Strophe.Connection.attach() with this data and a connection callback 34 | handler. 35 | 36 | To show that the session is attached and works, a disco info ping is 37 | done to jabber.org. 38 | -------------------------------------------------------------------------------- /src/types/shared-connection-worker.d.ts: -------------------------------------------------------------------------------- 1 | /** @type {ConnectionManager} */ 2 | declare let manager: ConnectionManager; 3 | declare namespace Status { 4 | let ERROR: number; 5 | let CONNECTING: number; 6 | let CONNFAIL: number; 7 | let AUTHENTICATING: number; 8 | let AUTHFAIL: number; 9 | let CONNECTED: number; 10 | let DISCONNECTED: number; 11 | let DISCONNECTING: number; 12 | let ATTACHED: number; 13 | let REDIRECT: number; 14 | let CONNTIMEOUT: number; 15 | let BINDREQUIRED: number; 16 | let ATTACHFAIL: number; 17 | } 18 | /** Class: ConnectionManager 19 | * 20 | * Manages the shared websocket connection as well as the ports of the 21 | * connected tabs. 22 | */ 23 | declare class ConnectionManager { 24 | /** @type {MessagePort[]} */ 25 | ports: MessagePort[]; 26 | /** @param {MessagePort} port */ 27 | addPort(port: MessagePort): void; 28 | /** 29 | * @param {[string, string]} data 30 | */ 31 | _connect(data: [string, string]): void; 32 | jid: string; 33 | socket: WebSocket; 34 | _attach(): void; 35 | /** @param {string} str */ 36 | send(str: string): void; 37 | /** @param {string} str */ 38 | close(str: string): void; 39 | _onOpen(): void; 40 | /** @param {CloseEvent} e */ 41 | _onClose(e: CloseEvent): void; 42 | /** @param {MessageEvent} message */ 43 | _onMessage(message: MessageEvent): void; 44 | /** @param {Event} error */ 45 | _onError(error: Event): void; 46 | _closeSocket(): void; 47 | } 48 | //# sourceMappingURL=shared-connection-worker.d.ts.map -------------------------------------------------------------------------------- /src/types/timed-handler.d.ts: -------------------------------------------------------------------------------- 1 | export default TimedHandler; 2 | /** 3 | * _Private_ helper class for managing timed handlers. 4 | * 5 | * A Strophe.TimedHandler encapsulates a user provided callback that 6 | * should be called after a certain period of time or at regular 7 | * intervals. The return value of the callback determines whether the 8 | * Strophe.TimedHandler will continue to fire. 9 | * 10 | * Users will not use Strophe.TimedHandler objects directly, but instead 11 | * they will use {@link Strophe.Connection#addTimedHandler|addTimedHandler()} and 12 | * {@link Strophe.Connection#deleteTimedHandler|deleteTimedHandler()}. 13 | * 14 | * @memberof Strophe 15 | */ 16 | declare class TimedHandler { 17 | /** 18 | * Create and initialize a new Strophe.TimedHandler object. 19 | * @param {number} period - The number of milliseconds to wait before the 20 | * handler is called. 21 | * @param {Function} handler - The callback to run when the handler fires. This 22 | * function should take no arguments. 23 | */ 24 | constructor(period: number, handler: Function); 25 | period: number; 26 | handler: Function; 27 | lastCalled: number; 28 | user: boolean; 29 | /** 30 | * Run the callback for the Strophe.TimedHandler. 31 | * 32 | * @return {boolean} Returns the result of running the handler, 33 | * which is `true` if the Strophe.TimedHandler should be called again, 34 | * and `false` otherwise. 35 | */ 36 | run(): boolean; 37 | /** 38 | * Reset the last called time for the Strophe.TimedHandler. 39 | */ 40 | reset(): void; 41 | /** 42 | * Get a string representation of the Strophe.TimedHandler object. 43 | * @return {string} 44 | */ 45 | toString(): string; 46 | } 47 | //# sourceMappingURL=timed-handler.d.ts.map -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | /* global module */ 2 | 3 | module.exports = function (config) { 4 | config.set({ 5 | // base path that will be used to resolve all patterns (eg. files, exclude) 6 | basePath: '', 7 | // frameworks to use 8 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 9 | frameworks: ['qunit'], 10 | 11 | // list of files / patterns to load in the browser 12 | files: [ 13 | 'node_modules/sinon/pkg/sinon.js', 14 | 'dist/strophe.js', 15 | 'tests/tests.js', 16 | ], 17 | 18 | // list of files to exclude 19 | exclude: [], 20 | 21 | // test results reporter to use 22 | // possible values: 'dots', 'progress' 23 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 24 | reporters: ['progress'], 25 | client: {}, 26 | 27 | // web server port 28 | port: 9876, 29 | 30 | // enable / disable colors in the output (reporters and logs) 31 | colors: true, 32 | 33 | // level of logging 34 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 35 | logLevel: config.LOG_INFO, 36 | 37 | // enable / disable watching file and executing tests whenever any file changes 38 | autoWatch: true, 39 | 40 | // start these browsers 41 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 42 | browsers: ['Chrome'], 43 | 44 | // Continuous Integration mode 45 | // if true, Karma captures browsers, runs the tests and exits 46 | singleRun: true, 47 | 48 | // Concurrency level 49 | // how many browser should be started simultaneous 50 | concurrency: Infinity, 51 | }); 52 | }; 53 | -------------------------------------------------------------------------------- /src/types/request.d.ts: -------------------------------------------------------------------------------- 1 | export default Request; 2 | /** 3 | * Helper class that provides a cross implementation abstraction 4 | * for a BOSH related XMLHttpRequest. 5 | * 6 | * The Request class is used internally to encapsulate BOSH request 7 | * information. It is not meant to be used from user's code. 8 | * 9 | * @property {number} id 10 | * @property {number} sends 11 | * @property {XMLHttpRequest} xhr 12 | */ 13 | declare class Request { 14 | /** 15 | * Create and initialize a new Request object. 16 | * 17 | * @param {Element} elem - The XML data to be sent in the request. 18 | * @param {Function} func - The function that will be called when the 19 | * XMLHttpRequest readyState changes. 20 | * @param {number} rid - The BOSH rid attribute associated with this request. 21 | * @param {number} [sends=0] - The number of times this same request has been sent. 22 | */ 23 | constructor(elem: Element, func: Function, rid: number, sends?: number); 24 | id: number; 25 | xmlData: Element; 26 | data: string; 27 | origFunc: Function; 28 | func: Function; 29 | rid: number; 30 | date: number; 31 | sends: number; 32 | abort: boolean; 33 | dead: any; 34 | age: () => number; 35 | timeDead: () => number; 36 | xhr: XMLHttpRequest; 37 | /** 38 | * Get a response from the underlying XMLHttpRequest. 39 | * This function attempts to get a response from the request and checks 40 | * for errors. 41 | * @throws "parsererror" - A parser error occured. 42 | * @throws "bad-format" - The entity has sent XML that cannot be processed. 43 | * @return {Element} - The DOM element tree of the response. 44 | */ 45 | getResponse(): Element; 46 | /** 47 | * _Private_ helper function to create XMLHttpRequests. 48 | * This function creates XMLHttpRequests across all implementations. 49 | * @private 50 | * @return {XMLHttpRequest} 51 | */ 52 | private _newXHR; 53 | } 54 | //# sourceMappingURL=request.d.ts.map -------------------------------------------------------------------------------- /src/timed-handler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * _Private_ helper class for managing timed handlers. 3 | * 4 | * A Strophe.TimedHandler encapsulates a user provided callback that 5 | * should be called after a certain period of time or at regular 6 | * intervals. The return value of the callback determines whether the 7 | * Strophe.TimedHandler will continue to fire. 8 | * 9 | * Users will not use Strophe.TimedHandler objects directly, but instead 10 | * they will use {@link Strophe.Connection#addTimedHandler|addTimedHandler()} and 11 | * {@link Strophe.Connection#deleteTimedHandler|deleteTimedHandler()}. 12 | * 13 | * @memberof Strophe 14 | */ 15 | class TimedHandler { 16 | /** 17 | * Create and initialize a new Strophe.TimedHandler object. 18 | * @param {number} period - The number of milliseconds to wait before the 19 | * handler is called. 20 | * @param {Function} handler - The callback to run when the handler fires. This 21 | * function should take no arguments. 22 | */ 23 | constructor(period, handler) { 24 | this.period = period; 25 | this.handler = handler; 26 | this.lastCalled = new Date().getTime(); 27 | this.user = true; 28 | } 29 | 30 | /** 31 | * Run the callback for the Strophe.TimedHandler. 32 | * 33 | * @return {boolean} Returns the result of running the handler, 34 | * which is `true` if the Strophe.TimedHandler should be called again, 35 | * and `false` otherwise. 36 | */ 37 | run() { 38 | this.lastCalled = new Date().getTime(); 39 | return this.handler(); 40 | } 41 | 42 | /** 43 | * Reset the last called time for the Strophe.TimedHandler. 44 | */ 45 | reset() { 46 | this.lastCalled = new Date().getTime(); 47 | } 48 | 49 | /** 50 | * Get a string representation of the Strophe.TimedHandler object. 51 | * @return {string} 52 | */ 53 | toString() { 54 | return '{TimedHandler: ' + this.handler + '(' + this.period + ')}'; 55 | } 56 | } 57 | 58 | export default TimedHandler; 59 | -------------------------------------------------------------------------------- /examples/main.js: -------------------------------------------------------------------------------- 1 | config.baseUrl = '../'; 2 | require.config(config); 3 | if (typeof require === 'function') { 4 | require(['jquery', 'strophe'], function ($, wrapper) { 5 | Strophe = wrapper.Strophe; 6 | 7 | var BOSH_SERVICE = 'http://bosh.metajack.im:5280/xmpp-httpbind'; 8 | var connection = null; 9 | 10 | function log(msg) { 11 | $('#log').append('
').append(document.createTextNode(msg)); 12 | } 13 | 14 | function rawInput(data) { 15 | log('RECV: ' + data); 16 | } 17 | 18 | function rawOutput(data) { 19 | log('SENT: ' + data); 20 | } 21 | 22 | function onConnect(status) { 23 | if (status == Strophe.Status.CONNECTING) { 24 | log('Strophe is connecting.'); 25 | } else if (status == Strophe.Status.CONNFAIL) { 26 | log('Strophe failed to connect.'); 27 | $('#connect').get(0).value = 'connect'; 28 | } else if (status == Strophe.Status.DISCONNECTING) { 29 | log('Strophe is disconnecting.'); 30 | } else if (status == Strophe.Status.DISCONNECTED) { 31 | log('Strophe is disconnected.'); 32 | $('#connect').get(0).value = 'connect'; 33 | } else if (status == Strophe.Status.CONNECTED) { 34 | log('Strophe is connected.'); 35 | connection.disconnect(); 36 | } 37 | } 38 | 39 | $(document).ready(function () { 40 | connection = new Strophe.Connection(BOSH_SERVICE); 41 | connection.rawInput = rawInput; 42 | connection.rawOutput = rawOutput; 43 | $('#connect').bind('click', function () { 44 | var button = $('#connect').get(0); 45 | if (button.value == 'connect') { 46 | button.value = 'disconnect'; 47 | connection.connect($('#jid').get(0).value, $('#pass').get(0).value, onConnect); 48 | } else { 49 | button.value = 'connect'; 50 | connection.disconnect(); 51 | } 52 | }); 53 | }); 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /src/types/stanza.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Tagged template literal function which generates {@link Stanza} objects 3 | * 4 | * @example 5 | * const pres = stx`${show}` 6 | * 7 | * connection.send(msg); 8 | * 9 | * @example 10 | * const msg = stx` 15 | * Hello world 16 | * `; 17 | * 18 | * connection.send(msg); 19 | * 20 | * @param {string[]} strings 21 | * @param {...any} values 22 | * @returns {Stanza} 23 | */ 24 | export function stx(strings: string[], ...values: any[]): Stanza; 25 | /** 26 | * A Stanza represents a XML element used in XMPP (commonly referred to as stanzas). 27 | */ 28 | export class Stanza extends Builder { 29 | /** 30 | * A directive which can be used to pass a string of XML as a value to the 31 | * stx tagged template literal. 32 | * 33 | * It's considered "unsafe" because it can pose a security risk if used with 34 | * untrusted input. 35 | * 36 | * @param {string} string 37 | * @returns {UnsafeXML} 38 | * @example 39 | * const status = 'I am busy!'; 40 | * const pres = stx` 41 | * 42 | * dnd 43 | * ${unsafeXML(status)} 44 | * `; 45 | * connection.send(pres); 46 | */ 47 | static unsafeXML(string: string): UnsafeXML; 48 | /** 49 | * Turns the passed-in string into an XML Element. 50 | * @param {string} string 51 | * @param {boolean} [throwErrorIfInvalidNS] 52 | * @returns {Element} 53 | */ 54 | static toElement(string: string, throwErrorIfInvalidNS?: boolean): Element; 55 | /** 56 | * @param {string[]} strings 57 | * @param {any[]} values 58 | */ 59 | constructor(strings: string[], values: any[]); 60 | #private; 61 | } 62 | import Builder from './builder.js'; 63 | declare class UnsafeXML extends String { 64 | } 65 | export {}; 66 | //# sourceMappingURL=stanza.d.ts.map -------------------------------------------------------------------------------- /examples/basic.js: -------------------------------------------------------------------------------- 1 | var BOSH_SERVICE = 'http://bosh.metajack.im:5280/xmpp-httpbind'; 2 | var connection = null; 3 | 4 | function log(msg, data) { 5 | var tr = document.createElement('tr'); 6 | var th = document.createElement('th'); 7 | th.setAttribute('style', 'text-align: left; vertical-align: top;'); 8 | var td; 9 | 10 | th.appendChild(document.createTextNode(msg)); 11 | tr.appendChild(th); 12 | 13 | if (data) { 14 | td = document.createElement('td'); 15 | pre = document.createElement('code'); 16 | pre.setAttribute('style', 'white-space: pre-wrap;'); 17 | td.appendChild(pre); 18 | pre.appendChild(document.createTextNode(vkbeautify.xml(data))); 19 | tr.appendChild(td); 20 | } else { 21 | th.setAttribute('colspan', '2'); 22 | } 23 | 24 | $('#log').append(tr); 25 | } 26 | 27 | function rawInput(data) { 28 | log('RECV', data); 29 | } 30 | 31 | function rawOutput(data) { 32 | log('SENT', data); 33 | } 34 | 35 | function onConnect(status) { 36 | if (status == Strophe.Status.CONNECTING) { 37 | log('Strophe is connecting.'); 38 | } else if (status == Strophe.Status.CONNFAIL) { 39 | log('Strophe failed to connect.'); 40 | $('#connect').get(0).value = 'connect'; 41 | } else if (status == Strophe.Status.DISCONNECTING) { 42 | log('Strophe is disconnecting.'); 43 | } else if (status == Strophe.Status.DISCONNECTED) { 44 | log('Strophe is disconnected.'); 45 | $('#connect').get(0).value = 'connect'; 46 | } else if (status == Strophe.Status.CONNECTED) { 47 | log('Strophe is connected.'); 48 | connection.disconnect(); 49 | } 50 | } 51 | 52 | $(document).ready(function () { 53 | connection = new Strophe.Connection(BOSH_SERVICE); 54 | connection.rawInput = rawInput; 55 | connection.rawOutput = rawOutput; 56 | 57 | $('#connect').bind('click', function () { 58 | var button = $('#connect').get(0); 59 | if (button.value == 'connect') { 60 | button.value = 'disconnect'; 61 | 62 | connection.connect($('#jid').get(0).value, $('#pass').get(0).value, onConnect); 63 | } else { 64 | button.value = 'connect'; 65 | connection.disconnect(); 66 | } 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/types/log.d.ts: -------------------------------------------------------------------------------- 1 | export default log; 2 | export type LogLevel = import("./constants").LogLevel; 3 | declare namespace log { 4 | /** 5 | * Library consumers can use this function to set the log level of Strophe. 6 | * The default log level is Strophe.LogLevel.INFO. 7 | * @param {LogLevel} level 8 | * @example Strophe.setLogLevel(Strophe.LogLevel.DEBUG); 9 | */ 10 | function setLogLevel(level: LogLevel): void; 11 | /** 12 | * 13 | * Please note that data sent and received over the wire is logged 14 | * via {@link Strophe.Connection#rawInput|Strophe.Connection.rawInput()} 15 | * and {@link Strophe.Connection#rawOutput|Strophe.Connection.rawOutput()}. 16 | * 17 | * The different levels and their meanings are 18 | * 19 | * DEBUG - Messages useful for debugging purposes. 20 | * INFO - Informational messages. This is mostly information like 21 | * 'disconnect was called' or 'SASL auth succeeded'. 22 | * WARN - Warnings about potential problems. This is mostly used 23 | * to report transient connection errors like request timeouts. 24 | * ERROR - Some error occurred. 25 | * FATAL - A non-recoverable fatal error occurred. 26 | * 27 | * @param {number} level - The log level of the log message. 28 | * This will be one of the values in Strophe.LOG_LEVELS. 29 | * @param {string} msg - The log message. 30 | */ 31 | function log(level: number, msg: string): void; 32 | /** 33 | * Log a message at the Strophe.LOG_LEVELS.DEBUG level. 34 | * @param {string} msg - The log message. 35 | */ 36 | function debug(msg: string): void; 37 | /** 38 | * Log a message at the Strophe.LOG_LEVELS.INFO level. 39 | * @param {string} msg - The log message. 40 | */ 41 | function info(msg: string): void; 42 | /** 43 | * Log a message at the Strophe.LOG_LEVELS.WARN level. 44 | * @param {string} msg - The log message. 45 | */ 46 | function warn(msg: string): void; 47 | /** 48 | * Log a message at the Strophe.LOG_LEVELS.ERROR level. 49 | * @param {string} msg - The log message. 50 | */ 51 | function error(msg: string): void; 52 | /** 53 | * Log a message at the Strophe.LOG_LEVELS.FATAL level. 54 | * @param {string} msg - The log message. 55 | */ 56 | function fatal(msg: string): void; 57 | } 58 | //# sourceMappingURL=log.d.ts.map -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Strophe.js 2 | 3 | ![Build Status](https://github.com/strophe/strophejs/actions/workflows/karma-tests.yml/badge.svg) 4 | 5 | Strophe.js is a JavaScript library for speaking XMPP via BOSH 6 | ([XEP 124](https://xmpp.org/extensions/xep-0124.html) 7 | and [XEP 206](https://xmpp.org/extensions/xep-0206.html)) and WebSockets 8 | ([RFC 7395](http://tools.ietf.org/html/rfc7395)). 9 | 10 | It runs in both NodeJS and in web browsers, and its purpose is to enable real-time 11 | XMPP applications. 12 | 13 | ## Quick Links 14 | 15 | * [Homepage](https://strophe.im/strophejs/) 16 | * [Documentation](https://strophe.im/strophejs/doc/2.0.0/files/strophe-umd-js.html) 17 | * [Mailing list](https://groups.google.com/g/strophe) 18 | * [Community Plugins](https://github.com/strophe/strophejs-plugins) 19 | 20 | ## Support in different environments 21 | 22 | ### Browsers 23 | 24 | Versions <= 1.2.16 have been tested on Firefox, Firefox for Android, IE, Safari, 25 | Mobile Safari, Chrome, Chrome for Android, Opera and the mobile Opera browser. 26 | 27 | Since version 1.3.0, support for IE < 11 has been dropped. 28 | 29 | ### Node.js 30 | 31 | When running in Node.js, you'll need to install these additional packages: 32 | 33 | ```bash 34 | npm install jsdom ws 35 | ``` 36 | 37 | These provide the required WebSocket and DOM APIs that are normally available in browsers. 38 | 39 | ### React Native 40 | 41 | Since version 1.6.0 the [WebCrypto](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) 42 | API (included by default in Browsers and NodeJS) is used for crypto primitives 43 | such as hashing and signatures. 44 | 45 | Unfortunately this API is not available in React Native, and integrators will 46 | need to look for a 3rd party implementations of this API if they want to use 47 | Strophe there. 48 | 49 | ## Running tests 50 | 51 | You can run `npm run test`, or alternatively if you have [GNU Make](https://www.gnu.org/software/make/) available, 52 | you can run `make check`. 53 | 54 | ## License 55 | 56 | Strophe.js is licensed under the [MIT license](https://github.com/strophe/strophejs/raw/master/LICENSE.txt). 57 | 58 | ## Author & History 59 | 60 | Strophe.js was created by Jack Moffitt. It was originally developed 61 | for Chesspark, an online chess community based on XMPP technology. It has been 62 | cared for and improved over the years and is currently maintained by many 63 | people in the community. 64 | 65 | The book [Professional XMPP Programming with JavaScript and jQuery](http://professionalxmpp.com) 66 | covers Strophe in detail in the context of web applications. 67 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DOC_DIR = doc 2 | DOC_TEMP = doc-temp 3 | HTTPSERVE ?= ./node_modules/http-server/bin/http-server 4 | HTTPSERVE_PORT ?= 8080 5 | ESLINT ?= ./node_modules/eslint/bin/eslint.js 6 | NDPROJ_DIR = ndproj 7 | SED ?= sed 8 | SHELL ?= /usr/env/bin/bash 9 | SRC_DIR = src 10 | STROPHE = dist/strophe.umd.js 11 | 12 | all: doc $(STROPHE) 13 | 14 | .PHONY: help 15 | help: 16 | @echo "Please use \`make ' where is one of the following:" 17 | @echo "" 18 | @echo " all Update docs + build strophe" 19 | @echo " doc Update docs" 20 | @echo " dist Build strophe" 21 | @echo " check Build and run the tests" 22 | @echo " eslint Check code quality" 23 | @echo " release Prepare a new release of strophe. E.g. \`make release VERSION=1.2.14\`" 24 | @echo " serve Serve this directory via a webserver on port 8000." 25 | @echo " node_modules Install all dependencies" 26 | @echo "" 27 | @echo "If you are a Mac user:\n 1. Install \`gnu-sed\` (brew install gnu-sed) \n 2. Set \`SED\` to \`gsed\` in all commands. E.g. \`make release SED=gsed VERSION=1.2.14\`" 28 | 29 | node_modules: package.json 30 | npm install 31 | 32 | .PHONY: doc 33 | doc: 34 | @@echo "Building Strophe documentation..." 35 | npx jsdoc -p -d $(DOC_DIR) -c jsdoc.json src/ 36 | @@echo "Documentation built." 37 | @@echo 38 | 39 | .PHONY: release 40 | release: 41 | $(SED) -i 's/\"version\":\ \"[0-9]\+\.[0-9]\+\.[0-9]\+\"/\"version\":\ \"$(VERSION)\"/' package.json 42 | $(SED) -i 's/VERSION:\ \"[0-9]\+\.[0-9]\+\.[0-9]\+\"/VERSION:\ \"$(VERSION)\"/' src/index.js 43 | $(SED) -i "s/Unreleased/`date +%Y-%m-%d`/" CHANGELOG.md 44 | make dist 45 | 46 | .PHONY: watchjs 47 | watchjs: node_modules 48 | ./node_modules/.bin/npx webpack --mode=development --watch 49 | 50 | dist/types: src/* 51 | npm run types 52 | 53 | .PHONY: dist 54 | dist: $(STROPHE) dist/types 55 | 56 | $(STROPHE): src/* rollup.config.js node_modules Makefile 57 | npm run build 58 | 59 | .PHONY: eslint 60 | eslint: node_modules 61 | $(ESLINT) src/ 62 | 63 | .PHONY: prettier 64 | prettier: node_modules 65 | npm run prettier 66 | 67 | .PHONY: check 68 | check:: node_modules eslint dist 69 | npm ci && npm run test 70 | 71 | .PHONY: test 72 | test: 73 | make check 74 | 75 | .PHONY: serve 76 | serve: node_modules 77 | $(HTTPSERVE) -p $(HTTPSERVE_PORT) 78 | 79 | .PHONY: serve_bg 80 | serve_bg: node_modules 81 | $(HTTPSERVE) -p $(HTTPSERVE_PORT) -c-1 -s & 82 | 83 | .PHONY: clean 84 | clean: 85 | @@rm -rf node_modules 86 | @@rm -f $(STROPHE) 87 | @@rm -f $(STROPHE_MIN) 88 | @@rm -f $(PLUGIN_FILES_MIN) 89 | @@rm -rf $(NDPROJ_DIR) $(DOC_DIR) $(DOC_TEMP) 90 | @@echo "Done." 91 | @@echo 92 | -------------------------------------------------------------------------------- /examples/restore.js: -------------------------------------------------------------------------------- 1 | config.baseUrl = '../'; 2 | require.config(config); 3 | if (typeof require === 'function') { 4 | require(['jquery', 'strophe'], function ($, wrapper) { 5 | Strophe = wrapper.Strophe; 6 | 7 | var button = document.getElementById('connect'); 8 | button.addEventListener('click', function () { 9 | if (button.value == 'connect') { 10 | button.value = 'disconnect'; 11 | connection.connect($('#jid').get(0).value, $('#pass').get(0).value, onConnect); 12 | } else { 13 | button.value = 'connect'; 14 | connection.disconnect(); 15 | } 16 | }); 17 | $(button).hide(); 18 | 19 | var BOSH_SERVICE = 'https://conversejs.org/http-bind/'; 20 | var connection = null; 21 | 22 | function log(msg) { 23 | $('#log').append('
').append(document.createTextNode(msg)); 24 | } 25 | 26 | function rawInput(data) { 27 | log('RECV: ' + data); 28 | } 29 | 30 | function rawOutput(data) { 31 | log('SENT: ' + data); 32 | } 33 | 34 | function onConnect(status) { 35 | if (status == Strophe.Status.CONNECTING) { 36 | log('Strophe is connecting.'); 37 | } else if (status == Strophe.Status.CONNFAIL) { 38 | log('Strophe failed to connect.'); 39 | $('#connect').get(0).value = 'connect'; 40 | } else if (status == Strophe.Status.DISCONNECTING) { 41 | log('Strophe is disconnecting.'); 42 | } else if (status == Strophe.Status.DISCONNECTED) { 43 | log('Strophe is disconnected.'); 44 | $('#connect').get(0).value = 'connect'; 45 | } else if (status == Strophe.Status.CONNECTED) { 46 | log('Strophe is connected.'); 47 | } else if (status == Strophe.Status.ATTACHED) { 48 | log('Strophe is attached.'); 49 | var button = $('#connect').get(0); 50 | button.value = 'disconnect'; 51 | $(button).show(); 52 | } 53 | } 54 | 55 | $(document).ready(function () { 56 | connection = new Strophe.Connection(BOSH_SERVICE, { 'keepalive': true }); 57 | connection.rawInput = rawInput; 58 | connection.rawOutput = rawOutput; 59 | try { 60 | connection.restore(null, onConnect); 61 | } catch (e) { 62 | if (e.name !== 'StropheSessionError') { 63 | throw e; 64 | } 65 | $(button).show(); 66 | } 67 | }); 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /examples/echobot.js: -------------------------------------------------------------------------------- 1 | /* global Strophe, $, $pres, $msg */ 2 | 3 | const BOSH_SERVICE = '/xmpp-httpbind'; 4 | let connection = null; 5 | 6 | function log(msg) { 7 | const log = document.querySelector('#log'); 8 | const div = document.createElement('div'); 9 | log.appendChild(div); 10 | div.appendChild(document.createTextNode(msg)); 11 | } 12 | 13 | function onConnect(status) { 14 | if (status == Strophe.Status.CONNECTING) { 15 | log('Strophe is connecting.'); 16 | } else if (status == Strophe.Status.CONNFAIL) { 17 | log('Strophe failed to connect.'); 18 | $('#connect').get(0).value = 'connect'; 19 | } else if (status == Strophe.Status.DISCONNECTING) { 20 | log('Strophe is disconnecting.'); 21 | } else if (status == Strophe.Status.DISCONNECTED) { 22 | log('Strophe is disconnected.'); 23 | $('#connect').get(0).value = 'connect'; 24 | } else if (status == Strophe.Status.CONNECTED) { 25 | log('Strophe is connected.'); 26 | log('ECHOBOT: Send a message to ' + connection.jid + ' to talk to me.'); 27 | 28 | connection.addHandler(onMessage, null, 'message', null, null, null); 29 | connection.send($pres().tree()); 30 | } 31 | } 32 | 33 | function onMessage(msg) { 34 | const to = msg.getAttribute('to'); 35 | const from = msg.getAttribute('from'); 36 | const type = msg.getAttribute('type'); 37 | const elems = msg.getElementsByTagName('body'); 38 | 39 | if (type == 'chat' && elems.length > 0) { 40 | const body = elems[0]; 41 | 42 | log('ECHOBOT: I got a message from ' + from + ': ' + Strophe.getText(body)); 43 | 44 | const reply = $msg({ to: from, from: to, type: 'chat' }).cnode(Strophe.copyElement(body)); 45 | connection.send(reply.tree()); 46 | 47 | log('ECHOBOT: I sent ' + from + ': ' + Strophe.getText(body)); 48 | } 49 | 50 | // we must return true to keep the handler alive. 51 | // returning false would remove it after it finishes. 52 | return true; 53 | } 54 | 55 | $(document).ready(function () { 56 | connection = new Strophe.Connection(BOSH_SERVICE); 57 | 58 | // Uncomment the following lines to spy on the wire traffic. 59 | //connection.rawInput = function (data) { log('RECV: ' + data); }; 60 | //connection.rawOutput = function (data) { log('SEND: ' + data); }; 61 | 62 | // Uncomment the following line to see all the debug output. 63 | //Strophe.log = function (level, msg) { log('LOG: ' + msg); }; 64 | 65 | $('#connect').bind('click', function () { 66 | const button = $('#connect').get(0); 67 | if (button.value == 'connect') { 68 | button.value = 'disconnect'; 69 | 70 | connection.connect($('#jid').get(0).value, $('#pass').get(0).value, onConnect); 71 | } else { 72 | button.value = 'connect'; 73 | connection.disconnect(); 74 | } 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /examples/attach/templates/attacher/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Strophe Attach Example 5 | 8 | 11 | 14 | 17 | 20 | 79 | 80 | 81 |

Strophe Attach Example

82 |

This example shows how to attach to an existing BOSH session with 83 | Strophe.

84 |

Log

85 |
86 |
87 | 88 | -------------------------------------------------------------------------------- /examples/attach/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for attach project. 2 | 3 | DEBUG = True 4 | TEMPLATE_DEBUG = DEBUG 5 | 6 | ADMINS = ( 7 | ('Some Body', 'romeo@example.com'), 8 | ) 9 | 10 | MANAGERS = ADMINS 11 | 12 | DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. 13 | DATABASE_NAME = '/path/to/attach.db' # Or path to database file if using sqlite3. 14 | DATABASE_USER = '' # Not used with sqlite3. 15 | DATABASE_PASSWORD = '' # Not used with sqlite3. 16 | DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3. 17 | DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3. 18 | 19 | # Local time zone for this installation. Choices can be found here: 20 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 21 | # although not all choices may be available on all operating systems. 22 | # If running in a Windows environment this must be set to the same as your 23 | # system time zone. 24 | TIME_ZONE = 'America/Denver' 25 | 26 | # Language code for this installation. All choices can be found here: 27 | # http://www.i18nguy.com/unicode/language-identifiers.html 28 | LANGUAGE_CODE = 'en-us' 29 | 30 | SITE_ID = 1 31 | 32 | # If you set this to False, Django will make some optimizations so as not 33 | # to load the internationalization machinery. 34 | USE_I18N = True 35 | 36 | # Absolute path to the directory that holds media. 37 | # Example: "/home/media/media.lawrence.com/" 38 | MEDIA_ROOT = '' 39 | 40 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 41 | # trailing slash if there is a path component (optional in other cases). 42 | # Examples: "http://media.lawrence.com", "http://example.com/media/" 43 | MEDIA_URL = '' 44 | 45 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a 46 | # trailing slash. 47 | # Examples: "http://foo.com/media/", "/media/". 48 | ADMIN_MEDIA_PREFIX = '/media/' 49 | 50 | # Make this unique, and don't share it with anybody. 51 | SECRET_KEY = 'asdf' 52 | 53 | # List of callables that know how to import templates from various sources. 54 | TEMPLATE_LOADERS = ( 55 | 'django.template.loaders.filesystem.load_template_source', 56 | 'django.template.loaders.app_directories.load_template_source', 57 | # 'django.template.loaders.eggs.load_template_source', 58 | ) 59 | 60 | MIDDLEWARE_CLASSES = ( 61 | 'django.middleware.common.CommonMiddleware', 62 | 'django.contrib.sessions.middleware.SessionMiddleware', 63 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 64 | ) 65 | 66 | ROOT_URLCONF = 'attach.urls' 67 | 68 | TEMPLATE_DIRS = ( 69 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 70 | # Always use forward slashes, even on Windows. 71 | # Don't forget to use absolute paths, not relative paths. 72 | '/path/to/attach/templates', 73 | ) 74 | 75 | INSTALLED_APPS = ( 76 | 'django.contrib.auth', 77 | 'django.contrib.contenttypes', 78 | 'django.contrib.sessions', 79 | 'django.contrib.sites', 80 | 'attach.attacher', 81 | ) 82 | 83 | BOSH_SERVICE = 'http://example.com/xmpp-httpbind' 84 | JABBERID = 'romeo@example.com/bosh' 85 | PASSWORD = 'juliet.is.hawt' 86 | -------------------------------------------------------------------------------- /src/types/handler.d.ts: -------------------------------------------------------------------------------- 1 | export default Handler; 2 | /** 3 | * _Private_ helper class for managing stanza handlers. 4 | * 5 | * A Handler encapsulates a user provided callback function to be 6 | * executed when matching stanzas are received by the connection. 7 | * Handlers can be either one-off or persistant depending on their 8 | * return value. Returning true will cause a Handler to remain active, and 9 | * returning false will remove the Handler. 10 | * 11 | * Users will not use Handler objects directly, but instead they 12 | * will use {@link Connection.addHandler} and 13 | * {@link Connection.deleteHandler}. 14 | */ 15 | declare class Handler { 16 | /** 17 | * @typedef {Object} HandlerOptions 18 | * @property {boolean} [HandlerOptions.matchBareFromJid] 19 | * @property {boolean} [HandlerOptions.ignoreNamespaceFragment] 20 | */ 21 | /** 22 | * Create and initialize a new Handler. 23 | * 24 | * @param {Function} handler - A function to be executed when the handler is run. 25 | * @param {string} ns - The namespace to match. 26 | * @param {string} name - The element name to match. 27 | * @param {string|string[]} type - The stanza type (or types if an array) to match. 28 | * @param {string} [id] - The element id attribute to match. 29 | * @param {string} [from] - The element from attribute to match. 30 | * @param {HandlerOptions} [options] - Handler options 31 | */ 32 | constructor(handler: Function, ns: string, name: string, type: string | string[], id?: string, from?: string, options?: { 33 | matchBareFromJid?: boolean; 34 | ignoreNamespaceFragment?: boolean; 35 | }); 36 | handler: Function; 37 | ns: string; 38 | name: string; 39 | type: string | string[]; 40 | id: string; 41 | options: { 42 | matchBareFromJid?: boolean; 43 | ignoreNamespaceFragment?: boolean; 44 | }; 45 | from: string; 46 | user: boolean; 47 | /** 48 | * Returns the XML namespace attribute on an element. 49 | * If `ignoreNamespaceFragment` was passed in for this handler, then the 50 | * URL fragment will be stripped. 51 | * @param {Element} elem - The XML element with the namespace. 52 | * @return {string} - The namespace, with optionally the fragment stripped. 53 | */ 54 | getNamespace(elem: Element): string; 55 | /** 56 | * Tests if a stanza element (or any of its children) matches the 57 | * namespace set for this Handler. 58 | * @param {Element} elem - The XML element to test. 59 | * @return {boolean} - true if the stanza matches and false otherwise. 60 | */ 61 | namespaceMatch(elem: Element): boolean; 62 | /** 63 | * Tests if a stanza matches the Handler. 64 | * @param {Element} elem - The XML element to test. 65 | * @return {boolean} - true if the stanza matches and false otherwise. 66 | */ 67 | isMatch(elem: Element): boolean; 68 | /** 69 | * Run the callback on a matching stanza. 70 | * @param {Element} elem - The DOM element that triggered the Handler. 71 | * @return {boolean} - A boolean indicating if the handler should remain active. 72 | */ 73 | run(elem: Element): boolean; 74 | /** 75 | * Get a String representation of the Handler object. 76 | * @return {string} 77 | */ 78 | toString(): string; 79 | } 80 | //# sourceMappingURL=handler.d.ts.map -------------------------------------------------------------------------------- /src/log.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('./constants').LogLevel} LogLevel 3 | */ 4 | import { LOG_LEVELS } from './constants.js'; 5 | 6 | let logLevel = LOG_LEVELS.DEBUG; 7 | 8 | const log = { 9 | /** 10 | * Library consumers can use this function to set the log level of Strophe. 11 | * The default log level is Strophe.LogLevel.INFO. 12 | * @param {LogLevel} level 13 | * @example Strophe.setLogLevel(Strophe.LogLevel.DEBUG); 14 | */ 15 | setLogLevel(level) { 16 | if (level < LOG_LEVELS.DEBUG || level > LOG_LEVELS.FATAL) { 17 | throw new Error("Invalid log level supplied to setLogLevel"); 18 | } 19 | logLevel = level; 20 | }, 21 | 22 | /** 23 | * 24 | * Please note that data sent and received over the wire is logged 25 | * via {@link Strophe.Connection#rawInput|Strophe.Connection.rawInput()} 26 | * and {@link Strophe.Connection#rawOutput|Strophe.Connection.rawOutput()}. 27 | * 28 | * The different levels and their meanings are 29 | * 30 | * DEBUG - Messages useful for debugging purposes. 31 | * INFO - Informational messages. This is mostly information like 32 | * 'disconnect was called' or 'SASL auth succeeded'. 33 | * WARN - Warnings about potential problems. This is mostly used 34 | * to report transient connection errors like request timeouts. 35 | * ERROR - Some error occurred. 36 | * FATAL - A non-recoverable fatal error occurred. 37 | * 38 | * @param {number} level - The log level of the log message. 39 | * This will be one of the values in Strophe.LOG_LEVELS. 40 | * @param {string} msg - The log message. 41 | */ 42 | log(level, msg) { 43 | if (level < logLevel) { 44 | return; 45 | } 46 | 47 | if (level >= LOG_LEVELS.ERROR) { 48 | console?.error(msg); 49 | } else if (level === LOG_LEVELS.INFO) { 50 | console?.info(msg); 51 | } else if (level === LOG_LEVELS.WARN) { 52 | console?.warn(msg); 53 | } else if (level === LOG_LEVELS.DEBUG) { 54 | console?.debug(msg); 55 | } 56 | }, 57 | 58 | /** 59 | * Log a message at the Strophe.LOG_LEVELS.DEBUG level. 60 | * @param {string} msg - The log message. 61 | */ 62 | debug(msg) { 63 | this.log(LOG_LEVELS.DEBUG, msg); 64 | }, 65 | 66 | /** 67 | * Log a message at the Strophe.LOG_LEVELS.INFO level. 68 | * @param {string} msg - The log message. 69 | */ 70 | info(msg) { 71 | this.log(LOG_LEVELS.INFO, msg); 72 | }, 73 | 74 | /** 75 | * Log a message at the Strophe.LOG_LEVELS.WARN level. 76 | * @param {string} msg - The log message. 77 | */ 78 | warn(msg) { 79 | this.log(LOG_LEVELS.WARN, msg); 80 | }, 81 | 82 | /** 83 | * Log a message at the Strophe.LOG_LEVELS.ERROR level. 84 | * @param {string} msg - The log message. 85 | */ 86 | error(msg) { 87 | this.log(LOG_LEVELS.ERROR, msg); 88 | }, 89 | 90 | /** 91 | * Log a message at the Strophe.LOG_LEVELS.FATAL level. 92 | * @param {string} msg - The log message. 93 | */ 94 | fatal(msg) { 95 | this.log(LOG_LEVELS.FATAL, msg); 96 | }, 97 | }; 98 | 99 | export default log; 100 | -------------------------------------------------------------------------------- /examples/prebind.js: -------------------------------------------------------------------------------- 1 | // http-pre-bind example 2 | // This example works with mod_http_pre_bind found here: 3 | // http://github.com/thepug/Mod-Http-Pre-Bind 4 | // 5 | // It expects both /xmpp-httpbind to be proxied and /http-pre-bind 6 | // 7 | // If you want to test this out without setting it up, you can use Collecta's 8 | // at http://www.collecta.com/xmpp-httpbind and 9 | // http://www.collecta.com/http-pre-bind 10 | // Use a JID of 'guest.collecta.com' to test. 11 | 12 | var BOSH_SERVICE = '/xmpp-httpbind'; 13 | var PREBIND_SERVICE = '/http-pre-bind'; 14 | var connection = null; 15 | 16 | function log(msg) { 17 | $('#log').append('
').append(document.createTextNode(msg)); 18 | } 19 | 20 | function rawInput(data) { 21 | log('RECV: ' + data); 22 | } 23 | 24 | function rawOutput(data) { 25 | log('SENT: ' + data); 26 | } 27 | 28 | function onConnect(status) { 29 | if (status === Strophe.Status.CONNECTING) { 30 | log('Strophe is connecting.'); 31 | } else if (status === Strophe.Status.CONNFAIL) { 32 | log('Strophe failed to connect.'); 33 | $('#connect').get(0).value = 'connect'; 34 | } else if (status === Strophe.Status.DISCONNECTING) { 35 | log('Strophe is disconnecting.'); 36 | } else if (status === Strophe.Status.DISCONNECTED) { 37 | log('Strophe is disconnected.'); 38 | $('#connect').get(0).value = 'connect'; 39 | } else if (status === Strophe.Status.CONNECTED) { 40 | log('Strophe is connected.'); 41 | connection.disconnect(); 42 | } else if (status === Strophe.Status.ATTACHED) { 43 | log('Strophe is attached.'); 44 | connection.disconnect(); 45 | } 46 | } 47 | 48 | function normal_connect() { 49 | log('Prebind failed. Connecting normally...'); 50 | 51 | connection = new Strophe.Connection(BOSH_SERVICE); 52 | connection.rawInput = rawInput; 53 | connection.rawOutput = rawOutput; 54 | 55 | connection.connect($('#jid').val(), $('#pass').val(), onConnect); 56 | } 57 | 58 | function attach(data) { 59 | log('Prebind succeeded. Attaching...'); 60 | 61 | connection = new Strophe.Connection(BOSH_SERVICE); 62 | connection.rawInput = rawInput; 63 | connection.rawOutput = rawOutput; 64 | 65 | var $body = $(data.documentElement); 66 | connection.attach($body.find('jid').text(), $body.attr('sid'), parseInt($body.attr('rid'), 10) + 1, onConnect); 67 | } 68 | 69 | $(document).ready(function () { 70 | $('#connect').bind('click', function () { 71 | var button = $('#connect').get(0); 72 | if (button.value == 'connect') { 73 | button.value = 'disconnect'; 74 | 75 | // attempt prebind 76 | $.ajax({ 77 | type: 'POST', 78 | url: PREBIND_SERVICE, 79 | contentType: 'text/xml', 80 | processData: false, 81 | data: $build('body', { 82 | to: Strophe.getDomainFromJid($('#jid').val()), 83 | rid: '' + Math.floor(Math.random() * 4294967295), 84 | wait: '60', 85 | hold: '1', 86 | }).toString(), 87 | dataType: 'xml', 88 | error: normal_connect, 89 | success: attach, 90 | }); 91 | } else { 92 | button.value = 'connect'; 93 | if (connection) { 94 | connection.disconnect(); 95 | } 96 | } 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import globals from 'rollup-plugin-node-globals'; 4 | import resolve from '@rollup/plugin-node-resolve'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | import { babelConfig } from './babel.config.json'; 7 | 8 | export default [ 9 | // Browser UMD build (unminified) 10 | { 11 | input: 'src/index.js', 12 | output: { 13 | name: 'Strophe', 14 | file: 'dist/strophe.js', 15 | format: 'umd', 16 | exports: 'named', 17 | globals: { 18 | 'ws': 'WebSocket', 19 | 'jsdom': 'JSDOM', 20 | }, 21 | }, 22 | plugins: [babel(babelConfig), resolve({ browser: true }), commonjs(), globals()], 23 | }, 24 | // Browser UMD build (minified) 25 | { 26 | input: 'src/index.js', 27 | output: { 28 | name: 'Strophe', 29 | file: 'dist/strophe.min.js', 30 | format: 'umd', 31 | exports: 'named', 32 | }, 33 | plugins: [babel(babelConfig), resolve({ browser: true }), commonjs(), globals(), terser()], 34 | }, 35 | // Browser ESM build 36 | { 37 | input: 'src/index.js', 38 | output: { 39 | file: 'dist/strophe.browser.esm.js', 40 | format: 'es', 41 | exports: 'named', 42 | }, 43 | plugins: [babel(babelConfig)], 44 | }, 45 | // Node.js ESM build 46 | { 47 | input: 'src/index.js', 48 | external: ['ws', 'jsdom'], 49 | output: { 50 | file: 'dist/strophe.node.esm.js', 51 | format: 'es', 52 | exports: 'named', 53 | }, 54 | plugins: [ 55 | { 56 | name: 'inject-shims', 57 | renderChunk(code) { 58 | return { 59 | code: 60 | `async function setupShims() { 61 | const { JSDOM } = await import('jsdom'); 62 | const { default: ws } = await import('ws'); 63 | const { window } = new JSDOM(); 64 | globalThis.WebSocket = ws; 65 | globalThis.XMLSerializer = window.XMLSerializer; 66 | globalThis.DOMParser = window.DOMParser; 67 | globalThis.document = window.document; 68 | } 69 | setupShims();` + code, 70 | map: { mappings: '' }, 71 | }; 72 | }, 73 | }, 74 | babel(babelConfig), 75 | ], 76 | }, 77 | // Node.js CJS build 78 | { 79 | input: 'src/index.js', 80 | external: ['ws', 'jsdom'], 81 | output: { 82 | file: 'dist/strophe.common.js', 83 | format: 'cjs', 84 | exports: 'named', 85 | intro: ` 86 | const { JSDOM } = require('jsdom'); 87 | const WebSocket = require('ws'); 88 | const { window } = new JSDOM(); 89 | globalThis.WebSocket = WebSocket; 90 | globalThis.XMLSerializer = window.XMLSerializer; 91 | globalThis.DOMParser = window.DOMParser; 92 | globalThis.document = window.document; 93 | `, 94 | }, 95 | plugins: [babel(babelConfig)], 96 | }, 97 | ]; 98 | -------------------------------------------------------------------------------- /src/shared-connection-worker.js: -------------------------------------------------------------------------------- 1 | /** @type {ConnectionManager} */ 2 | let manager; 3 | 4 | const Status = { 5 | ERROR: 0, 6 | CONNECTING: 1, 7 | CONNFAIL: 2, 8 | AUTHENTICATING: 3, 9 | AUTHFAIL: 4, 10 | CONNECTED: 5, 11 | DISCONNECTED: 6, 12 | DISCONNECTING: 7, 13 | ATTACHED: 8, 14 | REDIRECT: 9, 15 | CONNTIMEOUT: 10, 16 | BINDREQUIRED: 11, 17 | ATTACHFAIL: 12, 18 | }; 19 | 20 | /** Class: ConnectionManager 21 | * 22 | * Manages the shared websocket connection as well as the ports of the 23 | * connected tabs. 24 | */ 25 | class ConnectionManager { 26 | constructor() { 27 | /** @type {MessagePort[]} */ 28 | this.ports = []; 29 | } 30 | 31 | /** @param {MessagePort} port */ 32 | addPort(port) { 33 | this.ports.push(port); 34 | port.addEventListener('message', (e) => { 35 | const method = e.data[0]; 36 | try { 37 | this[/** @type {'send'|'_closeSocket'}*/ (method)](e.data.splice(1)); 38 | } catch (e) { 39 | console?.error(e); 40 | } 41 | }); 42 | port.start(); 43 | } 44 | 45 | /** 46 | * @param {[string, string]} data 47 | */ 48 | _connect(data) { 49 | this.jid = data[1]; 50 | this._closeSocket(); 51 | this.socket = new WebSocket(data[0], 'xmpp'); 52 | this.socket.onopen = () => this._onOpen(); 53 | this.socket.onerror = (e) => this._onError(e); 54 | this.socket.onclose = (e) => this._onClose(e); 55 | this.socket.onmessage = (message) => this._onMessage(message); 56 | } 57 | 58 | _attach() { 59 | if (this.socket && this.socket.readyState !== WebSocket.CLOSED) { 60 | this.ports.forEach((p) => p.postMessage(['_attachCallback', Status.ATTACHED, this.jid])); 61 | } else { 62 | this.ports.forEach((p) => p.postMessage(['_attachCallback', Status.ATTACHFAIL])); 63 | } 64 | } 65 | 66 | /** @param {string} str */ 67 | send(str) { 68 | this.socket.send(str); 69 | } 70 | 71 | /** @param {string} str */ 72 | close(str) { 73 | if (this.socket && this.socket.readyState !== WebSocket.CLOSED) { 74 | try { 75 | this.socket.send(str); 76 | } catch (e) { 77 | this.ports.forEach((p) => p.postMessage(['log', 'error', e])); 78 | this.ports.forEach((p) => p.postMessage(['log', 'error', "Couldn't send tag."])); 79 | } 80 | } 81 | } 82 | 83 | _onOpen() { 84 | this.ports.forEach((p) => p.postMessage(['_onOpen'])); 85 | } 86 | 87 | /** @param {CloseEvent} e */ 88 | _onClose(e) { 89 | this.ports.forEach((p) => p.postMessage(['_onClose', e.reason])); 90 | } 91 | 92 | /** @param {MessageEvent} message */ 93 | _onMessage(message) { 94 | const o = { 'data': message.data }; 95 | this.ports.forEach((p) => p.postMessage(['_onMessage', o])); 96 | } 97 | 98 | /** @param {Event} error */ 99 | _onError(error) { 100 | this.ports.forEach((p) => p.postMessage(['_onError', error])); 101 | } 102 | 103 | _closeSocket() { 104 | if (this.socket) { 105 | try { 106 | this.socket.onclose = null; 107 | this.socket.onerror = null; 108 | this.socket.onmessage = null; 109 | this.socket.close(); 110 | } catch (e) { 111 | this.ports.forEach((p) => p.postMessage(['log', 'error', e])); 112 | } 113 | } 114 | this.socket = null; 115 | } 116 | } 117 | 118 | addEventListener( 119 | 'connect', 120 | /** @param {MessageEvent} e */ 121 | (e) => { 122 | manager = manager || new ConnectionManager(); 123 | manager.addPort(e.ports[0]); 124 | } 125 | ); 126 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "strophe.js", 3 | "description": "Strophe.js is an XMPP library for JavaScript", 4 | "version": "4.0.0-rc0", 5 | "homepage": "http://strophe.im/strophejs", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/strophe/strophejs.git" 9 | }, 10 | "keywords": [ 11 | "xmpp", 12 | "message", 13 | "bosh", 14 | "websocket", 15 | "browser" 16 | ], 17 | "files": [ 18 | "src/", 19 | "dist/", 20 | "CHANGELOG.txt", 21 | "LICENSE.txt", 22 | "README.txt" 23 | ], 24 | "author": "Jack Moffit (metajack)", 25 | "contributors": [ 26 | "Nathan Zorn (thepug)", 27 | "Andreas Guth (Gordin)", 28 | "Anton Stroganov (Aeon)", 29 | "Florian Zeitz (Florob)", 30 | "Christopher Zorn (twonds)", 31 | "dodo", 32 | "Lee Boynton (lboynton)", 33 | "Theo Cushion (theozaurus)", 34 | "Brendon Crawford (brendoncrawford)", 35 | "JC Brand (jcbrand)" 36 | ], 37 | "license": "MIT", 38 | "browserslist": ">1%, maintained node versions", 39 | "main": "src/index.js", 40 | "types": "src/types/index.d.ts", 41 | "browser": "dist/strophe.umd.js", 42 | "module": "dist/strophe.esm.js", 43 | "unpkg": "dist/strophe.umd.min.js", 44 | "exports": { 45 | ".": { 46 | "types": "./src/types/index.d.ts", 47 | "browser": { 48 | "import": "./dist/strophe.browser.esm.js", 49 | "require": "./dist/strophe.js", 50 | "default": "./dist/strophe.js" 51 | }, 52 | "node": { 53 | "import": "./dist/strophe.node.esm.js", 54 | "require": "./dist/strophe.cjs.js", 55 | "default": "./dist/strophe.cjs.js" 56 | }, 57 | "default": "./dist/strophe.browser.esm.js" 58 | }, 59 | "./package.json": "./package.json" 60 | }, 61 | "scripts": { 62 | "types": "tsc", 63 | "build": "rollup -c", 64 | "lint": "eslint src/*.js tests/tests.js", 65 | "clean": "make clean", 66 | "doc": "make doc", 67 | "debug-node": "NODE_OPTIONS='--inspect-wait' qunit -w --require ./tests/node tests/tests.js", 68 | "test:node": "qunit --require ./tests/node tests/tests.js", 69 | "test:browser": "karma start", 70 | "test": "npm run test:node && npm run test:browser && npm run lint", 71 | "prettier": "prettier --write src/*.js" 72 | }, 73 | "volo": { 74 | "url": "https://raw.githubusercontent.com/strophe/strophejs/release-{version}/strophe.js" 75 | }, 76 | "devDependencies": { 77 | "@babel/core": "^7.18.5", 78 | "@babel/eslint-parser": "^7.19.1", 79 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", 80 | "@babel/plugin-proposal-optional-chaining": "^7.21.0", 81 | "@babel/preset-env": "^7.18.2", 82 | "@eslint/eslintrc": "^3.2.0", 83 | "@eslint/js": "^9.16.0", 84 | "@rollup/plugin-babel": "^6.0.3", 85 | "@rollup/plugin-commonjs": "^28.0.1", 86 | "@rollup/plugin-node-resolve": "^15.0.1", 87 | "@types/node": "^22.15.30", 88 | "@types/ws": "^8.18.1", 89 | "almond": "~0.3.0", 90 | "es6-promise": "^4.2.8", 91 | "eslint": "^9.16.0", 92 | "globals": "^15.13.0", 93 | "http-server": "^14.1.0", 94 | "jsdoc": "^4.0.2", 95 | "jsdom": "^25.0.1", 96 | "karma": "^6.3.17", 97 | "karma-chrome-launcher": "^3.1.1", 98 | "karma-qunit": "^4.1.2", 99 | "karma-rollup-preprocessor": "^7.0.8", 100 | "minimist": "^1.2.5", 101 | "prettier": "^3.5.3", 102 | "qunit": "2.24.1", 103 | "rollup": "^2.32.1", 104 | "rollup-plugin-node-globals": "^1.4.0", 105 | "rollup-plugin-terser": "^7.0.2", 106 | "run-headless-chromium": "^0.1.1", 107 | "sinon": "19.0.2", 108 | "terser": "^5.10.0", 109 | "typescript": "^5.1.6", 110 | "typescript-eslint": "^8.33.1", 111 | "ws": "^8.18.2", 112 | "xhr2": "^0.2.1" 113 | }, 114 | "peerDependencies": { 115 | "jsdom": ">=20.0.0", 116 | "ws": ">=8.0.0" 117 | }, 118 | "peerDependenciesMeta": { 119 | "jsdom": { 120 | "optional": true 121 | }, 122 | "ws": { 123 | "optional": true 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/types/sasl.d.ts: -------------------------------------------------------------------------------- 1 | export default SASLMechanism; 2 | export type Connection = import("./connection.js").default; 3 | /** 4 | * @typedef {import("./connection.js").default} Connection 5 | */ 6 | /** 7 | * Encapsulates an SASL authentication mechanism. 8 | * 9 | * User code may override the priority for each mechanism or disable it completely. 10 | * See for information about changing priority and for informatian on 11 | * how to disable a mechanism. 12 | * 13 | * By default, all mechanisms are enabled and t_he priorities are 14 | * 15 | * SCRAM-SHA-512 - 72 16 | * SCRAM-SHA-384 - 71 17 | * SCRAM-SHA-256 - 70 18 | * SCRAM-SHA-1 - 60 19 | * PLAIN - 50 20 | * OAUTHBEARER - 40 21 | * X-OAUTH2 - 30 22 | * ANONYMOUS - 20 23 | * EXTERNAL - 10 24 | * 25 | * See: {@link Strophe.Connection#registerSASLMechanisms} 26 | */ 27 | declare class SASLMechanism { 28 | /** 29 | * PrivateConstructor: Strophe.SASLMechanism 30 | * SASL auth mechanism abstraction. 31 | * @param {String} [name] - SASL Mechanism name. 32 | * @param {Boolean} [isClientFirst] - If client should send response first without challenge. 33 | * @param {Number} [priority] - Priority. 34 | */ 35 | constructor(name?: string, isClientFirst?: boolean, priority?: number); 36 | /** Mechanism name. */ 37 | mechname: string; 38 | /** 39 | * If client sends response without initial server challenge. 40 | */ 41 | isClientFirst: boolean; 42 | /** 43 | * Determines which {@link SASLMechanism} is chosen for authentication (Higher is better). 44 | * Users may override this to prioritize mechanisms differently. 45 | * 46 | * Example: (This will cause Strophe to choose the mechanism that the server sent first) 47 | * 48 | * > Strophe.SASLPlain.priority = Strophe.SASLSHA1.priority; 49 | * 50 | * See for a list of available mechanisms. 51 | */ 52 | priority: number; 53 | /** 54 | * Checks if mechanism able to run. 55 | * To disable a mechanism, make this return false; 56 | * 57 | * To disable plain authentication run 58 | * > Strophe.SASLPlain.test = function() { 59 | * > return false; 60 | * > } 61 | * 62 | * See for a list of available mechanisms. 63 | * @param {Connection} _connection - Target Connection. 64 | * @return {boolean} If mechanism was able to run. 65 | */ 66 | test(_connection: Connection): boolean; 67 | /** 68 | * Called before starting mechanism on some connection. 69 | * @param {Connection} connection - Target Connection. 70 | */ 71 | onStart(connection: Connection): void; 72 | _connection: import("./connection.js").default; 73 | /** 74 | * Called by protocol implementation on incoming challenge. 75 | * 76 | * By deafult, if the client is expected to send data first (isClientFirst === true), 77 | * this method is called with `challenge` as null on the first call, 78 | * unless `clientChallenge` is overridden in the relevant subclass. 79 | * @param {Connection} _connection - Target Connection. 80 | * @param {string} [_challenge] - current challenge to handle. 81 | * @return {string|Promise} Mechanism response. 82 | */ 83 | onChallenge(_connection: Connection, _challenge?: string): string | Promise; 84 | /** 85 | * Called by the protocol implementation if the client is expected to send 86 | * data first in the authentication exchange (i.e. isClientFirst === true). 87 | * @param {Connection} connection - Target Connection. 88 | * @return {string|Promise} Mechanism response. 89 | */ 90 | clientChallenge(connection: Connection): string | Promise; 91 | /** 92 | * Protocol informs mechanism implementation about SASL failure. 93 | */ 94 | onFailure(): void; 95 | /** 96 | * Protocol informs mechanism implementation about SASL success. 97 | */ 98 | onSuccess(): void; 99 | } 100 | //# sourceMappingURL=sasl.d.ts.map -------------------------------------------------------------------------------- /src/request.js: -------------------------------------------------------------------------------- 1 | import log from './log.js'; 2 | import Builder from './builder.js'; 3 | import { ErrorCondition } from './constants.js'; 4 | import { getParserError, xmlHtmlNode } from './utils.js'; 5 | 6 | /** 7 | * _Private_ variable that keeps track of the request ids for connections. 8 | */ 9 | let _requestId = 0; 10 | 11 | /** 12 | * Helper class that provides a cross implementation abstraction 13 | * for a BOSH related XMLHttpRequest. 14 | * 15 | * The Request class is used internally to encapsulate BOSH request 16 | * information. It is not meant to be used from user's code. 17 | * 18 | * @property {number} id 19 | * @property {number} sends 20 | * @property {XMLHttpRequest} xhr 21 | */ 22 | class Request { 23 | /** 24 | * Create and initialize a new Request object. 25 | * 26 | * @param {Element} elem - The XML data to be sent in the request. 27 | * @param {Function} func - The function that will be called when the 28 | * XMLHttpRequest readyState changes. 29 | * @param {number} rid - The BOSH rid attribute associated with this request. 30 | * @param {number} [sends=0] - The number of times this same request has been sent. 31 | */ 32 | constructor(elem, func, rid, sends = 0) { 33 | this.id = ++_requestId; 34 | this.xmlData = elem; 35 | this.data = Builder.serialize(elem); 36 | // save original function in case we need to make a new request 37 | // from this one. 38 | this.origFunc = func; 39 | this.func = func; 40 | this.rid = rid; 41 | this.date = NaN; 42 | this.sends = sends; 43 | this.abort = false; 44 | this.dead = null; 45 | 46 | this.age = () => (this.date ? (new Date().valueOf() - this.date.valueOf()) / 1000 : 0); 47 | this.timeDead = () => (this.dead ? (new Date().valueOf() - this.dead.valueOf()) / 1000 : 0); 48 | this.xhr = this._newXHR(); 49 | } 50 | 51 | /** 52 | * Get a response from the underlying XMLHttpRequest. 53 | * This function attempts to get a response from the request and checks 54 | * for errors. 55 | * @throws "parsererror" - A parser error occured. 56 | * @throws "bad-format" - The entity has sent XML that cannot be processed. 57 | * @return {Element} - The DOM element tree of the response. 58 | */ 59 | getResponse() { 60 | const node = this.xhr.responseXML?.documentElement; 61 | if (node) { 62 | if (node.tagName === 'parsererror') { 63 | log.error('invalid response received'); 64 | log.error('responseText: ' + this.xhr.responseText); 65 | log.error('responseXML: ' + Builder.serialize(node)); 66 | throw new Error('parsererror'); 67 | } 68 | } else if (this.xhr.responseText) { 69 | // In Node (with xhr2) or React Native, we may get responseText but no responseXML. 70 | // We can try to parse it manually. 71 | log.debug('Got responseText but no responseXML; attempting to parse it with DOMParser...'); 72 | 73 | const doc = xmlHtmlNode(this.xhr.responseText); 74 | const parserError = getParserError(doc); 75 | 76 | if (!doc || parserError) { 77 | if (parserError) { 78 | log.error('invalid response received: ' + parserError); 79 | log.error('responseText: ' + this.xhr.responseText); 80 | } 81 | const error = new Error(); 82 | error.name = ErrorCondition.BAD_FORMAT; 83 | throw error; 84 | } 85 | } 86 | return node; 87 | } 88 | 89 | /** 90 | * _Private_ helper function to create XMLHttpRequests. 91 | * This function creates XMLHttpRequests across all implementations. 92 | * @private 93 | * @return {XMLHttpRequest} 94 | */ 95 | _newXHR() { 96 | const xhr = new XMLHttpRequest(); 97 | if (xhr.overrideMimeType) { 98 | xhr.overrideMimeType('text/xml; charset=utf-8'); 99 | } 100 | // use Function.bind() to prepend ourselves as an argument 101 | xhr.onreadystatechange = this.func.bind(null, this); 102 | return xhr; 103 | } 104 | } 105 | 106 | export default Request; 107 | -------------------------------------------------------------------------------- /src/sasl.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import("./connection.js").default} Connection 3 | */ 4 | 5 | /** 6 | * Encapsulates an SASL authentication mechanism. 7 | * 8 | * User code may override the priority for each mechanism or disable it completely. 9 | * See for information about changing priority and for informatian on 10 | * how to disable a mechanism. 11 | * 12 | * By default, all mechanisms are enabled and t_he priorities are 13 | * 14 | * SCRAM-SHA-512 - 72 15 | * SCRAM-SHA-384 - 71 16 | * SCRAM-SHA-256 - 70 17 | * SCRAM-SHA-1 - 60 18 | * PLAIN - 50 19 | * OAUTHBEARER - 40 20 | * X-OAUTH2 - 30 21 | * ANONYMOUS - 20 22 | * EXTERNAL - 10 23 | * 24 | * See: {@link Strophe.Connection#registerSASLMechanisms} 25 | */ 26 | class SASLMechanism { 27 | /** 28 | * PrivateConstructor: Strophe.SASLMechanism 29 | * SASL auth mechanism abstraction. 30 | * @param {String} [name] - SASL Mechanism name. 31 | * @param {Boolean} [isClientFirst] - If client should send response first without challenge. 32 | * @param {Number} [priority] - Priority. 33 | */ 34 | constructor(name, isClientFirst, priority) { 35 | /** Mechanism name. */ 36 | this.mechname = name; 37 | 38 | /** 39 | * If client sends response without initial server challenge. 40 | */ 41 | this.isClientFirst = isClientFirst; 42 | 43 | /** 44 | * Determines which {@link SASLMechanism} is chosen for authentication (Higher is better). 45 | * Users may override this to prioritize mechanisms differently. 46 | * 47 | * Example: (This will cause Strophe to choose the mechanism that the server sent first) 48 | * 49 | * > Strophe.SASLPlain.priority = Strophe.SASLSHA1.priority; 50 | * 51 | * See for a list of available mechanisms. 52 | */ 53 | this.priority = priority; 54 | } 55 | 56 | /** 57 | * Checks if mechanism able to run. 58 | * To disable a mechanism, make this return false; 59 | * 60 | * To disable plain authentication run 61 | * > Strophe.SASLPlain.test = function() { 62 | * > return false; 63 | * > } 64 | * 65 | * See for a list of available mechanisms. 66 | * @param {Connection} _connection - Target Connection. 67 | * @return {boolean} If mechanism was able to run. 68 | */ 69 | test(_connection) { 70 | return true; 71 | } 72 | 73 | /** 74 | * Called before starting mechanism on some connection. 75 | * @param {Connection} connection - Target Connection. 76 | */ 77 | onStart(connection) { 78 | this._connection = connection; 79 | } 80 | 81 | /** 82 | * Called by protocol implementation on incoming challenge. 83 | * 84 | * By deafult, if the client is expected to send data first (isClientFirst === true), 85 | * this method is called with `challenge` as null on the first call, 86 | * unless `clientChallenge` is overridden in the relevant subclass. 87 | * @param {Connection} _connection - Target Connection. 88 | * @param {string} [_challenge] - current challenge to handle. 89 | * @return {string|Promise} Mechanism response. 90 | */ 91 | onChallenge(_connection, _challenge) { 92 | throw new Error('You should implement challenge handling!'); 93 | } 94 | 95 | /** 96 | * Called by the protocol implementation if the client is expected to send 97 | * data first in the authentication exchange (i.e. isClientFirst === true). 98 | * @param {Connection} connection - Target Connection. 99 | * @return {string|Promise} Mechanism response. 100 | */ 101 | clientChallenge(connection) { 102 | if (!this.isClientFirst) { 103 | throw new Error('clientChallenge should not be called if isClientFirst is false!'); 104 | } 105 | return this.onChallenge(connection); 106 | } 107 | 108 | /** 109 | * Protocol informs mechanism implementation about SASL failure. 110 | */ 111 | onFailure() { 112 | this._connection = null; 113 | } 114 | 115 | /** 116 | * Protocol informs mechanism implementation about SASL success. 117 | */ 118 | onSuccess() { 119 | this._connection = null; 120 | } 121 | } 122 | 123 | export default SASLMechanism; 124 | -------------------------------------------------------------------------------- /src/stanza.js: -------------------------------------------------------------------------------- 1 | import Builder from './builder.js'; 2 | import log from './log.js'; 3 | import { getFirstElementChild, getParserError, stripWhitespace, xmlHtmlNode, xmlescape } from './utils.js'; 4 | 5 | class UnsafeXML extends String {} 6 | 7 | /** 8 | * A Stanza represents a XML element used in XMPP (commonly referred to as stanzas). 9 | */ 10 | export class Stanza extends Builder { 11 | /** @type {string} */ 12 | #string; 13 | /** @type {Array} */ 14 | #strings; 15 | /** 16 | * @typedef {Array} StanzaValue 17 | * @type {StanzaValue|Array} 18 | */ 19 | #values; 20 | 21 | /** 22 | * @param {string[]} strings 23 | * @param {any[]} values 24 | */ 25 | constructor(strings, values) { 26 | super('stanza'); 27 | this.#strings = strings; 28 | this.#values = values; 29 | } 30 | 31 | /** 32 | * A directive which can be used to pass a string of XML as a value to the 33 | * stx tagged template literal. 34 | * 35 | * It's considered "unsafe" because it can pose a security risk if used with 36 | * untrusted input. 37 | * 38 | * @param {string} string 39 | * @returns {UnsafeXML} 40 | * @example 41 | * const status = 'I am busy!'; 42 | * const pres = stx` 43 | * 44 | * dnd 45 | * ${unsafeXML(status)} 46 | * `; 47 | * connection.send(pres); 48 | */ 49 | static unsafeXML(string) { 50 | return new UnsafeXML(string); 51 | } 52 | 53 | /** 54 | * Turns the passed-in string into an XML Element. 55 | * @param {string} string 56 | * @param {boolean} [throwErrorIfInvalidNS] 57 | * @returns {Element} 58 | */ 59 | static toElement(string, throwErrorIfInvalidNS) { 60 | const doc = xmlHtmlNode(string); 61 | const parserError = getParserError(doc); 62 | if (parserError) { 63 | throw new Error(`Parser Error: ${parserError}`); 64 | } 65 | 66 | const node = stripWhitespace(getFirstElementChild(doc)); 67 | if ( 68 | ['message', 'iq', 'presence'].includes(node.nodeName.toLowerCase()) && 69 | node.namespaceURI !== 'jabber:client' && 70 | node.namespaceURI !== 'jabber:server' 71 | ) { 72 | const err_msg = `Invalid namespaceURI ${node.namespaceURI}`; 73 | if (throwErrorIfInvalidNS) { 74 | throw new Error(err_msg); 75 | } else { 76 | log.error(err_msg); 77 | } 78 | } 79 | return node; 80 | } 81 | 82 | buildTree() { 83 | return Stanza.toElement(this.toString(), true); 84 | } 85 | 86 | /** 87 | * @return {string} 88 | */ 89 | toString() { 90 | this.#string = 91 | this.#string || 92 | this.#strings 93 | .reduce((acc, str, idx) => { 94 | const value = this.#values.length > idx ? this.#values[idx] : ''; 95 | return ( 96 | acc + 97 | str + 98 | (Array.isArray(value) 99 | ? value 100 | .map((v) => 101 | v instanceof UnsafeXML || v instanceof Builder 102 | ? v 103 | : xmlescape(v.toString()) 104 | ) 105 | .join('') 106 | : value instanceof UnsafeXML || value instanceof Builder 107 | ? value 108 | : xmlescape((value ?? '').toString())) 109 | ); 110 | }, '') 111 | .trim(); 112 | 113 | return this.#string; 114 | } 115 | } 116 | 117 | /** 118 | * Tagged template literal function which generates {@link Stanza} objects 119 | * 120 | * @example 121 | * const pres = stx`${show}` 122 | * 123 | * connection.send(msg); 124 | * 125 | * @example 126 | * const msg = stx` 131 | * Hello world 132 | * `; 133 | * 134 | * connection.send(msg); 135 | * 136 | * @param {string[]} strings 137 | * @param {...any} values 138 | * @returns {Stanza} 139 | */ 140 | export function stx(strings, ...values) { 141 | return new Stanza(strings, values); 142 | } 143 | -------------------------------------------------------------------------------- /src/handler.js: -------------------------------------------------------------------------------- 1 | import { getBareJidFromJid, handleError, isTagEqual } from './utils.js'; 2 | 3 | /** 4 | * _Private_ helper class for managing stanza handlers. 5 | * 6 | * A Handler encapsulates a user provided callback function to be 7 | * executed when matching stanzas are received by the connection. 8 | * Handlers can be either one-off or persistant depending on their 9 | * return value. Returning true will cause a Handler to remain active, and 10 | * returning false will remove the Handler. 11 | * 12 | * Users will not use Handler objects directly, but instead they 13 | * will use {@link Connection.addHandler} and 14 | * {@link Connection.deleteHandler}. 15 | */ 16 | class Handler { 17 | /** 18 | * @typedef {Object} HandlerOptions 19 | * @property {boolean} [HandlerOptions.matchBareFromJid] 20 | * @property {boolean} [HandlerOptions.ignoreNamespaceFragment] 21 | */ 22 | 23 | /** 24 | * Create and initialize a new Handler. 25 | * 26 | * @param {Function} handler - A function to be executed when the handler is run. 27 | * @param {string} ns - The namespace to match. 28 | * @param {string} name - The element name to match. 29 | * @param {string|string[]} type - The stanza type (or types if an array) to match. 30 | * @param {string} [id] - The element id attribute to match. 31 | * @param {string} [from] - The element from attribute to match. 32 | * @param {HandlerOptions} [options] - Handler options 33 | */ 34 | constructor(handler, ns, name, type, id, from, options) { 35 | this.handler = handler; 36 | this.ns = ns; 37 | this.name = name; 38 | this.type = type; 39 | this.id = id; 40 | this.options = options || { 'matchBareFromJid': false, 'ignoreNamespaceFragment': false }; 41 | if (this.options.matchBareFromJid) { 42 | this.from = from ? getBareJidFromJid(from) : null; 43 | } else { 44 | this.from = from; 45 | } 46 | // whether the handler is a user handler or a system handler 47 | this.user = true; 48 | } 49 | 50 | /** 51 | * Returns the XML namespace attribute on an element. 52 | * If `ignoreNamespaceFragment` was passed in for this handler, then the 53 | * URL fragment will be stripped. 54 | * @param {Element} elem - The XML element with the namespace. 55 | * @return {string} - The namespace, with optionally the fragment stripped. 56 | */ 57 | getNamespace(elem) { 58 | let elNamespace = elem.getAttribute('xmlns'); 59 | if (elNamespace && this.options.ignoreNamespaceFragment) { 60 | elNamespace = elNamespace.split('#')[0]; 61 | } 62 | return elNamespace; 63 | } 64 | 65 | /** 66 | * Tests if a stanza element (or any of its children) matches the 67 | * namespace set for this Handler. 68 | * @param {Element} elem - The XML element to test. 69 | * @return {boolean} - true if the stanza matches and false otherwise. 70 | */ 71 | namespaceMatch(elem) { 72 | if (!this.ns || this.getNamespace(elem) === this.ns) { 73 | return true; 74 | } 75 | for (const child of elem.children ?? []) { 76 | if (this.getNamespace(child) === this.ns) { 77 | return true; 78 | } else if (this.namespaceMatch(child)) { 79 | return true; 80 | } 81 | } 82 | return false; 83 | } 84 | 85 | /** 86 | * Tests if a stanza matches the Handler. 87 | * @param {Element} elem - The XML element to test. 88 | * @return {boolean} - true if the stanza matches and false otherwise. 89 | */ 90 | isMatch(elem) { 91 | let from = elem.getAttribute('from'); 92 | if (this.options.matchBareFromJid) { 93 | from = getBareJidFromJid(from); 94 | } 95 | const elem_type = elem.getAttribute('type'); 96 | if ( 97 | this.namespaceMatch(elem) && 98 | (!this.name || isTagEqual(elem, this.name)) && 99 | (!this.type || 100 | (Array.isArray(this.type) ? this.type.indexOf(elem_type) !== -1 : elem_type === this.type)) && 101 | (!this.id || elem.getAttribute('id') === this.id) && 102 | (!this.from || from === this.from) 103 | ) { 104 | return true; 105 | } 106 | return false; 107 | } 108 | 109 | /** 110 | * Run the callback on a matching stanza. 111 | * @param {Element} elem - The DOM element that triggered the Handler. 112 | * @return {boolean} - A boolean indicating if the handler should remain active. 113 | */ 114 | run(elem) { 115 | let result = null; 116 | try { 117 | result = this.handler(elem); 118 | } catch (e) { 119 | handleError(e); 120 | throw e; 121 | } 122 | return result; 123 | } 124 | 125 | /** 126 | * Get a String representation of the Handler object. 127 | * @return {string} 128 | */ 129 | toString() { 130 | return '{Handler: ' + this.handler + '(' + this.name + ',' + this.id + ',' + this.ns + ')}'; 131 | } 132 | } 133 | 134 | export default Handler; 135 | -------------------------------------------------------------------------------- /src/worker-websocket.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license MIT 3 | * @copyright JC Brand 4 | */ 5 | import Websocket from './websocket.js'; 6 | import log from './log.js'; 7 | import Builder, { $build } from './builder.js'; 8 | import { LOG_LEVELS, NS, Status } from './constants.js'; 9 | 10 | /** 11 | * Helper class that handles a websocket connection inside a shared worker. 12 | */ 13 | class WorkerWebsocket extends Websocket { 14 | /** 15 | * @typedef {import("./connection.js").default} Connection 16 | */ 17 | 18 | /** 19 | * Create and initialize a WorkerWebsocket object. 20 | * @param {Connection} connection - The Connection 21 | */ 22 | constructor(connection) { 23 | super(connection); 24 | this._conn = connection; 25 | this.worker = new SharedWorker(this._conn.options.worker, 'Strophe XMPP Connection'); 26 | this.worker.onerror = (e) => { 27 | console?.error(e); 28 | log.error(`Shared Worker Error: ${e}`); 29 | }; 30 | } 31 | 32 | /** 33 | * @private 34 | */ 35 | _setSocket() { 36 | this.socket = { 37 | /** @param {string} str */ 38 | send: (str) => this.worker.port.postMessage(['send', str]), 39 | close: () => this.worker.port.postMessage(['_closeSocket']), 40 | onopen: () => {}, 41 | /** @param {ErrorEvent} e */ 42 | onerror: (e) => this._onError(e), 43 | /** @param {CloseEvent} e */ 44 | onclose: (e) => this._onClose(e), 45 | onmessage: () => {}, 46 | readyState: null, 47 | }; 48 | } 49 | 50 | _connect() { 51 | this._setSocket(); 52 | /** @param {MessageEvent} m */ 53 | this._messageHandler = (m) => this._onInitialMessage(m); 54 | this.worker.port.start(); 55 | this.worker.port.onmessage = (ev) => this._onWorkerMessage(ev); 56 | this.worker.port.postMessage(['_connect', this._conn.service, this._conn.jid]); 57 | } 58 | 59 | /** 60 | * @param {Function} callback 61 | */ 62 | _attach(callback) { 63 | this._setSocket(); 64 | /** @param {MessageEvent} m */ 65 | this._messageHandler = (m) => this._onMessage(m); 66 | this._conn.connect_callback = callback; 67 | this.worker.port.start(); 68 | this.worker.port.onmessage = (ev) => this._onWorkerMessage(ev); 69 | this.worker.port.postMessage(['_attach', this._conn.service]); 70 | } 71 | 72 | /** 73 | * @param {number} status 74 | * @param {string} jid 75 | */ 76 | _attachCallback(status, jid) { 77 | if (status === Status.ATTACHED) { 78 | this._conn.jid = jid; 79 | this._conn.authenticated = true; 80 | this._conn.connected = true; 81 | this._conn.restored = true; 82 | this._conn._changeConnectStatus(Status.ATTACHED); 83 | } else if (status === Status.ATTACHFAIL) { 84 | this._conn.authenticated = false; 85 | this._conn.connected = false; 86 | this._conn.restored = false; 87 | this._conn._changeConnectStatus(Status.ATTACHFAIL); 88 | } 89 | } 90 | 91 | /** 92 | * @param {Element|Builder} pres - This stanza will be sent before disconnecting. 93 | */ 94 | _disconnect(pres) { 95 | pres && this._conn.send(pres); 96 | const close = $build('close', { 'xmlns': NS.FRAMING }); 97 | this._conn.xmlOutput(close.tree()); 98 | const closeString = Builder.serialize(close); 99 | this._conn.rawOutput(closeString); 100 | this.worker.port.postMessage(['send', closeString]); 101 | this._conn._doDisconnect(); 102 | } 103 | 104 | _closeSocket() { 105 | this.socket.close(); 106 | } 107 | 108 | /** 109 | * Called by _onInitialMessage in order to replace itself with the general message handler. 110 | * This method is overridden by WorkerWebsocket, which manages a 111 | * websocket connection via a service worker and doesn't have direct access 112 | * to the socket. 113 | */ 114 | _replaceMessageHandler() { 115 | /** @param {MessageEvent} m */ 116 | this._messageHandler = (m) => this._onMessage(m); 117 | } 118 | 119 | /** 120 | * function that handles messages received from the service worker 121 | * @private 122 | * @param {MessageEvent} ev 123 | */ 124 | _onWorkerMessage(ev) { 125 | const { data } = ev; 126 | const method_name = data[0]; 127 | if (method_name === '_onMessage') { 128 | this._messageHandler(data[1]); 129 | } else if (method_name in this) { 130 | try { 131 | this[ 132 | /** @type {'_attachCallback'|'_onOpen'|'_onClose'|'_onError'} */ 133 | (method_name) 134 | ].apply(this, ev.data.slice(1)); 135 | } catch (e) { 136 | log.error(e); 137 | } 138 | } else if (method_name === 'log') { 139 | /** @type {Object.} */ 140 | const lmap = { 141 | debug: LOG_LEVELS.DEBUG, 142 | info: LOG_LEVELS.INFO, 143 | warn: LOG_LEVELS.WARN, 144 | error: LOG_LEVELS.ERROR, 145 | fatal: LOG_LEVELS.FATAL, 146 | }; 147 | const level = data[1]; 148 | const msg = data[2]; 149 | log.log(lmap[level], msg); 150 | } else { 151 | log.error(`Found unhandled service worker message: ${data}`); 152 | } 153 | } 154 | } 155 | 156 | export default WorkerWebsocket; 157 | -------------------------------------------------------------------------------- /examples/attach/boshclient.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | import httplib, urllib 3 | import random, binascii 4 | from urlparse import urlparse 5 | 6 | from punjab.httpb import HttpbParse 7 | 8 | from twisted.words.xish import domish 9 | from twisted.words.protocols.jabber import jid 10 | 11 | TLS_XMLNS = 'urn:ietf:params:xml:ns:xmpp-tls' 12 | SASL_XMLNS = 'urn:ietf:params:xml:ns:xmpp-sasl' 13 | BIND_XMLNS = 'urn:ietf:params:xml:ns:xmpp-bind' 14 | SESSION_XMLNS = 'urn:ietf:params:xml:ns:xmpp-session' 15 | 16 | 17 | class BOSHClient: 18 | def __init__(self, jabberid, password, bosh_service): 19 | self.rid = random.randint(0, 10000000) 20 | self.jabberid = jid.internJID(jabberid) 21 | self.password = password 22 | 23 | self.authid = None 24 | self.sid = None 25 | self.logged_in = False 26 | self.headers = {"Content-type": "text/xml", 27 | "Accept": "text/xml"} 28 | 29 | self.bosh_service = urlparse(bosh_service) 30 | 31 | def buildBody(self, child=None): 32 | """Build a BOSH body. 33 | """ 34 | 35 | body = domish.Element(("http://jabber.org/protocol/httpbind", "body")) 36 | body['content'] = 'text/xml; charset=utf-8' 37 | self.rid = self.rid + 1 38 | body['rid'] = str(self.rid) 39 | body['sid'] = str(self.sid) 40 | body['xml:lang'] = 'en' 41 | 42 | if child is not None: 43 | body.addChild(child) 44 | 45 | return body 46 | 47 | def sendBody(self, body): 48 | """Send the body. 49 | """ 50 | 51 | parser = HttpbParse(True) 52 | 53 | # start new session 54 | conn = httplib.HTTPConnection(self.bosh_service.netloc) 55 | conn.request("POST", self.bosh_service.path, 56 | body.toXml(), self.headers) 57 | 58 | response = conn.getresponse() 59 | data = '' 60 | if response.status == 200: 61 | data = response.read() 62 | conn.close() 63 | 64 | return parser.parse(data) 65 | 66 | def startSessionAndAuth(self, hold='1', wait='70'): 67 | # Create a session 68 | # create body 69 | body = domish.Element(("http://jabber.org/protocol/httpbind", "body")) 70 | 71 | body['content'] = 'text/xml; charset=utf-8' 72 | body['hold'] = hold 73 | body['rid'] = str(self.rid) 74 | body['to'] = self.jabberid.host 75 | body['wait'] = wait 76 | body['window'] = '5' 77 | body['xml:lang'] = 'en' 78 | 79 | 80 | retb, elems = self.sendBody(body) 81 | if type(retb) != str and retb.hasAttribute('authid') and \ 82 | retb.hasAttribute('sid'): 83 | self.authid = retb['authid'] 84 | self.sid = retb['sid'] 85 | 86 | # go ahead and auth 87 | auth = domish.Element((SASL_XMLNS, 'auth')) 88 | auth['mechanism'] = 'PLAIN' 89 | 90 | # TODO: add authzid 91 | if auth['mechanism'] == 'PLAIN': 92 | auth_str = "" 93 | auth_str += "\000" 94 | auth_str += self.jabberid.user.encode('utf-8') 95 | auth_str += "\000" 96 | try: 97 | auth_str += self.password.encode('utf-8').strip() 98 | except UnicodeDecodeError: 99 | auth_str += self.password.decode('latin1') \ 100 | .encode('utf-8').strip() 101 | 102 | auth.addContent(binascii.b2a_base64(auth_str)) 103 | 104 | retb, elems = self.sendBody(self.buildBody(auth)) 105 | if len(elems) == 0: 106 | # poll for data 107 | retb, elems = self.sendBody(self.buildBody()) 108 | 109 | if len(elems) > 0: 110 | if elems[0].name == 'success': 111 | retb, elems = self.sendBody(self.buildBody()) 112 | 113 | has_bind = False 114 | for child in elems[0].children: 115 | if child.name == 'bind': 116 | has_bind = True 117 | break 118 | 119 | if has_bind: 120 | iq = domish.Element(('jabber:client', 'iq')) 121 | iq['type'] = 'set' 122 | iq.addUniqueId() 123 | iq.addElement('bind') 124 | iq.bind['xmlns'] = BIND_XMLNS 125 | if self.jabberid.resource: 126 | iq.bind.addElement('resource') 127 | iq.bind.resource.addContent( 128 | self.jabberid.resource) 129 | 130 | retb, elems = self.sendBody(self.buildBody(iq)) 131 | if type(retb) != str and retb.name == 'body': 132 | # send session 133 | iq = domish.Element(('jabber:client', 'iq')) 134 | iq['type'] = 'set' 135 | iq.addUniqueId() 136 | iq.addElement('session') 137 | iq.session['xmlns'] = SESSION_XMLNS 138 | 139 | retb, elems = self.sendBody(self.buildBody(iq)) 140 | 141 | # did not bind, TODO - add a retry? 142 | if type(retb) != str and retb.name == 'body': 143 | self.logged_in = True 144 | # bump up the rid, punjab already 145 | # received self.rid 146 | self.rid += 1 147 | 148 | 149 | if __name__ == '__main__': 150 | USERNAME = sys.argv[1] 151 | PASSWORD = sys.argv[2] 152 | SERVICE = sys.argv[3] 153 | 154 | c = BOSHClient(USERNAME, PASSWORD, SERVICE) 155 | c.startSessionAndAuth() 156 | 157 | print c.logged_in 158 | 159 | -------------------------------------------------------------------------------- /src/types/constants.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Common namespace constants from the XMPP RFCs and XEPs. 3 | */ 4 | export type NS = { 5 | /** 6 | * - HTTP BIND namespace from XEP 124. 7 | */ 8 | HTTPBIND: string; 9 | /** 10 | * - BOSH namespace from XEP 206. 11 | */ 12 | BOSH: string; 13 | /** 14 | * - Main XMPP client namespace. 15 | */ 16 | CLIENT: string; 17 | /** 18 | * - Legacy authentication namespace. 19 | */ 20 | AUTH: string; 21 | /** 22 | * - Roster operations namespace. 23 | */ 24 | ROSTER: string; 25 | /** 26 | * - Profile namespace. 27 | */ 28 | PROFILE: string; 29 | /** 30 | * - Service discovery info namespace from XEP 30. 31 | */ 32 | DISCO_INFO: string; 33 | /** 34 | * - Service discovery items namespace from XEP 30. 35 | */ 36 | DISCO_ITEMS: string; 37 | /** 38 | * - Multi-User Chat namespace from XEP 45. 39 | */ 40 | MUC: string; 41 | /** 42 | * - XMPP SASL namespace from RFC 3920. 43 | */ 44 | SASL: string; 45 | /** 46 | * - XMPP Streams namespace from RFC 3920. 47 | */ 48 | STREAM: string; 49 | /** 50 | * - XMPP Binding namespace from RFC 3920 and RFC 6120. 51 | */ 52 | BIND: string; 53 | /** 54 | * - XMPP Session namespace from RFC 3920. 55 | */ 56 | SESSION: string; 57 | /** 58 | * - XHTML-IM namespace from XEP 71. 59 | */ 60 | XHTML_IM: string; 61 | /** 62 | * - XHTML body namespace from XEP 71. 63 | */ 64 | XHTML: string; 65 | STANZAS: string; 66 | FRAMING: string; 67 | }; 68 | export namespace NS { 69 | let HTTPBIND: string; 70 | let BOSH: string; 71 | let CLIENT: string; 72 | let SERVER: string; 73 | let AUTH: string; 74 | let ROSTER: string; 75 | let PROFILE: string; 76 | let DISCO_INFO: string; 77 | let DISCO_ITEMS: string; 78 | let MUC: string; 79 | let SASL: string; 80 | let STREAM: string; 81 | let FRAMING: string; 82 | let BIND: string; 83 | let SESSION: string; 84 | let VERSION: string; 85 | let STANZAS: string; 86 | let XHTML_IM: string; 87 | let XHTML: string; 88 | } 89 | export const PARSE_ERROR_NS: "http://www.w3.org/1999/xhtml"; 90 | export namespace XHTML { 91 | let tags: string[]; 92 | namespace attributes { 93 | let a: string[]; 94 | let blockquote: string[]; 95 | let br: never[]; 96 | let cite: string[]; 97 | let em: never[]; 98 | let img: string[]; 99 | let li: string[]; 100 | let ol: string[]; 101 | let p: string[]; 102 | let span: string[]; 103 | let strong: never[]; 104 | let ul: string[]; 105 | let body: never[]; 106 | } 107 | let css: string[]; 108 | } 109 | /** 110 | * Connection status constants for use by the connection handler 111 | * callback. 112 | */ 113 | export type Status = { 114 | /** 115 | * - An error has occurred 116 | */ 117 | ERROR: connstatus; 118 | /** 119 | * - The connection is currently being made 120 | */ 121 | CONNECTING: connstatus; 122 | /** 123 | * - The connection attempt failed 124 | */ 125 | CONNFAIL: connstatus; 126 | /** 127 | * - The connection is authenticating 128 | */ 129 | AUTHENTICATING: connstatus; 130 | /** 131 | * - The authentication attempt failed 132 | */ 133 | AUTHFAIL: connstatus; 134 | /** 135 | * - The connection has succeeded 136 | */ 137 | CONNECTED: connstatus; 138 | /** 139 | * - The connection has been terminated 140 | */ 141 | DISCONNECTED: connstatus; 142 | /** 143 | * - The connection is currently being terminated 144 | */ 145 | DISCONNECTING: connstatus; 146 | /** 147 | * - The connection has been attached 148 | */ 149 | ATTACHED: connstatus; 150 | /** 151 | * - The connection has been redirected 152 | */ 153 | REDIRECT: connstatus; 154 | /** 155 | * - The connection has timed out 156 | */ 157 | CONNTIMEOUT: connstatus; 158 | /** 159 | * - The JID resource needs to be bound for this session 160 | */ 161 | BINDREQUIRED: connstatus; 162 | /** 163 | * - Failed to attach to a pre-existing session 164 | */ 165 | ATTACHFAIL: connstatus; 166 | /** 167 | * - Not used by Strophe, but added for integrators 168 | */ 169 | RECONNECTING: connstatus; 170 | }; 171 | export namespace Status { 172 | let ERROR: number; 173 | let CONNECTING: number; 174 | let CONNFAIL: number; 175 | let AUTHENTICATING: number; 176 | let AUTHFAIL: number; 177 | let CONNECTED: number; 178 | let DISCONNECTED: number; 179 | let DISCONNECTING: number; 180 | let ATTACHED: number; 181 | let REDIRECT: number; 182 | let CONNTIMEOUT: number; 183 | let BINDREQUIRED: number; 184 | let ATTACHFAIL: number; 185 | let RECONNECTING: number; 186 | } 187 | export namespace ErrorCondition { 188 | let BAD_FORMAT: string; 189 | let CONFLICT: string; 190 | let MISSING_JID_NODE: string; 191 | let NO_AUTH_MECH: string; 192 | let UNKNOWN_REASON: string; 193 | } 194 | export namespace LOG_LEVELS { 195 | export let DEBUG: number; 196 | export let INFO: number; 197 | export let WARN: number; 198 | let ERROR_1: number; 199 | export { ERROR_1 as ERROR }; 200 | export let FATAL: number; 201 | } 202 | export namespace ElementType { 203 | let NORMAL: number; 204 | let TEXT: number; 205 | let CDATA: number; 206 | let FRAGMENT: number; 207 | } 208 | export type connstatus = number; 209 | /** 210 | * Logging level indicators. 211 | */ 212 | export type LogLevel = 0 | 1 | 2 | 3 | 4; 213 | /** 214 | * Logging level indicators. 215 | */ 216 | export type LogLevelName = "DEBUG" | "INFO" | "WARN" | "ERROR" | "FATAL"; 217 | /** 218 | * Logging level indicators. 219 | */ 220 | export type LogLevels = Record; 221 | //# sourceMappingURL=constants.d.ts.map -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Common namespace constants from the XMPP RFCs and XEPs. 3 | * 4 | * @typedef { Object } NS 5 | * @property {string} NS.HTTPBIND - HTTP BIND namespace from XEP 124. 6 | * @property {string} NS.BOSH - BOSH namespace from XEP 206. 7 | * @property {string} NS.CLIENT - Main XMPP client namespace. 8 | * @property {string} NS.AUTH - Legacy authentication namespace. 9 | * @property {string} NS.ROSTER - Roster operations namespace. 10 | * @property {string} NS.PROFILE - Profile namespace. 11 | * @property {string} NS.DISCO_INFO - Service discovery info namespace from XEP 30. 12 | * @property {string} NS.DISCO_ITEMS - Service discovery items namespace from XEP 30. 13 | * @property {string} NS.MUC - Multi-User Chat namespace from XEP 45. 14 | * @property {string} NS.SASL - XMPP SASL namespace from RFC 3920. 15 | * @property {string} NS.STREAM - XMPP Streams namespace from RFC 3920. 16 | * @property {string} NS.BIND - XMPP Binding namespace from RFC 3920 and RFC 6120. 17 | * @property {string} NS.SESSION - XMPP Session namespace from RFC 3920. 18 | * @property {string} NS.XHTML_IM - XHTML-IM namespace from XEP 71. 19 | * @property {string} NS.XHTML - XHTML body namespace from XEP 71. 20 | * @property {string} NS.STANZAS 21 | * @property {string} NS.FRAMING 22 | */ 23 | export const NS = { 24 | HTTPBIND: 'http://jabber.org/protocol/httpbind', 25 | BOSH: 'urn:xmpp:xbosh', 26 | CLIENT: 'jabber:client', 27 | SERVER: 'jabber:server', 28 | AUTH: 'jabber:iq:auth', 29 | ROSTER: 'jabber:iq:roster', 30 | PROFILE: 'jabber:iq:profile', 31 | DISCO_INFO: 'http://jabber.org/protocol/disco#info', 32 | DISCO_ITEMS: 'http://jabber.org/protocol/disco#items', 33 | MUC: 'http://jabber.org/protocol/muc', 34 | SASL: 'urn:ietf:params:xml:ns:xmpp-sasl', 35 | STREAM: 'http://etherx.jabber.org/streams', 36 | FRAMING: 'urn:ietf:params:xml:ns:xmpp-framing', 37 | BIND: 'urn:ietf:params:xml:ns:xmpp-bind', 38 | SESSION: 'urn:ietf:params:xml:ns:xmpp-session', 39 | VERSION: 'jabber:iq:version', 40 | STANZAS: 'urn:ietf:params:xml:ns:xmpp-stanzas', 41 | XHTML_IM: 'http://jabber.org/protocol/xhtml-im', 42 | XHTML: 'http://www.w3.org/1999/xhtml', 43 | }; 44 | 45 | export const PARSE_ERROR_NS = 'http://www.w3.org/1999/xhtml'; 46 | 47 | /** 48 | * Contains allowed tags, tag attributes, and css properties. 49 | * Used in the {@link Strophe.createHtml} function to filter incoming html into the allowed XHTML-IM subset. 50 | * See [XEP-0071](http://xmpp.org/extensions/xep-0071.html#profile-summary) for the list of recommended 51 | * allowed tags and their attributes. 52 | */ 53 | export const XHTML = { 54 | tags: ['a', 'blockquote', 'br', 'cite', 'em', 'img', 'li', 'ol', 'p', 'span', 'strong', 'ul', 'body'], 55 | attributes: { 56 | 'a': ['href'], 57 | 'blockquote': ['style'], 58 | /** @type {never[]} */ 59 | 'br': [], 60 | 'cite': ['style'], 61 | /** @type {never[]} */ 62 | 'em': [], 63 | 'img': ['src', 'alt', 'style', 'height', 'width'], 64 | 'li': ['style'], 65 | 'ol': ['style'], 66 | 'p': ['style'], 67 | 'span': ['style'], 68 | /** @type {never[]} */ 69 | 'strong': [], 70 | 'ul': ['style'], 71 | /** @type {never[]} */ 72 | 'body': [], 73 | }, 74 | css: [ 75 | 'background-color', 76 | 'color', 77 | 'font-family', 78 | 'font-size', 79 | 'font-style', 80 | 'font-weight', 81 | 'margin-left', 82 | 'margin-right', 83 | 'text-align', 84 | 'text-decoration', 85 | ], 86 | }; 87 | 88 | /** @typedef {number} connstatus */ 89 | 90 | /** 91 | * Connection status constants for use by the connection handler 92 | * callback. 93 | * 94 | * @typedef {Object} Status 95 | * @property {connstatus} Status.ERROR - An error has occurred 96 | * @property {connstatus} Status.CONNECTING - The connection is currently being made 97 | * @property {connstatus} Status.CONNFAIL - The connection attempt failed 98 | * @property {connstatus} Status.AUTHENTICATING - The connection is authenticating 99 | * @property {connstatus} Status.AUTHFAIL - The authentication attempt failed 100 | * @property {connstatus} Status.CONNECTED - The connection has succeeded 101 | * @property {connstatus} Status.DISCONNECTED - The connection has been terminated 102 | * @property {connstatus} Status.DISCONNECTING - The connection is currently being terminated 103 | * @property {connstatus} Status.ATTACHED - The connection has been attached 104 | * @property {connstatus} Status.REDIRECT - The connection has been redirected 105 | * @property {connstatus} Status.CONNTIMEOUT - The connection has timed out 106 | * @property {connstatus} Status.BINDREQUIRED - The JID resource needs to be bound for this session 107 | * @property {connstatus} Status.ATTACHFAIL - Failed to attach to a pre-existing session 108 | * @property {connstatus} Status.RECONNECTING - Not used by Strophe, but added for integrators 109 | */ 110 | export const Status = { 111 | ERROR: 0, 112 | CONNECTING: 1, 113 | CONNFAIL: 2, 114 | AUTHENTICATING: 3, 115 | AUTHFAIL: 4, 116 | CONNECTED: 5, 117 | DISCONNECTED: 6, 118 | DISCONNECTING: 7, 119 | ATTACHED: 8, 120 | REDIRECT: 9, 121 | CONNTIMEOUT: 10, 122 | BINDREQUIRED: 11, 123 | ATTACHFAIL: 12, 124 | RECONNECTING: 13, 125 | }; 126 | 127 | export const ErrorCondition = { 128 | BAD_FORMAT: 'bad-format', 129 | CONFLICT: 'conflict', 130 | MISSING_JID_NODE: 'x-strophe-bad-non-anon-jid', 131 | NO_AUTH_MECH: 'no-auth-mech', 132 | UNKNOWN_REASON: 'unknown', 133 | }; 134 | 135 | /** 136 | * Logging level indicators. 137 | * @typedef {0|1|2|3|4} LogLevel 138 | * @typedef {'DEBUG'|'INFO'|'WARN'|'ERROR'|'FATAL'} LogLevelName 139 | * @typedef {Record} LogLevels 140 | */ 141 | export const LOG_LEVELS = { 142 | DEBUG: 0, 143 | INFO: 1, 144 | WARN: 2, 145 | ERROR: 3, 146 | FATAL: 4, 147 | }; 148 | 149 | /** 150 | * DOM element types. 151 | * 152 | * - ElementType.NORMAL - Normal element. 153 | * - ElementType.TEXT - Text data element. 154 | * - ElementType.FRAGMENT - XHTML fragment element. 155 | */ 156 | export const ElementType = { 157 | NORMAL: 1, 158 | TEXT: 3, 159 | CDATA: 4, 160 | FRAGMENT: 11, 161 | }; 162 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /*global globalThis */ 2 | import * as utils from './utils.js'; 3 | import Bosh from './bosh.js'; 4 | import Builder, { $build, $msg, $pres, $iq } from './builder.js'; 5 | import Connection from './connection.js'; 6 | import Handler from './handler.js'; 7 | import Request from './request.js'; 8 | import SASLAnonymous from './sasl-anon.js'; 9 | import SASLExternal from './sasl-external.js'; 10 | import SASLMechanism from './sasl.js'; 11 | import SASLOAuthBearer from './sasl-oauthbearer.js'; 12 | import SASLPlain from './sasl-plain.js'; 13 | import SASLSHA1 from './sasl-sha1.js'; 14 | import SASLSHA256 from './sasl-sha256.js'; 15 | import SASLSHA384 from './sasl-sha384.js'; 16 | import SASLSHA512 from './sasl-sha512.js'; 17 | import SASLXOAuth2 from './sasl-xoauth2.js'; 18 | import TimedHandler from './timed-handler.js'; 19 | import Websocket from './websocket.js'; 20 | import WorkerWebsocket from './worker-websocket.js'; 21 | import log from './log.js'; 22 | import { ElementType, ErrorCondition, LOG_LEVELS, NS, Status, XHTML } from './constants.js'; 23 | import { stx, Stanza } from './stanza.js'; 24 | 25 | /** 26 | * A container for all Strophe library functions. 27 | * 28 | * This object is a container for all the objects and constants 29 | * used in the library. It is not meant to be instantiated, but to 30 | * provide a namespace for library objects, constants, and functions. 31 | * 32 | * @namespace Strophe 33 | * @property {Handler} Handler 34 | * @property {Builder} Builder 35 | * @property {Request} Request Represents HTTP Requests made for a BOSH connection 36 | * @property {Bosh} Bosh Support for XMPP-over-HTTP via XEP-0124 (BOSH) 37 | * @property {Websocket} Websocket Support for XMPP over websocket 38 | * @property {WorkerWebsocket} WorkerWebsocket Support for XMPP over websocket in a shared worker 39 | * @property {number} TIMEOUT=1.1 Timeout multiplier. A waiting BOSH HTTP request 40 | * will be considered failed after Math.floor(TIMEOUT * wait) seconds have elapsed. 41 | * This defaults to 1.1, and with default wait, 66 seconds. 42 | * @property {number} SECONDARY_TIMEOUT=0.1 Secondary timeout multiplier. 43 | * In cases where Strophe can detect early failure, it will consider the request 44 | * failed if it doesn't return after `Math.floor(SECONDARY_TIMEOUT * wait)` 45 | * seconds have elapsed. This defaults to 0.1, and with default wait, 6 seconds. 46 | * @property {SASLAnonymous} SASLAnonymous SASL ANONYMOUS authentication. 47 | * @property {SASLPlain} SASLPlain SASL PLAIN authentication 48 | * @property {SASLSHA1} SASLSHA1 SASL SCRAM-SHA-1 authentication 49 | * @property {SASLSHA256} SASLSHA256 SASL SCRAM-SHA-256 authentication 50 | * @property {SASLSHA384} SASLSHA384 SASL SCRAM-SHA-384 authentication 51 | * @property {SASLSHA512} SASLSHA512 SASL SCRAM-SHA-512 authentication 52 | * @property {SASLOAuthBearer} SASLOAuthBearer SASL OAuth Bearer authentication 53 | * @property {SASLExternal} SASLExternal SASL EXTERNAL authentication 54 | * @property {SASLXOAuth2} SASLXOAuth2 SASL X-OAuth2 authentication 55 | * @property {Status} Status 56 | * @property {Object.} NS 57 | * @property {XHTML} XHTML 58 | */ 59 | const Strophe = { 60 | /** @constant: VERSION */ 61 | VERSION: '3.0.0', 62 | 63 | /** 64 | * @returns {number} 65 | */ 66 | get TIMEOUT() { 67 | return Bosh.getTimeoutMultplier(); 68 | }, 69 | 70 | /** 71 | * @param {number} n 72 | */ 73 | set TIMEOUT(n) { 74 | Bosh.setTimeoutMultiplier(n); 75 | }, 76 | 77 | /** 78 | * @returns {number} 79 | */ 80 | get SECONDARY_TIMEOUT() { 81 | return Bosh.getSecondaryTimeoutMultplier(); 82 | }, 83 | 84 | /** 85 | * @param {number} n 86 | */ 87 | set SECONDARY_TIMEOUT(n) { 88 | Bosh.setSecondaryTimeoutMultiplier(n); 89 | }, 90 | 91 | ...utils, 92 | ...log, 93 | 94 | Request, 95 | 96 | // Transports 97 | Bosh, 98 | Websocket, 99 | WorkerWebsocket, 100 | Connection, 101 | Handler, 102 | 103 | // Available authentication mechanisms 104 | SASLAnonymous, 105 | SASLPlain, 106 | SASLSHA1, 107 | SASLSHA256, 108 | SASLSHA384, 109 | SASLSHA512, 110 | SASLOAuthBearer, 111 | SASLExternal, 112 | SASLXOAuth2, 113 | 114 | Stanza, 115 | Builder, 116 | ElementType, 117 | ErrorCondition, 118 | LogLevel: LOG_LEVELS, 119 | /** @type {Object.} */ 120 | NS, 121 | SASLMechanism, 122 | /** @type {Status} */ 123 | Status, 124 | TimedHandler, 125 | 126 | XHTML: { 127 | ...XHTML, 128 | validTag: utils.validTag, 129 | validCSS: utils.validCSS, 130 | validAttribute: utils.validAttribute, 131 | }, 132 | 133 | /** 134 | * Render a DOM element and all descendants to a String. 135 | * @method Strophe.serialize 136 | * @param {Element|Builder} elem - A DOM element. 137 | * @return {string} - The serialized element tree as a String. 138 | */ 139 | serialize(elem) { 140 | return Builder.serialize(elem); 141 | }, 142 | 143 | /** 144 | * @typedef {import('./constants').LogLevel} LogLevel 145 | * 146 | * Library consumers can use this function to set the log level of Strophe. 147 | * The default log level is Strophe.LogLevel.INFO. 148 | * @param {LogLevel} level 149 | * @example Strophe.setLogLevel(Strophe.LogLevel.DEBUG); 150 | */ 151 | setLogLevel(level) { 152 | log.setLogLevel(level); 153 | }, 154 | 155 | /** 156 | * This function is used to extend the current namespaces in 157 | * Strophe.NS. It takes a key and a value with the key being the 158 | * name of the new namespace, with its actual value. 159 | * @example: Strophe.addNamespace('PUBSUB', "http://jabber.org/protocol/pubsub"); 160 | * 161 | * @param {string} name - The name under which the namespace will be 162 | * referenced under Strophe.NS 163 | * @param {string} value - The actual namespace. 164 | */ 165 | addNamespace(name, value) { 166 | Strophe.NS[name] = value; 167 | }, 168 | 169 | /** 170 | * Extends the Strophe.Connection object with the given plugin. 171 | * @param {string} name - The name of the extension. 172 | * @param {Object} ptype - The plugin's prototype. 173 | */ 174 | addConnectionPlugin(name, ptype) { 175 | Connection.addConnectionPlugin(name, ptype); 176 | }, 177 | }; 178 | 179 | globalThis.$build = $build; 180 | globalThis.$iq = $iq; 181 | globalThis.$msg = $msg; 182 | globalThis.$pres = $pres; 183 | globalThis.Strophe = Strophe; 184 | globalThis.stx = stx; 185 | 186 | const toStanza = Stanza.toElement; 187 | globalThis.toStanza = Stanza.toElement; // Deprecated 188 | 189 | export { Builder, $build, $iq, $msg, $pres, Strophe, Stanza, stx, toStanza, Request }; 190 | -------------------------------------------------------------------------------- /src/types/websocket.d.ts: -------------------------------------------------------------------------------- 1 | export default Websocket; 2 | export type Connection = import("./connection.js").default; 3 | /** 4 | * Helper class that handles WebSocket Connections 5 | * 6 | * The WebSocket class is used internally by Connection 7 | * to encapsulate WebSocket sessions. It is not meant to be used from user's code. 8 | */ 9 | declare class Websocket { 10 | /** 11 | * Create and initialize a WebSocket object. 12 | * Currently only sets the connection Object. 13 | * @param {Connection} connection - The Connection that will use WebSockets. 14 | */ 15 | constructor(connection: Connection); 16 | _conn: import("./connection.js").default; 17 | strip: string; 18 | /** 19 | * _Private_ helper function to generate the start tag for WebSockets 20 | * @private 21 | * @return {Builder} - A Builder with a element. 22 | */ 23 | private _buildStream; 24 | /** 25 | * _Private_ checks a message for stream:error 26 | * @private 27 | * @param {Element} bodyWrap - The received stanza. 28 | * @param {number} connectstatus - The ConnectStatus that will be set on error. 29 | * @return {boolean} - true if there was a streamerror, false otherwise. 30 | */ 31 | private _checkStreamError; 32 | /** 33 | * Reset the connection. 34 | * 35 | * This function is called by the reset function of the Strophe Connection. 36 | * Is not needed by WebSockets. 37 | */ 38 | _reset(): void; 39 | /** 40 | * _Private_ function called by Connection.connect 41 | * 42 | * Creates a WebSocket for a connection and assigns Callbacks to it. 43 | * Does nothing if there already is a WebSocket. 44 | */ 45 | _connect(): void; 46 | /** 47 | * @typedef {Object} WebsocketLike 48 | * @property {(str: string) => void} WebsocketLike.send 49 | * @property {function(): void} WebsocketLike.close 50 | * @property {function(): void} WebsocketLike.onopen 51 | * @property {(e: ErrorEvent) => void} WebsocketLike.onerror 52 | * @property {(e: CloseEvent) => void} WebsocketLike.onclose 53 | * @property {(message: MessageEvent) => void} WebsocketLike.onmessage 54 | * @property {string} WebsocketLike.readyState 55 | */ 56 | /** @type {import('ws')|WebSocket|WebsocketLike} */ 57 | socket: import("ws") | WebSocket | { 58 | send: (str: string) => void; 59 | close: () => void; 60 | onopen: () => void; 61 | onerror: (e: ErrorEvent) => void; 62 | onclose: (e: CloseEvent) => void; 63 | onmessage: (message: MessageEvent) => void; 64 | readyState: string; 65 | }; 66 | /** 67 | * _Private_ function called by Connection._connect_cb 68 | * checks for stream:error 69 | * @param {Element} bodyWrap - The received stanza. 70 | */ 71 | _connect_cb(bodyWrap: Element): number; 72 | /** 73 | * _Private_ function that checks the opening tag for errors. 74 | * 75 | * Disconnects if there is an error and returns false, true otherwise. 76 | * @private 77 | * @param {Element} message - Stanza containing the tag. 78 | */ 79 | private _handleStreamStart; 80 | /** 81 | * _Private_ function that handles the first connection messages. 82 | * 83 | * On receiving an opening stream tag this callback replaces itself with the real 84 | * message handler. On receiving a stream error the connection is terminated. 85 | * @param {MessageEvent} message 86 | */ 87 | _onInitialMessage(message: MessageEvent): void; 88 | /** 89 | * Called by _onInitialMessage in order to replace itself with the general message handler. 90 | * This method is overridden by WorkerWebsocket, which manages a 91 | * websocket connection via a service worker and doesn't have direct access 92 | * to the socket. 93 | */ 94 | _replaceMessageHandler(): void; 95 | /** 96 | * _Private_ function called by Connection.disconnect 97 | * Disconnects and sends a last stanza if one is given 98 | * @param {Element|Builder} [pres] - This stanza will be sent before disconnecting. 99 | */ 100 | _disconnect(pres?: Element | Builder): void; 101 | /** 102 | * _Private_ function to disconnect. 103 | * Just closes the Socket for WebSockets 104 | */ 105 | _doDisconnect(): void; 106 | /** 107 | * PrivateFunction _streamWrap 108 | * _Private_ helper function to wrap a stanza in a tag. 109 | * This is used so Strophe can process stanzas from WebSockets like BOSH 110 | * @param {string} stanza 111 | */ 112 | _streamWrap(stanza: string): string; 113 | /** 114 | * _Private_ function to close the WebSocket. 115 | * 116 | * Closes the socket if it is still open and deletes it 117 | */ 118 | _closeSocket(): void; 119 | /** 120 | * _Private_ function to check if the message queue is empty. 121 | * @return {true} - True, because WebSocket messages are send immediately after queueing. 122 | */ 123 | _emptyQueue(): true; 124 | /** 125 | * _Private_ function to handle websockets closing. 126 | * @param {CloseEvent} [e] 127 | */ 128 | _onClose(e?: CloseEvent): void; 129 | /** 130 | * @callback connectionCallback 131 | * @param {Connection} connection 132 | */ 133 | /** 134 | * Called on stream start/restart when no stream:features 135 | * has been received. 136 | * @param {connectionCallback} callback 137 | */ 138 | _no_auth_received(callback: (connection: Connection) => any): void; 139 | /** 140 | * _Private_ timeout handler for handling non-graceful disconnection. 141 | * 142 | * This does nothing for WebSockets 143 | */ 144 | _onDisconnectTimeout(): void; 145 | /** 146 | * _Private_ helper function that makes sure all pending requests are aborted. 147 | */ 148 | _abortAllRequests(): void; 149 | /** 150 | * _Private_ function to handle websockets errors. 151 | * @param {Object} error - The websocket error. 152 | */ 153 | _onError(error: Object): void; 154 | /** 155 | * _Private_ function called by Connection._onIdle 156 | * sends all queued stanzas 157 | */ 158 | _onIdle(): void; 159 | /** 160 | * _Private_ function to handle websockets messages. 161 | * 162 | * This function parses each of the messages as if they are full documents. 163 | * [TODO : We may actually want to use a SAX Push parser]. 164 | * 165 | * Since all XMPP traffic starts with 166 | * 172 | * 173 | * The first stanza will always fail to be parsed. 174 | * 175 | * Additionally, the seconds stanza will always be with 176 | * the stream NS defined in the previous stanza, so we need to 'force' 177 | * the inclusion of the NS in this stanza. 178 | * 179 | * @param {MessageEvent} message - The websocket message event 180 | */ 181 | _onMessage(message: MessageEvent): void; 182 | /** 183 | * _Private_ function to handle websockets connection setup. 184 | * The opening stream tag is sent here. 185 | * @private 186 | */ 187 | private _onOpen; 188 | /** 189 | * _Private_ part of the Connection.send function for WebSocket 190 | * Just flushes the messages that are in the queue 191 | */ 192 | _send(): void; 193 | /** 194 | * Send an xmpp:restart stanza. 195 | */ 196 | _sendRestart(): void; 197 | } 198 | import Builder from './builder.js'; 199 | //# sourceMappingURL=websocket.d.ts.map -------------------------------------------------------------------------------- /src/types/builder.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a {@link Strophe.Builder} 3 | * This is an alias for `new Strophe.Builder(name, attrs)`. 4 | * @param {string} name - The root element name. 5 | * @param {Object.} [attrs] - The attributes for the root element in object notation. 6 | * @return {Builder} A new Strophe.Builder object. 7 | */ 8 | export function $build(name: string, attrs?: { 9 | [x: string]: string | number; 10 | }): Builder; 11 | /** 12 | * Create a {@link Strophe.Builder} with a `` element as the root. 13 | * @param {Object.} [attrs] - The element attributes in object notation. 14 | * @return {Builder} A new Strophe.Builder object. 15 | */ 16 | export function $msg(attrs?: { 17 | [x: string]: string; 18 | }): Builder; 19 | /** 20 | * Create a {@link Strophe.Builder} with an `` element as the root. 21 | * @param {Object.} [attrs] - The element attributes in object notation. 22 | * @return {Builder} A new Strophe.Builder object. 23 | */ 24 | export function $iq(attrs?: { 25 | [x: string]: string; 26 | }): Builder; 27 | /** 28 | * Create a {@link Strophe.Builder} with a `` element as the root. 29 | * @param {Object.} [attrs] - The element attributes in object notation. 30 | * @return {Builder} A new Strophe.Builder object. 31 | */ 32 | export function $pres(attrs?: { 33 | [x: string]: string; 34 | }): Builder; 35 | export default Builder; 36 | /** 37 | * This class provides an interface similar to JQuery but for building 38 | * DOM elements easily and rapidly. All the functions except for `toString()` 39 | * and tree() return the object, so calls can be chained. 40 | * 41 | * The corresponding DOM manipulations to get a similar fragment would be 42 | * a lot more tedious and probably involve several helper variables. 43 | * 44 | * Since adding children makes new operations operate on the child, up() 45 | * is provided to traverse up the tree. To add two children, do 46 | * > builder.c('child1', ...).up().c('child2', ...) 47 | * 48 | * The next operation on the Builder will be relative to the second child. 49 | * 50 | * @example 51 | * // Here's an example using the $iq() builder helper. 52 | * $iq({to: 'you', from: 'me', type: 'get', id: '1'}) 53 | * .c('query', {xmlns: 'strophe:example'}) 54 | * .c('example') 55 | * .toString() 56 | * 57 | * // The above generates this XML fragment 58 | * // 59 | * // 60 | * // 61 | * // 62 | * // 63 | */ 64 | declare class Builder { 65 | /** 66 | * Creates a new Builder object from an XML string. 67 | * @param {string} str 68 | * @returns {Builder} 69 | * @example const stanza = Builder.fromString(''); 70 | */ 71 | static fromString(str: string): Builder; 72 | /** 73 | * Render a DOM element and all descendants to a String. 74 | * @param {Element|Builder} elem - A DOM element. 75 | * @return {string} - The serialized element tree as a String. 76 | */ 77 | static serialize(elem: Element | Builder): string; 78 | /** 79 | * The attributes should be passed in object notation. 80 | * @param {string} name - The name of the root element. 81 | * @param {StanzaAttrs} [attrs] - The attributes for the root element in object notation. 82 | * @example const b = new Builder('message', {to: 'you', from: 'me'}); 83 | * @example const b = new Builder('messsage', {'xml:lang': 'en'}); 84 | */ 85 | constructor(name: string, attrs?: { 86 | [x: string]: string | number; 87 | }); 88 | buildTree(): Element; 89 | /** @return {Element} */ 90 | get nodeTree(): Element; 91 | /** @param {Element} el */ 92 | set node(el: Element); 93 | /** @return {Element} */ 94 | get node(): Element; 95 | /** 96 | * Return the DOM tree. 97 | * 98 | * This function returns the current DOM tree as an element object. This 99 | * is suitable for passing to functions like Strophe.Connection.send(). 100 | * 101 | * @return {Element} The DOM tree as a element object. 102 | */ 103 | tree(): Element; 104 | /** 105 | * Serialize the DOM tree to a String. 106 | * 107 | * This function returns a string serialization of the current DOM 108 | * tree. It is often used internally to pass data to a 109 | * Strophe.Request object. 110 | * 111 | * @return {string} The serialized DOM tree in a String. 112 | */ 113 | toString(): string; 114 | /** 115 | * Make the current parent element the new current element. 116 | * This function is often used after c() to traverse back up the tree. 117 | * 118 | * @example 119 | * // For example, to add two children to the same element 120 | * builder.c('child1', {}).up().c('child2', {}); 121 | * 122 | * @return {Builder} The Strophe.Builder object. 123 | */ 124 | up(): Builder; 125 | /** 126 | * Make the root element the new current element. 127 | * 128 | * When at a deeply nested element in the tree, this function can be used 129 | * to jump back to the root of the tree, instead of having to repeatedly 130 | * call up(). 131 | * 132 | * @return {Builder} The Strophe.Builder object. 133 | */ 134 | root(): Builder; 135 | /** 136 | * Add or modify attributes of the current element. 137 | * 138 | * The attributes should be passed in object notation. 139 | * This function does not move the current element pointer. 140 | * @param {Object.} moreattrs - The attributes to add/modify in object notation. 141 | * If an attribute is set to `null` or `undefined`, it will be removed. 142 | * @return {Builder} The Strophe.Builder object. 143 | */ 144 | attrs(moreattrs: { 145 | [x: string]: string | number; 146 | }): Builder; 147 | /** 148 | * Add a child to the current element and make it the new current 149 | * element. 150 | * 151 | * This function moves the current element pointer to the child, 152 | * unless text is provided. If you need to add another child, it 153 | * is necessary to use up() to go back to the parent in the tree. 154 | * 155 | * @param {string} name - The name of the child. 156 | * @param {Object.|string} [attrs] - The attributes of the child in object notation. 157 | * @param {string} [text] - The text to add to the child. 158 | * 159 | * @return {Builder} The Strophe.Builder object. 160 | */ 161 | c(name: string, attrs?: { 162 | [x: string]: string; 163 | } | string, text?: string): Builder; 164 | /** 165 | * Add a child to the current element and make it the new current 166 | * element. 167 | * 168 | * This function is the same as c() except that instead of using a 169 | * name and an attributes object to create the child it uses an 170 | * existing DOM element object. 171 | * 172 | * @param {Element|Builder} elem - A DOM element. 173 | * @return {Builder} The Strophe.Builder object. 174 | */ 175 | cnode(elem: Element | Builder): Builder; 176 | /** 177 | * Add a child text element. 178 | * 179 | * This *does not* make the child the new current element since there 180 | * are no children of text elements. 181 | * 182 | * @param {string} text - The text data to append to the current element. 183 | * @return {Builder} The Strophe.Builder object. 184 | */ 185 | t(text: string): Builder; 186 | /** 187 | * Replace current element contents with the HTML passed in. 188 | * 189 | * This *does not* make the child the new current element 190 | * 191 | * @param {string} html - The html to insert as contents of current element. 192 | * @return {Builder} The Strophe.Builder object. 193 | */ 194 | h(html: string): Builder; 195 | #private; 196 | } 197 | //# sourceMappingURL=builder.d.ts.map -------------------------------------------------------------------------------- /src/scram.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import("./connection.js").default} Connection 3 | */ 4 | import utils from './utils'; 5 | import log from './log.js'; 6 | 7 | /** 8 | * @param {string} authMessage 9 | * @param {ArrayBufferLike} clientKey 10 | * @param {string} hashName 11 | */ 12 | async function scramClientProof(authMessage, clientKey, hashName) { 13 | const storedKey = await crypto.subtle.importKey( 14 | 'raw', 15 | await crypto.subtle.digest(hashName, clientKey), 16 | { 'name': 'HMAC', 'hash': hashName }, 17 | false, 18 | ['sign'] 19 | ); 20 | const clientSignature = await crypto.subtle.sign('HMAC', storedKey, utils.stringToArrayBuf(authMessage)); 21 | 22 | return utils.xorArrayBuffers(clientKey, clientSignature); 23 | } 24 | 25 | /** 26 | * This function parses the information in a SASL SCRAM challenge response, 27 | * into an object of the form 28 | * { nonce: String, 29 | * salt: ArrayBuffer, 30 | * iter: Int 31 | * } 32 | * Returns undefined on failure. 33 | * @param {string} challenge 34 | */ 35 | function scramParseChallenge(challenge) { 36 | let nonce, salt, iter; 37 | const attribMatch = /([a-z]+)=([^,]+)(,|$)/; 38 | while (challenge.match(attribMatch)) { 39 | const matches = challenge.match(attribMatch); 40 | challenge = challenge.replace(matches[0], ''); 41 | switch (matches[1]) { 42 | case 'r': 43 | nonce = matches[2]; 44 | break; 45 | case 's': 46 | salt = utils.base64ToArrayBuf(matches[2]); 47 | break; 48 | case 'i': 49 | iter = parseInt(matches[2], 10); 50 | break; 51 | case 'm': 52 | // Mandatory but unknown extension, per RFC 5802 we should abort 53 | return undefined; 54 | default: 55 | // Non-mandatory extension, per RFC 5802 we should ignore it 56 | break; 57 | } 58 | } 59 | 60 | // Consider iteration counts less than 4096 insecure, as reccommended by 61 | // RFC 5802 62 | if (isNaN(iter) || iter < 4096) { 63 | log.warn('Failing SCRAM authentication because server supplied iteration count < 4096.'); 64 | return undefined; 65 | } 66 | 67 | if (!salt) { 68 | log.warn('Failing SCRAM authentication because server supplied incorrect salt.'); 69 | return undefined; 70 | } 71 | 72 | return { 'nonce': nonce, 'salt': salt, 'iter': iter }; 73 | } 74 | 75 | /** 76 | * Derive the client and server keys given a string password, 77 | * a hash name, and a bit length. 78 | * Returns an object of the following form: 79 | * { ck: ArrayBuffer, the client key 80 | * sk: ArrayBuffer, the server key 81 | * } 82 | * @param {string} password 83 | * @param {BufferSource} salt 84 | * @param {number} iter 85 | * @param {string} hashName 86 | * @param {number} hashBits 87 | */ 88 | async function scramDeriveKeys(password, salt, iter, hashName, hashBits) { 89 | const saltedPasswordBits = await crypto.subtle.deriveBits( 90 | { 'name': 'PBKDF2', 'salt': salt, 'iterations': iter, 'hash': { 'name': hashName } }, 91 | await crypto.subtle.importKey('raw', utils.stringToArrayBuf(password), 'PBKDF2', false, ['deriveBits']), 92 | hashBits 93 | ); 94 | const saltedPassword = await crypto.subtle.importKey( 95 | 'raw', 96 | saltedPasswordBits, 97 | { 'name': 'HMAC', 'hash': hashName }, 98 | false, 99 | ['sign'] 100 | ); 101 | 102 | return { 103 | 'ck': await crypto.subtle.sign('HMAC', saltedPassword, utils.stringToArrayBuf('Client Key')), 104 | 'sk': await crypto.subtle.sign('HMAC', saltedPassword, utils.stringToArrayBuf('Server Key')), 105 | }; 106 | } 107 | 108 | /** 109 | * @param {string} authMessage 110 | * @param {BufferSource} sk 111 | * @param {string} hashName 112 | */ 113 | async function scramServerSign(authMessage, sk, hashName) { 114 | const serverKey = await crypto.subtle.importKey('raw', sk, { 'name': 'HMAC', 'hash': hashName }, false, ['sign']); 115 | 116 | return crypto.subtle.sign('HMAC', serverKey, utils.stringToArrayBuf(authMessage)); 117 | } 118 | 119 | /** 120 | * Generate an ASCII nonce (not containing the ',' character) 121 | * @return {string} 122 | */ 123 | function generate_cnonce() { 124 | // generate 16 random bytes of nonce, base64 encoded 125 | const bytes = new Uint8Array(16); 126 | return utils.arrayBufToBase64(crypto.getRandomValues(bytes).buffer); 127 | } 128 | 129 | /** 130 | * @typedef {Object} Password 131 | * @property {string} Password.name 132 | * @property {string} Password.ck 133 | * @property {string} Password.sk 134 | * @property {number} Password.iter 135 | * @property {string} salt 136 | */ 137 | 138 | const scram = { 139 | /** 140 | * On success, sets 141 | * connection_sasl_data["server-signature"] 142 | * and 143 | * connection._sasl_data.keys 144 | * 145 | * The server signature should be verified after this function completes.. 146 | * 147 | * On failure, returns connection._sasl_failure_cb(); 148 | * @param {Connection} connection 149 | * @param {string} challenge 150 | * @param {string} hashName 151 | * @param {number} hashBits 152 | */ 153 | async scramResponse(connection, challenge, hashName, hashBits) { 154 | const cnonce = connection._sasl_data.cnonce; 155 | const challengeData = scramParseChallenge(challenge); 156 | 157 | // The RFC requires that we verify the (server) nonce has the client 158 | // nonce as an initial substring. 159 | if (!challengeData && challengeData?.nonce.slice(0, cnonce.length) !== cnonce) { 160 | log.warn('Failing SCRAM authentication because server supplied incorrect nonce.'); 161 | connection._sasl_data = {}; 162 | return connection._sasl_failure_cb(); 163 | } 164 | 165 | let clientKey, serverKey; 166 | 167 | const { pass } = connection; 168 | 169 | if (typeof connection.pass === 'string' || connection.pass instanceof String) { 170 | const keys = await scramDeriveKeys( 171 | /** @type {string} */ (pass), 172 | challengeData.salt, 173 | challengeData.iter, 174 | hashName, 175 | hashBits 176 | ); 177 | clientKey = keys.ck; 178 | serverKey = keys.sk; 179 | } else if ( 180 | // Either restore the client key and server key passed in, or derive new ones 181 | /** @type {Password} */ (pass)?.name === hashName && 182 | /** @type {Password} */ (pass)?.salt === utils.arrayBufToBase64(challengeData.salt) && 183 | /** @type {Password} */ (pass)?.iter === challengeData.iter 184 | ) { 185 | const { ck, sk } = /** @type {Password} */ (pass); 186 | clientKey = utils.base64ToArrayBuf(ck); 187 | serverKey = utils.base64ToArrayBuf(sk); 188 | } else { 189 | return connection._sasl_failure_cb(); 190 | } 191 | 192 | const clientFirstMessageBare = connection._sasl_data['client-first-message-bare']; 193 | const serverFirstMessage = challenge; 194 | const clientFinalMessageBare = `c=biws,r=${challengeData.nonce}`; 195 | 196 | const authMessage = `${clientFirstMessageBare},${serverFirstMessage},${clientFinalMessageBare}`; 197 | 198 | const clientProof = await scramClientProof(authMessage, clientKey, hashName); 199 | const serverSignature = await scramServerSign(authMessage, serverKey, hashName); 200 | 201 | connection._sasl_data['server-signature'] = utils.arrayBufToBase64(serverSignature); 202 | connection._sasl_data.keys = { 203 | 'name': hashName, 204 | 'iter': challengeData.iter, 205 | 'salt': utils.arrayBufToBase64(challengeData.salt), 206 | 'ck': utils.arrayBufToBase64(clientKey), 207 | 'sk': utils.arrayBufToBase64(serverKey), 208 | }; 209 | 210 | return `${clientFinalMessageBare},p=${utils.arrayBufToBase64(clientProof)}`; 211 | }, 212 | 213 | /** 214 | * Returns a string containing the client first message 215 | * @param {Connection} connection 216 | * @param {string} test_cnonce 217 | */ 218 | clientChallenge(connection, test_cnonce) { 219 | const cnonce = test_cnonce || generate_cnonce(); 220 | const client_first_message_bare = `n=${connection.authcid},r=${cnonce}`; 221 | connection._sasl_data.cnonce = cnonce; 222 | connection._sasl_data['client-first-message-bare'] = client_first_message_bare; 223 | return `n,,${client_first_message_bare}`; 224 | }, 225 | }; 226 | 227 | export { scram as default }; 228 | -------------------------------------------------------------------------------- /src/types/utils.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Takes a string and turns it into an XML Element. 3 | * @param {string} string 4 | * @param {boolean} [throwErrorIfInvalidNS] 5 | * @returns {Element} 6 | */ 7 | export function toElement(string: string, throwErrorIfInvalidNS?: boolean): Element; 8 | /** 9 | * Properly logs an error to the console 10 | * @param {Error} e 11 | */ 12 | export function handleError(e: Error): void; 13 | /** 14 | * @param {string} str 15 | * @return {string} 16 | */ 17 | export function utf16to8(str: string): string; 18 | /** 19 | * @param {ArrayBufferLike} x 20 | * @param {ArrayBufferLike} y 21 | */ 22 | export function xorArrayBuffers(x: ArrayBufferLike, y: ArrayBufferLike): ArrayBuffer; 23 | /** 24 | * @param {ArrayBufferLike} buffer 25 | * @return {string} 26 | */ 27 | export function arrayBufToBase64(buffer: ArrayBufferLike): string; 28 | /** 29 | * @param {string} str 30 | * @return {ArrayBufferLike} 31 | */ 32 | export function base64ToArrayBuf(str: string): ArrayBufferLike; 33 | /** 34 | * @param {string} str 35 | * @return {ArrayBufferLike} 36 | */ 37 | export function stringToArrayBuf(str: string): ArrayBufferLike; 38 | /** 39 | * @param {Cookies} cookies 40 | */ 41 | export function addCookies(cookies: { 42 | [x: string]: string; 43 | } | { 44 | [x: string]: { 45 | [x: string]: string; 46 | }; 47 | }): void; 48 | /** 49 | * Get the DOM document to generate elements. 50 | * @return {Document} - The currently used DOM document. 51 | */ 52 | export function xmlGenerator(): Document; 53 | /** 54 | * Creates an XML DOM text node. 55 | * Provides a cross implementation version of document.createTextNode. 56 | * @param {string} text - The content of the text node. 57 | * @return {Text} - A new XML DOM text node. 58 | */ 59 | export function xmlTextNode(text: string): Text; 60 | /** 61 | * @param {Element} stanza 62 | * @return {Element} 63 | */ 64 | export function stripWhitespace(stanza: Element): Element; 65 | /** 66 | * Creates an XML DOM node. 67 | * @param {string} text - The contents of the XML element. 68 | * @return {XMLDocument} 69 | */ 70 | export function xmlHtmlNode(text: string): XMLDocument; 71 | /** 72 | * @param {XMLDocument} doc 73 | * @returns {string|null} 74 | */ 75 | export function getParserError(doc: XMLDocument): string | null; 76 | /** 77 | * @param {XMLDocument} el 78 | * @returns {Element} 79 | */ 80 | export function getFirstElementChild(el: XMLDocument): Element; 81 | /** 82 | * Create an XML DOM element. 83 | * 84 | * This function creates an XML DOM element correctly across all 85 | * implementations. Note that these are not HTML DOM elements, which 86 | * aren't appropriate for XMPP stanzas. 87 | * 88 | * @param {string} name - The name for the element. 89 | * @param {Array>|Object.|string|number} [attrs] 90 | * An optional array or object containing 91 | * key/value pairs to use as element attributes. 92 | * The object should be in the format `{'key': 'value'}`. 93 | * The array should have the format `[['key1', 'value1'], ['key2', 'value2']]`. 94 | * @param {string|number} [text] - The text child data for the element. 95 | * 96 | * @return {Element} A new XML DOM element. 97 | */ 98 | export function xmlElement(name: string, attrs?: Array> | { 99 | [x: string]: string | number; 100 | } | string | number, text?: string | number): Element; 101 | /** 102 | * Utility method to determine whether a tag is allowed 103 | * in the XHTML_IM namespace. 104 | * 105 | * XHTML tag names are case sensitive and must be lower case. 106 | * @method Strophe.XHTML.validTag 107 | * @param {string} tag 108 | */ 109 | export function validTag(tag: string): boolean; 110 | /** 111 | * @typedef {'a'|'blockquote'|'br'|'cite'|'em'|'img'|'li'|'ol'|'p'|'span'|'strong'|'ul'|'body'} XHTMLAttrs 112 | */ 113 | /** 114 | * Utility method to determine whether an attribute is allowed 115 | * as recommended per XEP-0071 116 | * 117 | * XHTML attribute names are case sensitive and must be lower case. 118 | * @method Strophe.XHTML.validAttribute 119 | * @param {string} tag 120 | * @param {string} attribute 121 | */ 122 | export function validAttribute(tag: string, attribute: string): boolean; 123 | /** 124 | * @method Strophe.XHTML.validCSS 125 | * @param {string} style 126 | */ 127 | export function validCSS(style: string): boolean; 128 | /** 129 | * Copy an HTML DOM Node into an XML DOM. 130 | * This function copies a DOM element and all its descendants and returns 131 | * the new copy. 132 | * @method Strophe.createHtml 133 | * @param {Node} node - A DOM element. 134 | * @return {Node} - A new, copied DOM element tree. 135 | */ 136 | export function createHtml(node: Node): Node; 137 | /** 138 | * Copy an XML DOM element. 139 | * 140 | * This function copies a DOM element and all its descendants and returns 141 | * the new copy. 142 | * @method Strophe.copyElement 143 | * @param {Node} node - A DOM element. 144 | * @return {Element|Text} - A new, copied DOM element tree. 145 | */ 146 | export function copyElement(node: Node): Element | Text; 147 | /** 148 | * Excapes invalid xml characters. 149 | * @method Strophe.xmlescape 150 | * @param {string} text - text to escape. 151 | * @return {string} - Escaped text. 152 | */ 153 | export function xmlescape(text: string): string; 154 | /** 155 | * Unexcapes invalid xml characters. 156 | * @method Strophe.xmlunescape 157 | * @param {string} text - text to unescape. 158 | * @return {string} - Unescaped text. 159 | */ 160 | export function xmlunescape(text: string): string; 161 | /** 162 | * Map a function over some or all child elements of a given element. 163 | * 164 | * This is a small convenience function for mapping a function over 165 | * some or all of the children of an element. If elemName is null, all 166 | * children will be passed to the function, otherwise only children 167 | * whose tag names match elemName will be passed. 168 | * 169 | * @method Strophe.forEachChild 170 | * @param {Element} elem - The element to operate on. 171 | * @param {string} elemName - The child element tag name filter. 172 | * @param {Function} func - The function to apply to each child. This 173 | * function should take a single argument, a DOM element. 174 | */ 175 | export function forEachChild(elem: Element, elemName: string, func: Function): void; 176 | /** 177 | * Compare an element's tag name with a string. 178 | * This function is case sensitive. 179 | * @method Strophe.isTagEqual 180 | * @param {Element} el - A DOM element. 181 | * @param {string} name - The element name. 182 | * @return {boolean} 183 | * true if the element's tag name matches _el_, and false 184 | * otherwise. 185 | */ 186 | export function isTagEqual(el: Element, name: string): boolean; 187 | /** 188 | * Get the concatenation of all text children of an element. 189 | * @method Strophe.getText 190 | * @param {Element} elem - A DOM element. 191 | * @return {string} - A String with the concatenated text of all text element children. 192 | */ 193 | export function getText(elem: Element): string; 194 | /** 195 | * Escape the node part (also called local part) of a JID. 196 | * @method Strophe.escapeNode 197 | * @param {string} node - A node (or local part). 198 | * @return {string} An escaped node (or local part). 199 | */ 200 | export function escapeNode(node: string): string; 201 | /** 202 | * Unescape a node part (also called local part) of a JID. 203 | * @method Strophe.unescapeNode 204 | * @param {string} node - A node (or local part). 205 | * @return {string} An unescaped node (or local part). 206 | */ 207 | export function unescapeNode(node: string): string; 208 | /** 209 | * Get the node portion of a JID String. 210 | * @method Strophe.getNodeFromJid 211 | * @param {string} jid - A JID. 212 | * @return {string} - A String containing the node. 213 | */ 214 | export function getNodeFromJid(jid: string): string; 215 | /** 216 | * Get the domain portion of a JID String. 217 | * @method Strophe.getDomainFromJid 218 | * @param {string} jid - A JID. 219 | * @return {string} - A String containing the domain. 220 | */ 221 | export function getDomainFromJid(jid: string): string; 222 | /** 223 | * Get the resource portion of a JID String. 224 | * @method Strophe.getResourceFromJid 225 | * @param {string} jid - A JID. 226 | * @return {string} - A String containing the resource. 227 | */ 228 | export function getResourceFromJid(jid: string): string; 229 | /** 230 | * Get the bare JID from a JID String. 231 | * @method Strophe.getBareJidFromJid 232 | * @param {string} jid - A JID. 233 | * @return {string} - A String containing the bare JID. 234 | */ 235 | export function getBareJidFromJid(jid: string): string; 236 | export { utils as default }; 237 | export type XHTMLAttrs = "a" | "blockquote" | "br" | "cite" | "em" | "img" | "li" | "ol" | "p" | "span" | "strong" | "ul" | "body"; 238 | declare namespace utils { 239 | export { utf16to8 }; 240 | export { xorArrayBuffers }; 241 | export { arrayBufToBase64 }; 242 | export { base64ToArrayBuf }; 243 | export { stringToArrayBuf }; 244 | export { addCookies }; 245 | } 246 | //# sourceMappingURL=utils.d.ts.map -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import Builder from './builder.js'; 2 | import { $build } from './builder.js'; 3 | import { $iq } from './builder.js'; 4 | import { $msg } from './builder.js'; 5 | import { $pres } from './builder.js'; 6 | /** 7 | * A container for all Strophe library functions. 8 | * 9 | * This object is a container for all the objects and constants 10 | * used in the library. It is not meant to be instantiated, but to 11 | * provide a namespace for library objects, constants, and functions. 12 | * 13 | * @namespace Strophe 14 | * @property {Handler} Handler 15 | * @property {Builder} Builder 16 | * @property {Request} Request Represents HTTP Requests made for a BOSH connection 17 | * @property {Bosh} Bosh Support for XMPP-over-HTTP via XEP-0124 (BOSH) 18 | * @property {Websocket} Websocket Support for XMPP over websocket 19 | * @property {WorkerWebsocket} WorkerWebsocket Support for XMPP over websocket in a shared worker 20 | * @property {number} TIMEOUT=1.1 Timeout multiplier. A waiting BOSH HTTP request 21 | * will be considered failed after Math.floor(TIMEOUT * wait) seconds have elapsed. 22 | * This defaults to 1.1, and with default wait, 66 seconds. 23 | * @property {number} SECONDARY_TIMEOUT=0.1 Secondary timeout multiplier. 24 | * In cases where Strophe can detect early failure, it will consider the request 25 | * failed if it doesn't return after `Math.floor(SECONDARY_TIMEOUT * wait)` 26 | * seconds have elapsed. This defaults to 0.1, and with default wait, 6 seconds. 27 | * @property {SASLAnonymous} SASLAnonymous SASL ANONYMOUS authentication. 28 | * @property {SASLPlain} SASLPlain SASL PLAIN authentication 29 | * @property {SASLSHA1} SASLSHA1 SASL SCRAM-SHA-1 authentication 30 | * @property {SASLSHA256} SASLSHA256 SASL SCRAM-SHA-256 authentication 31 | * @property {SASLSHA384} SASLSHA384 SASL SCRAM-SHA-384 authentication 32 | * @property {SASLSHA512} SASLSHA512 SASL SCRAM-SHA-512 authentication 33 | * @property {SASLOAuthBearer} SASLOAuthBearer SASL OAuth Bearer authentication 34 | * @property {SASLExternal} SASLExternal SASL EXTERNAL authentication 35 | * @property {SASLXOAuth2} SASLXOAuth2 SASL X-OAuth2 authentication 36 | * @property {Status} Status 37 | * @property {Object.} NS 38 | * @property {XHTML} XHTML 39 | */ 40 | export const Strophe: { 41 | Request: typeof Request; 42 | Bosh: typeof Bosh; 43 | Websocket: typeof Websocket; 44 | WorkerWebsocket: typeof WorkerWebsocket; 45 | Connection: typeof Connection; 46 | Handler: typeof Handler; 47 | SASLAnonymous: typeof SASLAnonymous; 48 | SASLPlain: typeof SASLPlain; 49 | SASLSHA1: typeof SASLSHA1; 50 | SASLSHA256: typeof SASLSHA256; 51 | SASLSHA384: typeof SASLSHA384; 52 | SASLSHA512: typeof SASLSHA512; 53 | SASLOAuthBearer: typeof SASLOAuthBearer; 54 | SASLExternal: typeof SASLExternal; 55 | SASLXOAuth2: typeof SASLXOAuth2; 56 | Stanza: typeof Stanza; 57 | Builder: typeof Builder; 58 | ElementType: { 59 | NORMAL: number; 60 | TEXT: number; 61 | CDATA: number; 62 | FRAGMENT: number; 63 | }; 64 | ErrorCondition: { 65 | BAD_FORMAT: string; 66 | CONFLICT: string; 67 | MISSING_JID_NODE: string; 68 | NO_AUTH_MECH: string; 69 | UNKNOWN_REASON: string; 70 | }; 71 | LogLevel: { 72 | DEBUG: number; 73 | INFO: number; 74 | WARN: number; 75 | ERROR: number; 76 | FATAL: number; 77 | }; 78 | /** @type {Object.} */ 79 | NS: { 80 | [x: string]: string; 81 | }; 82 | SASLMechanism: typeof SASLMechanism; 83 | /** @type {Status} */ 84 | Status: Status; 85 | TimedHandler: typeof TimedHandler; 86 | XHTML: { 87 | validTag: typeof utils.validTag; 88 | validCSS: typeof utils.validCSS; 89 | validAttribute: typeof utils.validAttribute; 90 | tags: string[]; 91 | attributes: { 92 | a: string[]; 93 | blockquote: string[]; 94 | br: never[]; 95 | cite: string[]; 96 | em: never[]; 97 | img: string[]; 98 | li: string[]; 99 | ol: string[]; 100 | p: string[]; 101 | span: string[]; 102 | strong: never[]; 103 | ul: string[]; 104 | body: never[]; 105 | }; 106 | css: string[]; 107 | }; 108 | /** 109 | * Render a DOM element and all descendants to a String. 110 | * @method Strophe.serialize 111 | * @param {Element|Builder} elem - A DOM element. 112 | * @return {string} - The serialized element tree as a String. 113 | */ 114 | serialize(elem: Element | Builder): string; 115 | /** 116 | * @typedef {import('./constants').LogLevel} LogLevel 117 | * 118 | * Library consumers can use this function to set the log level of Strophe. 119 | * The default log level is Strophe.LogLevel.INFO. 120 | * @param {LogLevel} level 121 | * @example Strophe.setLogLevel(Strophe.LogLevel.DEBUG); 122 | */ 123 | setLogLevel(level: import("./constants.js").LogLevel): void; 124 | /** 125 | * This function is used to extend the current namespaces in 126 | * Strophe.NS. It takes a key and a value with the key being the 127 | * name of the new namespace, with its actual value. 128 | * @example: Strophe.addNamespace('PUBSUB', "http://jabber.org/protocol/pubsub"); 129 | * 130 | * @param {string} name - The name under which the namespace will be 131 | * referenced under Strophe.NS 132 | * @param {string} value - The actual namespace. 133 | */ 134 | addNamespace(name: string, value: string): void; 135 | /** 136 | * Extends the Strophe.Connection object with the given plugin. 137 | * @param {string} name - The name of the extension. 138 | * @param {Object} ptype - The plugin's prototype. 139 | */ 140 | addConnectionPlugin(name: string, ptype: Object): void; 141 | log(level: number, msg: string): void; 142 | debug(msg: string): void; 143 | info(msg: string): void; 144 | warn(msg: string): void; 145 | error(msg: string): void; 146 | fatal(msg: string): void; 147 | toElement(string: string, throwErrorIfInvalidNS?: boolean): Element; 148 | handleError(e: Error): void; 149 | utf16to8(str: string): string; 150 | xorArrayBuffers(x: ArrayBufferLike, y: ArrayBufferLike): ArrayBuffer; 151 | arrayBufToBase64(buffer: ArrayBufferLike): string; 152 | base64ToArrayBuf(str: string): ArrayBufferLike; 153 | stringToArrayBuf(str: string): ArrayBufferLike; 154 | addCookies(cookies: { 155 | [x: string]: string; 156 | } | { 157 | [x: string]: { 158 | [x: string]: string; 159 | }; 160 | }): void; 161 | xmlGenerator(): Document; 162 | xmlTextNode(text: string): Text; 163 | stripWhitespace(stanza: Element): Element; 164 | xmlHtmlNode(text: string): XMLDocument; 165 | getParserError(doc: XMLDocument): string | null; 166 | getFirstElementChild(el: XMLDocument): Element; 167 | xmlElement(name: string, attrs?: Array> | { 168 | [x: string]: string | number; 169 | } | string | number, text?: string | number): Element; 170 | validTag(tag: string): boolean; 171 | validAttribute(tag: string, attribute: string): boolean; 172 | validCSS(style: string): boolean; 173 | createHtml(node: Node): Node; 174 | copyElement(node: Node): Element | Text; 175 | xmlescape(text: string): string; 176 | xmlunescape(text: string): string; 177 | forEachChild(elem: Element, elemName: string, func: Function): void; 178 | isTagEqual(el: Element, name: string): boolean; 179 | getText(elem: Element): string; 180 | escapeNode(node: string): string; 181 | unescapeNode(node: string): string; 182 | getNodeFromJid(jid: string): string; 183 | getDomainFromJid(jid: string): string; 184 | getResourceFromJid(jid: string): string; 185 | getBareJidFromJid(jid: string): string; 186 | default: { 187 | utf16to8: typeof utils.utf16to8; 188 | xorArrayBuffers: typeof utils.xorArrayBuffers; 189 | arrayBufToBase64: typeof utils.arrayBufToBase64; 190 | base64ToArrayBuf: typeof utils.base64ToArrayBuf; 191 | stringToArrayBuf: typeof utils.stringToArrayBuf; 192 | addCookies: typeof utils.addCookies; 193 | }; 194 | /** @constant: VERSION */ 195 | VERSION: string; 196 | /** 197 | * @returns {number} 198 | */ 199 | TIMEOUT: number; 200 | /** 201 | * @returns {number} 202 | */ 203 | SECONDARY_TIMEOUT: number; 204 | }; 205 | import { Stanza } from './stanza.js'; 206 | import { stx } from './stanza.js'; 207 | export const toStanza: typeof Stanza.toElement; 208 | import Request from './request.js'; 209 | import Bosh from './bosh.js'; 210 | import Websocket from './websocket.js'; 211 | import WorkerWebsocket from './worker-websocket.js'; 212 | import Connection from './connection.js'; 213 | import Handler from './handler.js'; 214 | import SASLAnonymous from './sasl-anon.js'; 215 | import SASLPlain from './sasl-plain.js'; 216 | import SASLSHA1 from './sasl-sha1.js'; 217 | import SASLSHA256 from './sasl-sha256.js'; 218 | import SASLSHA384 from './sasl-sha384.js'; 219 | import SASLSHA512 from './sasl-sha512.js'; 220 | import SASLOAuthBearer from './sasl-oauthbearer.js'; 221 | import SASLExternal from './sasl-external.js'; 222 | import SASLXOAuth2 from './sasl-xoauth2.js'; 223 | import SASLMechanism from './sasl.js'; 224 | import { Status } from './constants.js'; 225 | import TimedHandler from './timed-handler.js'; 226 | import * as utils from './utils.js'; 227 | export { Builder, $build, $iq, $msg, $pres, Stanza, stx, Request }; 228 | //# sourceMappingURL=index.d.ts.map -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import typescriptEslint from "@typescript-eslint/eslint-plugin"; 3 | import babelParser from "@babel/eslint-parser"; 4 | import path from "node:path"; 5 | import { fileURLToPath } from "node:url"; 6 | import js from "@eslint/js"; 7 | import { FlatCompat } from "@eslint/eslintrc"; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | const compat = new FlatCompat({ 12 | baseDirectory: __dirname, 13 | recommendedConfig: js.configs.recommended, 14 | allConfig: js.configs.all 15 | }); 16 | 17 | export default [...compat.extends("eslint:recommended"), { 18 | 19 | plugins: { 20 | "@typescript-eslint": typescriptEslint, 21 | }, 22 | 23 | languageOptions: { 24 | globals: { 25 | ...globals.browser, 26 | Uint8Array: true, 27 | Promise: true, 28 | define: true, 29 | require: true, 30 | sinon: true, 31 | window: true, 32 | }, 33 | 34 | parser: babelParser, 35 | ecmaVersion: 2017, 36 | sourceType: "module", 37 | 38 | parserOptions: { 39 | allowImportExportEverywhere: true, 40 | }, 41 | }, 42 | 43 | rules: { 44 | "accessor-pairs": "error", 45 | "array-bracket-spacing": "off", 46 | "array-callback-return": "error", 47 | "arrow-body-style": "off", 48 | "arrow-parens": "off", 49 | "arrow-spacing": "error", 50 | "block-scoped-var": "off", 51 | "block-spacing": "off", 52 | "brace-style": "off", 53 | "callback-return": "off", 54 | camelcase: "off", 55 | "capitalized-comments": "off", 56 | "class-methods-use-this": "off", 57 | "comma-dangle": "off", 58 | "comma-spacing": "off", 59 | "comma-style": "off", 60 | complexity: "off", 61 | "computed-property-spacing": ["error", "never"], 62 | "consistent-return": "off", 63 | "consistent-this": "off", 64 | curly: "off", 65 | "default-case": "off", 66 | "dot-location": ["error", "property"], 67 | 68 | "dot-notation": ["off", { 69 | allowKeywords: true, 70 | }], 71 | 72 | "eol-last": "error", 73 | eqeqeq: "off", 74 | "func-call-spacing": "off", 75 | "no-spaced-func": "off", 76 | "no-redeclare": "off", 77 | "func-name-matching": "error", 78 | "func-names": "off", 79 | "func-style": "off", 80 | "generator-star-spacing": "error", 81 | "global-require": "off", 82 | "guard-for-in": "error", 83 | "handle-callback-err": "error", 84 | "id-blacklist": "error", 85 | "id-length": "off", 86 | "id-match": "error", 87 | indent: "off", 88 | "init-declarations": "off", 89 | "jsx-quotes": "error", 90 | "key-spacing": "off", 91 | "keyword-spacing": "off", 92 | "line-comment-position": "off", 93 | "linebreak-style": ["error", "unix"], 94 | "lines-around-comment": "off", 95 | "lines-around-directive": "off", 96 | "max-depth": "off", 97 | "max-len": "off", 98 | "max-lines": "off", 99 | "max-nested-callbacks": "off", 100 | "max-params": "off", 101 | "max-statements": "off", 102 | "max-statements-per-line": "off", 103 | "multiline-ternary": "off", 104 | "new-parens": "error", 105 | "newline-after-var": "off", 106 | "newline-before-return": "off", 107 | "newline-per-chained-call": "off", 108 | "no-alert": "off", 109 | "no-array-constructor": "error", 110 | "no-await-in-loop": "off", 111 | "no-bitwise": "off", 112 | "no-caller": "error", 113 | "no-console": "off", 114 | "no-catch-shadow": "off", 115 | "no-cond-assign": ["off", "except-parens"], 116 | "no-confusing-arrow": "off", 117 | "no-continue": "off", 118 | "no-div-regex": "error", 119 | "no-duplicate-imports": "error", 120 | "no-else-return": "off", 121 | "no-empty-function": "off", 122 | "no-eq-null": "error", 123 | "no-eval": "error", 124 | "no-extend-native": "off", 125 | "no-extra-bind": "off", 126 | "no-extra-label": "error", 127 | "no-extra-parens": "off", 128 | "no-floating-decimal": "error", 129 | "no-implicit-globals": "off", 130 | "no-implied-eval": "error", 131 | "no-inline-comments": "off", 132 | "no-inner-declarations": ["error", "functions"], 133 | "no-invalid-this": "off", 134 | "no-iterator": "error", 135 | "no-label-var": "error", 136 | "no-labels": "error", 137 | "no-lone-blocks": "error", 138 | "no-lonely-if": "off", 139 | "no-loop-func": "error", 140 | "no-magic-numbers": "off", 141 | "no-mixed-operators": "off", 142 | "no-mixed-requires": "error", 143 | "no-multi-assign": "off", 144 | "no-multi-spaces": "off", 145 | "no-multi-str": "error", 146 | "no-multiple-empty-lines": "error", 147 | "no-native-reassign": "error", 148 | "no-negated-condition": "off", 149 | "no-negated-in-lhs": "error", 150 | "no-nested-ternary": "off", 151 | "no-new": "off", 152 | "no-new-func": "error", 153 | "no-new-object": "error", 154 | "no-new-require": "error", 155 | "no-new-wrappers": "error", 156 | "no-octal-escape": "error", 157 | "no-param-reassign": "off", 158 | "no-path-concat": "error", 159 | "no-plusplus": "off", 160 | "no-process-env": "off", 161 | "no-process-exit": "error", 162 | "no-proto": "error", 163 | "no-prototype-builtins": "error", 164 | "no-restricted-globals": "error", 165 | "no-restricted-imports": "error", 166 | "no-restricted-modules": "error", 167 | "no-restricted-properties": "error", 168 | "no-restricted-syntax": "error", 169 | "no-return-assign": "error", 170 | "no-return-await": "off", 171 | "no-script-url": "error", 172 | "no-self-compare": "error", 173 | "no-sequences": "error", 174 | "no-shadow": "off", 175 | "no-shadow-restricted-names": "error", 176 | "no-sync": "error", 177 | "no-tabs": "error", 178 | "no-template-curly-in-string": "error", 179 | "no-ternary": "off", 180 | "no-throw-literal": "error", 181 | "no-trailing-spaces": "off", 182 | "no-undef-init": "error", 183 | "no-undefined": "off", 184 | "no-underscore-dangle": "off", 185 | "no-unmodified-loop-condition": "error", 186 | "no-unneeded-ternary": "off", 187 | "no-unused-vars": "off", 188 | "@typescript-eslint/no-unused-vars": ["error", { 189 | args: "all", 190 | argsIgnorePattern: "^_", 191 | caughtErrors: "all", 192 | caughtErrorsIgnorePattern: "^_", 193 | destructuredArrayIgnorePattern: "^_", 194 | varsIgnorePattern: "^_", 195 | ignoreRestSiblings: true, 196 | }], 197 | "no-unused-expressions": "off", 198 | "no-use-before-define": "off", 199 | "no-useless-call": "error", 200 | "no-useless-catch": "off", 201 | "no-useless-computed-key": "error", 202 | "no-useless-concat": "off", 203 | "no-useless-constructor": "error", 204 | "no-useless-escape": "off", 205 | "no-useless-rename": "error", 206 | "no-useless-return": "off", 207 | "no-var": "off", 208 | "no-void": "error", 209 | "no-warning-comments": "off", 210 | "no-whitespace-before-property": "error", 211 | "no-with": "error", 212 | "object-curly-newline": "off", 213 | "object-curly-spacing": "off", 214 | 215 | "object-property-newline": ["off", { 216 | allowMultiplePropertiesPerLine: true, 217 | }], 218 | 219 | "object-shorthand": "off", 220 | "one-var": "off", 221 | "one-var-declaration-per-line": "off", 222 | "operator-assignment": "off", 223 | "operator-linebreak": "off", 224 | "padded-blocks": "off", 225 | "prefer-arrow-callback": "off", 226 | "prefer-const": "error", 227 | 228 | "prefer-destructuring": ["error", { 229 | array: false, 230 | object: false, 231 | }], 232 | 233 | "prefer-numeric-literals": "error", 234 | "prefer-promise-reject-errors": "off", 235 | "prefer-reflect": "off", 236 | "prefer-rest-params": "off", 237 | "prefer-spread": "off", 238 | "prefer-template": "off", 239 | "quote-props": "off", 240 | quotes: "off", 241 | radix: ["error", "always"], 242 | "require-atomic-updates": "off", 243 | "require-await": "error", 244 | "require-jsdoc": "off", 245 | "rest-spread-spacing": "error", 246 | semi: "off", 247 | "semi-spacing": "off", 248 | "sort-imports": "off", 249 | "sort-keys": "off", 250 | "sort-vars": "off", 251 | "space-before-blocks": "off", 252 | "space-before-function-paren": "off", 253 | "space-in-parens": "off", 254 | "space-infix-ops": "off", 255 | "space-unary-ops": "off", 256 | "spaced-comment": "off", 257 | strict: "off", 258 | "symbol-description": "error", 259 | "template-curly-spacing": "off", 260 | "unicode-bom": ["error", "never"], 261 | "valid-jsdoc": "off", 262 | "vars-on-top": "off", 263 | "wrap-iife": ["error", "any"], 264 | "wrap-regex": "error", 265 | "yield-star-spacing": "error", 266 | yoda: "off", 267 | }, 268 | }]; 269 | -------------------------------------------------------------------------------- /src/types/bosh.d.ts: -------------------------------------------------------------------------------- 1 | export default Bosh; 2 | export type Connection = import("./connection.js").default; 3 | /** 4 | * _Private_ helper class that handles BOSH Connections 5 | * The Bosh class is used internally by Connection 6 | * to encapsulate BOSH sessions. It is not meant to be used from user's code. 7 | */ 8 | declare class Bosh { 9 | /** 10 | * @param {number} m 11 | */ 12 | static setTimeoutMultiplier(m: number): void; 13 | /** 14 | * @returns {number} 15 | */ 16 | static getTimeoutMultplier(): number; 17 | /** 18 | * @param {number} m 19 | */ 20 | static setSecondaryTimeoutMultiplier(m: number): void; 21 | /** 22 | * @returns {number} 23 | */ 24 | static getSecondaryTimeoutMultplier(): number; 25 | /** 26 | * Returns the HTTP status code from a {@link Request} 27 | * @private 28 | * @param {Request} req - The {@link Request} instance. 29 | * @param {number} [def] - The default value that should be returned if no status value was found. 30 | */ 31 | private static _getRequestStatus; 32 | /** 33 | * @param {Connection} connection - The Connection that will use BOSH. 34 | */ 35 | constructor(connection: Connection); 36 | _conn: import("./connection.js").default; 37 | rid: number; 38 | sid: string; 39 | hold: number; 40 | wait: number; 41 | window: number; 42 | errors: number; 43 | inactivity: number; 44 | /** 45 | * BOSH-Connections will have all stanzas wrapped in a tag when 46 | * passed to {@link Connection#xmlInput|xmlInput()} or {@link Connection#xmlOutput|xmlOutput()}. 47 | * To strip this tag, User code can set {@link Bosh#strip|strip} to `true`: 48 | * 49 | * > // You can set `strip` on the prototype 50 | * > Bosh.prototype.strip = true; 51 | * 52 | * > // Or you can set it on the Bosh instance (which is `._proto` on the connection instance. 53 | * > const conn = new Connection(); 54 | * > conn._proto.strip = true; 55 | * 56 | * This will enable stripping of the body tag in both 57 | * {@link Connection#xmlInput|xmlInput} and {@link Connection#xmlOutput|xmlOutput}. 58 | * 59 | * @property {boolean} [strip=false] 60 | */ 61 | strip: boolean; 62 | lastResponseHeaders: string; 63 | /** @type {Request[]} */ 64 | _requests: Request[]; 65 | /** 66 | * _Private_ helper function to generate the wrapper for BOSH. 67 | * @private 68 | * @return {Builder} - A Builder with a element. 69 | */ 70 | private _buildBody; 71 | /** 72 | * Reset the connection. 73 | * This function is called by the reset function of the Connection 74 | */ 75 | _reset(): void; 76 | /** 77 | * _Private_ function that initializes the BOSH connection. 78 | * Creates and sends the Request that initializes the BOSH connection. 79 | * @param {number} wait - The optional HTTPBIND wait value. This is the 80 | * time the server will wait before returning an empty result for 81 | * a request. The default setting of 60 seconds is recommended. 82 | * Other settings will require tweaks to the Strophe.TIMEOUT value. 83 | * @param {number} hold - The optional HTTPBIND hold value. This is the 84 | * number of connections the server will hold at one time. This 85 | * should almost always be set to 1 (the default). 86 | * @param {string} route 87 | */ 88 | _connect(wait: number, hold: number, route: string): void; 89 | /** 90 | * Attach to an already created and authenticated BOSH session. 91 | * 92 | * This function is provided to allow Strophe to attach to BOSH 93 | * sessions which have been created externally, perhaps by a Web 94 | * application. This is often used to support auto-login type features 95 | * without putting user credentials into the page. 96 | * 97 | * @param {string} jid - The full JID that is bound by the session. 98 | * @param {string} sid - The SID of the BOSH session. 99 | * @param {number} rid - The current RID of the BOSH session. This RID 100 | * will be used by the next request. 101 | * @param {Function} callback The connect callback function. 102 | * @param {number} wait - The optional HTTPBIND wait value. This is the 103 | * time the server will wait before returning an empty result for 104 | * a request. The default setting of 60 seconds is recommended. 105 | * Other settings will require tweaks to the Strophe.TIMEOUT value. 106 | * @param {number} hold - The optional HTTPBIND hold value. This is the 107 | * number of connections the server will hold at one time. This 108 | * should almost always be set to 1 (the default). 109 | * @param {number} wind - The optional HTTBIND window value. This is the 110 | * allowed range of request ids that are valid. The default is 5. 111 | */ 112 | _attach(jid: string, sid: string, rid: number, callback: Function, wait: number, hold: number, wind: number): void; 113 | /** 114 | * Attempt to restore a cached BOSH session 115 | * 116 | * @param {string} jid - The full JID that is bound by the session. 117 | * This parameter is optional but recommended, specifically in cases 118 | * where prebinded BOSH sessions are used where it's important to know 119 | * that the right session is being restored. 120 | * @param {Function} callback The connect callback function. 121 | * @param {number} wait - The optional HTTPBIND wait value. This is the 122 | * time the server will wait before returning an empty result for 123 | * a request. The default setting of 60 seconds is recommended. 124 | * Other settings will require tweaks to the Strophe.TIMEOUT value. 125 | * @param {number} hold - The optional HTTPBIND hold value. This is the 126 | * number of connections the server will hold at one time. This 127 | * should almost always be set to 1 (the default). 128 | * @param {number} wind - The optional HTTBIND window value. This is the 129 | * allowed range of request ids that are valid. The default is 5. 130 | */ 131 | _restore(jid: string, callback: Function, wait: number, hold: number, wind: number): void; 132 | /** 133 | * _Private_ handler for the beforeunload event. 134 | * This handler is used to process the Bosh-part of the initial request. 135 | * @private 136 | */ 137 | private _cacheSession; 138 | /** 139 | * _Private_ handler for initial connection request. 140 | * This handler is used to process the Bosh-part of the initial request. 141 | * @param {Element} bodyWrap - The received stanza. 142 | */ 143 | _connect_cb(bodyWrap: Element): number; 144 | /** 145 | * _Private_ part of Connection.disconnect for Bosh 146 | * @param {Element|Builder} pres - This stanza will be sent before disconnecting. 147 | */ 148 | _disconnect(pres: Element | Builder): void; 149 | /** 150 | * _Private_ function to disconnect. 151 | * Resets the SID and RID. 152 | */ 153 | _doDisconnect(): void; 154 | /** 155 | * _Private_ function to check if the Request queue is empty. 156 | * @return {boolean} - True, if there are no Requests queued, False otherwise. 157 | */ 158 | _emptyQueue(): boolean; 159 | /** 160 | * _Private_ function to call error handlers registered for HTTP errors. 161 | * @private 162 | * @param {Request} req - The request that is changing readyState. 163 | */ 164 | private _callProtocolErrorHandlers; 165 | /** 166 | * _Private_ function to handle the error count. 167 | * 168 | * Requests are resent automatically until their error count reaches 169 | * 5. Each time an error is encountered, this function is called to 170 | * increment the count and disconnect if the count is too high. 171 | * @private 172 | * @param {number} reqStatus - The request status. 173 | */ 174 | private _hitError; 175 | /** 176 | * @callback connectionCallback 177 | * @param {Connection} connection 178 | */ 179 | /** 180 | * Called on stream start/restart when no stream:features 181 | * has been received and sends a blank poll request. 182 | * @param {connectionCallback} callback 183 | */ 184 | _no_auth_received(callback: (connection: Connection) => any): void; 185 | /** 186 | * _Private_ timeout handler for handling non-graceful disconnection. 187 | * Cancels all remaining Requests and clears the queue. 188 | */ 189 | _onDisconnectTimeout(): void; 190 | /** 191 | * _Private_ helper function that makes sure all pending requests are aborted. 192 | */ 193 | _abortAllRequests(): void; 194 | /** 195 | * _Private_ handler called by {@link Connection#_onIdle|Connection._onIdle()}. 196 | * Sends all queued Requests or polls with empty Request if there are none. 197 | */ 198 | _onIdle(): void; 199 | /** 200 | * _Private_ handler for {@link Request} state changes. 201 | * 202 | * This function is called when the XMLHttpRequest readyState changes. 203 | * It contains a lot of error handling logic for the many ways that 204 | * requests can fail, and calls the request callback when requests 205 | * succeed. 206 | * @private 207 | * 208 | * @param {Function} func - The handler for the request. 209 | * @param {Request} req - The request that is changing readyState. 210 | */ 211 | private _onRequestStateChange; 212 | /** 213 | * _Private_ function to process a request in the queue. 214 | * 215 | * This function takes requests off the queue and sends them and 216 | * restarts dead requests. 217 | * @private 218 | * 219 | * @param {number} i - The index of the request in the queue. 220 | */ 221 | private _processRequest; 222 | /** 223 | * _Private_ function to remove a request from the queue. 224 | * @private 225 | * @param {Request} req - The request to remove. 226 | */ 227 | private _removeRequest; 228 | /** 229 | * _Private_ function to restart a request that is presumed dead. 230 | * @private 231 | * 232 | * @param {number} i - The index of the request in the queue. 233 | */ 234 | private _restartRequest; 235 | /** 236 | * _Private_ function to get a stanza out of a request. 237 | * Tries to extract a stanza out of a Request Object. 238 | * When this fails the current connection will be disconnected. 239 | * 240 | * @param {Request} req - The Request. 241 | * @return {Element} - The stanza that was passed. 242 | */ 243 | _reqToData(req: Request): Element; 244 | /** 245 | * _Private_ function to send initial disconnect sequence. 246 | * 247 | * This is the first step in a graceful disconnect. It sends 248 | * the BOSH server a terminate body and includes an unavailable 249 | * presence if authentication has completed. 250 | * @private 251 | * @param {Element|Builder} [pres] 252 | */ 253 | private _sendTerminate; 254 | /** 255 | * _Private_ part of the Connection.send function for BOSH 256 | * Just triggers the RequestHandler to send the messages that are in the queue 257 | */ 258 | _send(): void; 259 | /** 260 | * Send an xmpp:restart stanza. 261 | */ 262 | _sendRestart(): void; 263 | /** 264 | * _Private_ function to throttle requests to the connection window. 265 | * 266 | * This function makes sure we don't send requests so fast that the 267 | * request ids overflow the connection window in the case that one 268 | * request died. 269 | * @private 270 | */ 271 | private _throttledRequestHandler; 272 | } 273 | import Request from './request.js'; 274 | import Builder from './builder.js'; 275 | //# sourceMappingURL=bosh.d.ts.map -------------------------------------------------------------------------------- /src/builder.js: -------------------------------------------------------------------------------- 1 | import { ElementType, NS } from './constants.js'; 2 | import { copyElement, createHtml, toElement, xmlElement, xmlGenerator, xmlTextNode, xmlescape } from './utils.js'; 3 | 4 | /** 5 | * Create a {@link Strophe.Builder} 6 | * This is an alias for `new Strophe.Builder(name, attrs)`. 7 | * @param {string} name - The root element name. 8 | * @param {Object.} [attrs] - The attributes for the root element in object notation. 9 | * @return {Builder} A new Strophe.Builder object. 10 | */ 11 | export function $build(name, attrs) { 12 | return new Builder(name, attrs); 13 | } 14 | 15 | /** 16 | * Create a {@link Strophe.Builder} with a `` element as the root. 17 | * @param {Object.} [attrs] - The element attributes in object notation. 18 | * @return {Builder} A new Strophe.Builder object. 19 | */ 20 | export function $msg(attrs) { 21 | return new Builder('message', attrs); 22 | } 23 | 24 | /** 25 | * Create a {@link Strophe.Builder} with an `` element as the root. 26 | * @param {Object.} [attrs] - The element attributes in object notation. 27 | * @return {Builder} A new Strophe.Builder object. 28 | */ 29 | export function $iq(attrs) { 30 | return new Builder('iq', attrs); 31 | } 32 | 33 | /** 34 | * Create a {@link Strophe.Builder} with a `` element as the root. 35 | * @param {Object.} [attrs] - The element attributes in object notation. 36 | * @return {Builder} A new Strophe.Builder object. 37 | */ 38 | export function $pres(attrs) { 39 | return new Builder('presence', attrs); 40 | } 41 | 42 | /** 43 | * This class provides an interface similar to JQuery but for building 44 | * DOM elements easily and rapidly. All the functions except for `toString()` 45 | * and tree() return the object, so calls can be chained. 46 | * 47 | * The corresponding DOM manipulations to get a similar fragment would be 48 | * a lot more tedious and probably involve several helper variables. 49 | * 50 | * Since adding children makes new operations operate on the child, up() 51 | * is provided to traverse up the tree. To add two children, do 52 | * > builder.c('child1', ...).up().c('child2', ...) 53 | * 54 | * The next operation on the Builder will be relative to the second child. 55 | * 56 | * @example 57 | * // Here's an example using the $iq() builder helper. 58 | * $iq({to: 'you', from: 'me', type: 'get', id: '1'}) 59 | * .c('query', {xmlns: 'strophe:example'}) 60 | * .c('example') 61 | * .toString() 62 | * 63 | * // The above generates this XML fragment 64 | * // 65 | * // 66 | * // 67 | * // 68 | * // 69 | */ 70 | class Builder { 71 | /** 72 | * @typedef {Object.} StanzaAttrs 73 | * @property {string} [StanzaAttrs.xmlns] 74 | */ 75 | 76 | /** @type {Element} */ 77 | #nodeTree; 78 | /** @type {Element} */ 79 | #node; 80 | /** @type {string} */ 81 | #name; 82 | /** @type {StanzaAttrs} */ 83 | #attrs; 84 | 85 | /** 86 | * The attributes should be passed in object notation. 87 | * @param {string} name - The name of the root element. 88 | * @param {StanzaAttrs} [attrs] - The attributes for the root element in object notation. 89 | * @example const b = new Builder('message', {to: 'you', from: 'me'}); 90 | * @example const b = new Builder('messsage', {'xml:lang': 'en'}); 91 | */ 92 | constructor(name, attrs) { 93 | // Set correct namespace for jabber:client elements 94 | if (name === 'presence' || name === 'message' || name === 'iq') { 95 | if (attrs && !attrs.xmlns) { 96 | attrs.xmlns = NS.CLIENT; 97 | } else if (!attrs) { 98 | attrs = { xmlns: NS.CLIENT }; 99 | } 100 | } 101 | 102 | this.#name = name; 103 | this.#attrs = attrs; 104 | } 105 | 106 | /** 107 | * Creates a new Builder object from an XML string. 108 | * @param {string} str 109 | * @returns {Builder} 110 | * @example const stanza = Builder.fromString(''); 111 | */ 112 | static fromString(str) { 113 | const el = toElement(str, true); 114 | const b = new Builder(''); 115 | b.#nodeTree = el; 116 | return b; 117 | } 118 | 119 | buildTree() { 120 | return xmlElement(this.#name, this.#attrs); 121 | } 122 | 123 | /** @return {Element} */ 124 | get nodeTree() { 125 | if (!this.#nodeTree) { 126 | // Holds the tree being built. 127 | this.#nodeTree = this.buildTree(); 128 | } 129 | return this.#nodeTree; 130 | } 131 | 132 | /** @return {Element} */ 133 | get node() { 134 | if (!this.#node) { 135 | this.#node = this.tree(); 136 | } 137 | return this.#node; 138 | } 139 | 140 | /** @param {Element} el */ 141 | set node(el) { 142 | this.#node = el; 143 | } 144 | 145 | /** 146 | * Render a DOM element and all descendants to a String. 147 | * @param {Element|Builder} elem - A DOM element. 148 | * @return {string} - The serialized element tree as a String. 149 | */ 150 | static serialize(elem) { 151 | if (!elem) return null; 152 | 153 | const el = elem instanceof Builder ? elem.tree() : elem; 154 | 155 | const names = [...Array(el.attributes.length).keys()].map((i) => el.attributes[i].nodeName); 156 | names.sort(); 157 | let result = names.reduce( 158 | (a, n) => `${a} ${n}="${xmlescape(el.attributes.getNamedItem(n).value)}"`, 159 | `<${el.nodeName}` 160 | ); 161 | 162 | if (el.childNodes.length > 0) { 163 | result += '>'; 164 | for (let i = 0; i < el.childNodes.length; i++) { 165 | const child = el.childNodes[i]; 166 | switch (child.nodeType) { 167 | case ElementType.NORMAL: 168 | // normal element, so recurse 169 | result += Builder.serialize(/** @type {Element} */ (child)); 170 | break; 171 | case ElementType.TEXT: 172 | // text element to escape values 173 | result += xmlescape(child.nodeValue); 174 | break; 175 | case ElementType.CDATA: 176 | // cdata section so don't escape values 177 | result += ''; 178 | } 179 | } 180 | result += ''; 181 | } else { 182 | result += '/>'; 183 | } 184 | return result; 185 | } 186 | 187 | /** 188 | * Return the DOM tree. 189 | * 190 | * This function returns the current DOM tree as an element object. This 191 | * is suitable for passing to functions like Strophe.Connection.send(). 192 | * 193 | * @return {Element} The DOM tree as a element object. 194 | */ 195 | tree() { 196 | return this.nodeTree; 197 | } 198 | 199 | /** 200 | * Serialize the DOM tree to a String. 201 | * 202 | * This function returns a string serialization of the current DOM 203 | * tree. It is often used internally to pass data to a 204 | * Strophe.Request object. 205 | * 206 | * @return {string} The serialized DOM tree in a String. 207 | */ 208 | toString() { 209 | return Builder.serialize(this.tree()); 210 | } 211 | 212 | /** 213 | * Make the current parent element the new current element. 214 | * This function is often used after c() to traverse back up the tree. 215 | * 216 | * @example 217 | * // For example, to add two children to the same element 218 | * builder.c('child1', {}).up().c('child2', {}); 219 | * 220 | * @return {Builder} The Strophe.Builder object. 221 | */ 222 | up() { 223 | // Depending on context, parentElement is not always available 224 | this.node = this.node.parentElement ? this.node.parentElement : /** @type {Element} */ (this.node.parentNode); 225 | return this; 226 | } 227 | 228 | /** 229 | * Make the root element the new current element. 230 | * 231 | * When at a deeply nested element in the tree, this function can be used 232 | * to jump back to the root of the tree, instead of having to repeatedly 233 | * call up(). 234 | * 235 | * @return {Builder} The Strophe.Builder object. 236 | */ 237 | root() { 238 | this.node = this.tree(); 239 | return this; 240 | } 241 | 242 | /** 243 | * Add or modify attributes of the current element. 244 | * 245 | * The attributes should be passed in object notation. 246 | * This function does not move the current element pointer. 247 | * @param {Object.} moreattrs - The attributes to add/modify in object notation. 248 | * If an attribute is set to `null` or `undefined`, it will be removed. 249 | * @return {Builder} The Strophe.Builder object. 250 | */ 251 | attrs(moreattrs) { 252 | for (const k in moreattrs) { 253 | if (Object.prototype.hasOwnProperty.call(moreattrs, k)) { 254 | // eslint-disable-next-line no-eq-null 255 | if (moreattrs[k] != null) { 256 | this.node.setAttribute(k, moreattrs[k].toString()); 257 | } else { 258 | this.node.removeAttribute(k); 259 | } 260 | } 261 | } 262 | return this; 263 | } 264 | 265 | /** 266 | * Add a child to the current element and make it the new current 267 | * element. 268 | * 269 | * This function moves the current element pointer to the child, 270 | * unless text is provided. If you need to add another child, it 271 | * is necessary to use up() to go back to the parent in the tree. 272 | * 273 | * @param {string} name - The name of the child. 274 | * @param {Object.|string} [attrs] - The attributes of the child in object notation. 275 | * @param {string} [text] - The text to add to the child. 276 | * 277 | * @return {Builder} The Strophe.Builder object. 278 | */ 279 | c(name, attrs, text) { 280 | const child = xmlElement(name, attrs, text); 281 | this.node.appendChild(child); 282 | if (typeof text !== 'string' && typeof text !== 'number') { 283 | this.node = child; 284 | } 285 | return this; 286 | } 287 | 288 | /** 289 | * Add a child to the current element and make it the new current 290 | * element. 291 | * 292 | * This function is the same as c() except that instead of using a 293 | * name and an attributes object to create the child it uses an 294 | * existing DOM element object. 295 | * 296 | * @param {Element|Builder} elem - A DOM element. 297 | * @return {Builder} The Strophe.Builder object. 298 | */ 299 | cnode(elem) { 300 | if (elem instanceof Builder) { 301 | elem = elem.tree(); 302 | } 303 | let impNode; 304 | const xmlGen = xmlGenerator(); 305 | try { 306 | impNode = xmlGen.importNode !== undefined; 307 | } catch (_e) { 308 | impNode = false; 309 | } 310 | 311 | const newElem = impNode ? xmlGen.importNode(elem, true) : copyElement(elem); 312 | this.node.appendChild(newElem); 313 | this.node = /** @type {Element} */ (newElem); 314 | return this; 315 | } 316 | 317 | /** 318 | * Add a child text element. 319 | * 320 | * This *does not* make the child the new current element since there 321 | * are no children of text elements. 322 | * 323 | * @param {string} text - The text data to append to the current element. 324 | * @return {Builder} The Strophe.Builder object. 325 | */ 326 | t(text) { 327 | const child = xmlTextNode(text); 328 | this.node.appendChild(child); 329 | return this; 330 | } 331 | 332 | /** 333 | * Replace current element contents with the HTML passed in. 334 | * 335 | * This *does not* make the child the new current element 336 | * 337 | * @param {string} html - The html to insert as contents of current element. 338 | * @return {Builder} The Strophe.Builder object. 339 | */ 340 | h(html) { 341 | const fragment = xmlGenerator().createElement('body'); 342 | // force the browser to try and fix any invalid HTML tags 343 | fragment.innerHTML = html; 344 | // copy cleaned html into an xml dom 345 | const xhtml = createHtml(fragment); 346 | while (xhtml.childNodes.length > 0) { 347 | this.node.appendChild(xhtml.childNodes[0]); 348 | } 349 | return this; 350 | } 351 | } 352 | 353 | export default Builder; 354 | --------------------------------------------------------------------------------