Commit bdfafb4 for jssip.net
commit bdfafb47a9fa7c18f61a7d64e777ec166c13c345
Author: José Luis Millán <jmillan@aliax.net>
Date: Mon Jan 26 16:13:57 2026 +0100
Modernize (#955)
- Moderlize eslint
- Use prettier
- Prepare environment for TS
diff --git a/.gitignore b/.gitignore
index b0a5c34..216bf0e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
-/node_modules/
-/dist/
+/node_modules
+/lib
+/dist
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..a824e54
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,10 @@
+{
+ "useTabs": true,
+ "tabWidth": 2,
+ "arrowParens": "avoid",
+ "bracketSpacing": true,
+ "semi": true,
+ "singleQuote": true,
+ "trailingComma": "es5",
+ "endOfLine": "auto"
+}
diff --git a/eslint.config.js b/eslint.config.js
deleted file mode 100644
index aaf9247..0000000
--- a/eslint.config.js
+++ /dev/null
@@ -1,245 +0,0 @@
-const {
- defineConfig,
- globalIgnores,
-} = require("eslint/config");
-
-const globals = require("globals");
-const js = require("@eslint/js");
-
-const {
- FlatCompat,
-} = require("@eslint/eslintrc");
-
-const compat = new FlatCompat({
- baseDirectory: __dirname,
- recommendedConfig: js.configs.recommended,
- allConfig: js.configs.all
-});
-
-module.exports = defineConfig([
- {
- files: ['test/**/*.js'], // Apply these settings only to test files
- languageOptions: {
- globals: {
- jest: true, // Enables Jest global variables like `test`, `describe`, and `expect`
- describe: true, // TMP: explicitly enable globally.
- test: true, // TMP: explicitly enable globally.
- expect: true, // TMP: explicitly enable globally.
- },
- },
- plugins: {
- jest: require('eslint-plugin-jest'), // Load the Jest plugin
- },
- rules: {
- ...require('eslint-plugin-jest').configs.recommended.rules, // Use Jest's recommended rules
- },
- },
- {
- languageOptions: {
- globals: {
- ...globals.browser,
- ...globals.node,
- },
-
- ecmaVersion: 6,
- sourceType: "module",
-
- parserOptions: {
- ecmaFeatures: {
- impliedStrict: true,
- },
- },
- },
-
- plugins: {},
- extends: compat.extends("eslint:recommended"),
- settings: {},
-
- rules: {
- "array-bracket-spacing": [2, "always", {
- objectsInArrays: true,
- arraysInArrays: true,
- }],
-
- "arrow-parens": [2, "always"],
- "arrow-spacing": 2,
- "block-spacing": [2, "always"],
-
- "brace-style": [2, "allman", {
- allowSingleLine: true,
- }],
-
- "camelcase": 0,
- "comma-dangle": 2,
-
- "comma-spacing": [2, {
- before: false,
- after: true,
- }],
-
- "comma-style": 2,
- "computed-property-spacing": 2,
- "constructor-super": 2,
- "func-call-spacing": 2,
- "generator-star-spacing": 2,
- "guard-for-in": 2,
-
- "indent": [2, 2, {
- "SwitchCase": 1,
- }],
-
- "key-spacing": [2, {
- singleLine: {
- beforeColon: false,
- afterColon: true,
- },
-
- multiLine: {
- beforeColon: true,
- afterColon: true,
- align: "colon",
- },
- }],
-
- "keyword-spacing": 2,
- "linebreak-style": [2, "unix"],
-
- "lines-around-comment": [2, {
- allowBlockStart: true,
- allowObjectStart: true,
- beforeBlockComment: true,
- beforeLineComment: false,
- }],
-
- "max-len": [2, 90, {
- tabWidth: 2,
- comments: 110,
- ignoreUrls: true,
- ignoreStrings: true,
- ignoreTemplateLiterals: true,
- ignoreRegExpLiterals: true,
- }],
-
- "newline-after-var": 2,
- "newline-before-return": 2,
- "newline-per-chained-call": 2,
- "no-alert": 2,
- "no-caller": 2,
- "no-case-declarations": 2,
- "no-catch-shadow": 2,
- "no-class-assign": 2,
- "no-confusing-arrow": 2,
-
- "no-console": [2, {
- allow: ["warn"],
- }],
-
- "no-const-assign": 2,
-
- "no-constant-condition": [2, {
- "checkLoops": false,
- }],
-
- "no-debugger": 2,
- "no-dupe-args": 2,
- "no-dupe-keys": 2,
- "no-duplicate-case": 2,
- "no-div-regex": 2,
-
- "no-empty": [2, {
- allowEmptyCatch: true,
- }],
-
- "no-empty-pattern": 2,
- "no-else-return": 0,
- "no-eval": 2,
- "no-extend-native": 2,
- "no-ex-assign": 2,
- "no-extra-bind": 2,
- "no-extra-boolean-cast": 2,
- "no-extra-label": 2,
- "no-extra-semi": 2,
- "no-fallthrough": 2,
- "no-func-assign": 2,
- "no-global-assign": 2,
- "no-implicit-coercion": 2,
- "no-implicit-globals": 2,
- "no-inner-declarations": 2,
- "no-invalid-regexp": 2,
- "no-invalid-this": 0,
- "no-irregular-whitespace": 2,
- "no-lonely-if": 2,
- "no-mixed-operators": 2,
- "no-mixed-spaces-and-tabs": 2,
- "no-multi-spaces": 2,
- "no-multi-str": 2,
- "no-multiple-empty-lines": 2,
- "no-native-reassign": 2,
- "no-negated-in-lhs": 2,
- "no-new": 2,
- "no-new-func": 2,
- "no-new-wrappers": 2,
- "no-obj-calls": 2,
- "no-proto": 2,
- "no-prototype-builtins": 0,
- "no-redeclare": 2,
- "no-regex-spaces": 2,
- "no-restricted-imports": 2,
- "no-return-assign": 2,
- "no-self-assign": 2,
- "no-self-compare": 2,
- "no-sequences": 2,
- "no-shadow": 2,
- "no-shadow-restricted-names": 2,
- "no-spaced-func": 2,
- "no-sparse-arrays": 2,
- "no-this-before-super": 2,
- "no-throw-literal": 2,
- "no-trailing-spaces": 2,
- "no-undef": 2,
- "no-unexpected-multiline": 2,
- "no-unmodified-loop-condition": 2,
- "no-unreachable": 2,
-
- "no-unused-vars": [1, {
- vars: "all",
- args: "after-used",
- }],
-
- "no-use-before-define": [2, {
- functions: false,
- }],
-
- "no-useless-call": 2,
- "no-useless-computed-key": 2,
- "no-useless-concat": 2,
- "no-useless-rename": 2,
- "no-var": 2,
- "no-whitespace-before-property": 2,
- "object-curly-newline": 0,
- "object-curly-spacing": [2, "always"],
-
- "object-property-newline": [2, {
- allowMultiplePropertiesPerLine: true,
- }],
-
- "prefer-const": 2,
- "prefer-rest-params": 2,
- "prefer-spread": 2,
- "prefer-template": 2,
-
- "quotes": [2, "single", {
- avoidEscape: true,
- }],
-
- "semi": [2, "always"],
- "semi-spacing": 2,
- "space-before-blocks": 2,
- "space-before-function-paren": [2, "never"],
- "space-in-parens": [2, "never"],
- "spaced-comment": [2, "always"],
- "strict": 2,
- "valid-typeof": 2,
- "yoda": 2,
- },
-}, globalIgnores(["src/Grammar.js"])]);
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 0000000..e23db21
--- /dev/null
+++ b/eslint.config.mjs
@@ -0,0 +1,186 @@
+import eslint from '@eslint/js';
+import tsEslint from 'typescript-eslint';
+import jestEslint from 'eslint-plugin-jest';
+import prettierRecommendedEslint from 'eslint-plugin-prettier/recommended';
+import globals from 'globals';
+
+const config = tsEslint.config(
+ {
+ languageOptions: {
+ sourceType: 'module',
+ globals: { ...globals.node, ...globals.jest },
+ },
+ linterOptions: {
+ noInlineConfig: false,
+ reportUnusedDisableDirectives: 'error',
+ },
+ },
+ eslint.configs.recommended,
+ prettierRecommendedEslint,
+ {
+ rules: {
+ 'constructor-super': 2,
+ curly: [2, 'all'],
+ // Unfortunatelly `curly` does not apply to blocks in `switch` cases so
+ // this is needed.
+ 'no-restricted-syntax': [
+ 2,
+ {
+ selector: 'SwitchCase > *.consequent[type!="BlockStatement"]',
+ message: 'Switch cases without blocks are disallowed',
+ },
+ ],
+ 'guard-for-in': 2,
+ 'newline-after-var': 2,
+ 'newline-before-return': 2,
+ 'no-alert': 2,
+ 'no-caller': 2,
+ 'no-case-declarations': 2,
+ 'no-catch-shadow': 2,
+ 'no-class-assign': 2,
+ 'no-console': 2,
+ 'no-const-assign': 2,
+ 'no-debugger': 2,
+ 'no-dupe-args': 2,
+ 'no-dupe-keys': 2,
+ 'no-duplicate-case': 2,
+ 'no-div-regex': 2,
+ 'no-empty': [2, { allowEmptyCatch: true }],
+ 'no-empty-pattern': 2,
+ 'no-eval': 2,
+ 'no-extend-native': 2,
+ 'no-ex-assign': 2,
+ 'no-extra-bind': 2,
+ 'no-extra-boolean-cast': 2,
+ 'no-extra-label': 2,
+ 'no-fallthrough': 2,
+ 'no-func-assign': 2,
+ 'no-global-assign': 2,
+ 'no-implicit-coercion': 2,
+ 'no-implicit-globals': 2,
+ 'no-inner-declarations': 2,
+ 'no-invalid-regexp': 2,
+ 'no-invalid-this': 2,
+ 'no-irregular-whitespace': 2,
+ 'no-lonely-if': 2,
+ 'no-multi-str': 2,
+ 'no-native-reassign': 2,
+ 'no-negated-in-lhs': 2,
+ 'no-new': 2,
+ 'no-new-func': 2,
+ 'no-new-wrappers': 2,
+ 'no-obj-calls': 2,
+ 'no-proto': 2,
+ 'no-prototype-builtins': 0,
+ 'no-redeclare': 2,
+ 'no-regex-spaces': 2,
+ 'no-restricted-imports': 2,
+ 'no-return-assign': 2,
+ 'no-self-assign': 2,
+ 'no-self-compare': 2,
+ 'no-sequences': 2,
+ 'no-shadow': 2,
+ 'no-shadow-restricted-names': 2,
+ 'no-sparse-arrays': 2,
+ 'no-this-before-super': 2,
+ 'no-throw-literal': 2,
+ 'no-undef': 2,
+ 'no-unmodified-loop-condition': 2,
+ 'no-unreachable': 2,
+ 'no-unused-vars': [
+ 2,
+ { vars: 'all', args: 'after-used', caughtErrors: 'none' },
+ ],
+ 'no-use-before-define': 0,
+ 'no-useless-call': 2,
+ 'no-useless-computed-key': 2,
+ 'no-useless-concat': 2,
+ 'no-useless-rename': 2,
+ 'no-var': 2,
+ 'object-curly-newline': 0,
+ 'prefer-const': 2,
+ 'prefer-rest-params': 2,
+ 'prefer-spread': 2,
+ 'prefer-template': 2,
+ 'spaced-comment': [2, 'always'],
+ strict: 2,
+ 'valid-typeof': 2,
+ yoda: 2,
+ },
+ },
+ // NOTE: We need to apply this only to .ts source files (and not to .mjs
+ // files).
+ ...tsEslint.configs.recommendedTypeChecked.map(item => ({
+ ...item,
+ files: ['src/**/*.ts'],
+ })),
+ // NOTE: We need to apply this only to .ts source files (and not to .mjs
+ // files).
+ ...tsEslint.configs.stylisticTypeChecked.map(item => ({
+ ...item,
+ files: ['src/**/*.ts'],
+ })),
+ {
+ name: '.ts source files',
+ files: ['src/**/*.ts'],
+ languageOptions: {
+ parserOptions: {
+ project: 'tsconfig.json',
+ },
+ },
+ rules: {
+ '@typescript-eslint/consistent-generic-constructors': [
+ 2,
+ 'type-annotation',
+ ],
+ '@typescript-eslint/prefer-function-type': 0,
+ '@typescript-eslint/dot-notation': 0,
+ '@typescript-eslint/no-unused-vars': [
+ 2,
+ {
+ vars: 'all',
+ args: 'after-used',
+ caughtErrors: 'none',
+ ignoreRestSiblings: false,
+ },
+ ],
+ // We want to use `type` instead of `interface`.
+ '@typescript-eslint/consistent-type-definitions': 0,
+ '@typescript-eslint/explicit-function-return-type': [
+ 2,
+ { allowExpressions: true },
+ ],
+ '@typescript-eslint/no-inferrable-types': 0,
+ '@typescript-eslint/no-unsafe-member-access': 0,
+ '@typescript-eslint/no-unsafe-assignment': 0,
+ '@typescript-eslint/no-unsafe-call': 0,
+ '@typescript-eslint/no-unsafe-return': 0,
+ '@typescript-eslint/no-unsafe-argument': 0,
+ '@typescript-eslint/consistent-indexed-object-style': 0,
+ '@typescript-eslint/no-empty-function': 0,
+ '@typescript-eslint/prefer-nullish-coalescing': 0,
+ '@typescript-eslint/prefer-regexp-exec': 0,
+ '@typescript-eslint/require-await': 0,
+ '@typescript-eslint/restrict-template-expressions': 0,
+ '@typescript-eslint/unbound-method': 0,
+ '@typescript-eslint/no-redundant-type-constituents': 0,
+ },
+ },
+ {
+ name: '.ts test files',
+ ...jestEslint.configs['flat/recommended'],
+ files: ['src/test/**/*.ts'],
+ rules: {
+ ...jestEslint.configs['flat/recommended'].rules,
+ 'jest/no-disabled-tests': 2,
+ 'jest/prefer-expect-assertions': 0,
+ '@typescript-eslint/no-unnecessary-type-assertion': 0,
+ },
+ },
+ {
+ name: 'lib/ files',
+ ignores: ['lib/**'],
+ }
+);
+
+export default config;
diff --git a/jest.config.js b/jest.config.js
index 5e022fc..37e2081 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -1,3 +1,3 @@
module.exports = {
- testRegex : 'test/test-.*\\.js'
+ testRegex: 'test/test-.*\\.js',
};
diff --git a/npm-scripts.js b/npm-scripts.js
deleted file mode 100644
index 0ff5de2..0000000
--- a/npm-scripts.js
+++ /dev/null
@@ -1,159 +0,0 @@
-const esbuild = require("esbuild");
-const fs = require('fs');
-const path = require('path');
-const process = require('process');
-const { execSync } = require('child_process');
-const pkg = require('./package.json');
-
-const task = process.argv.slice(2).join(' ');
-
-const ESLINT_PATHS = [ 'src', 'test' ].join(' ');
-
-// eslint-disable-next-line no-console
-console.log(`npm-scripts.js [INFO] running task "${task}"`);
-
-void run();
-
-async function run() {
- switch (task)
- {
- case 'grammar': {
- grammar();
- break;
- }
-
- case 'lint': {
- lint();
- break;
- }
-
- case 'test': {
- test();
- break;
- }
-
- case 'build': {
- await build(true /* minify */);
- await build(false /* minify */);
-
- break;
- }
-
- case 'release': {
- lint();
- test();
- executeCmd(`git commit -am '${pkg.version}'`);
- executeCmd(`git tag -a ${pkg.version} -m '${pkg.version}'`);
- executeCmd('git push origin master && git push origin --tags');
- executeCmd('npm publish');
-
- // eslint-disable-next-line no-console
- console.log('update tryit-jssip and JsSIP website');
- break;
- }
-
- default: {
- throw new TypeError(`unknown task "${task}"`);
- }
- }
-}
-
-function lint()
-{
- logInfo('lint()');
-
- executeCmd(`eslint -c eslint.config.js --max-warnings 0 ${ESLINT_PATHS}`);
-}
-
-function test()
-{
- logInfo('test()');
-
- executeCmd('jest');
-}
-
-function grammar()
-{
- logInfo('grammar()');
-
- const local_pegjs = path.resolve('./node_modules/.bin/pegjs');
- const Grammar_pegjs = path.resolve('src/Grammar.pegjs');
- const Grammar_js = path.resolve('src/Grammar.js');
-
- logInfo('compiling Grammar.pegjs into Grammar.js...');
-
- executeCmd(`${local_pegjs} ${Grammar_pegjs} ${Grammar_js}`);
-
- logInfo('grammar compiled');
-
- // Modify the generated Grammar.js file with custom changes.
- logInfo('applying custom changes to Grammar.js...');
-
- const current_grammar = fs.readFileSync('src/Grammar.js').toString();
- let modified_grammar = current_grammar.replace(
- /throw new this\.SyntaxError\(([\s\S]*?)\);([\s\S]*?)}([\s\S]*?)return result;/,
- 'new this.SyntaxError($1);\n return -1;$2}$3return data;'
- );
-
- modified_grammar = modified_grammar.replace(/\s+$/gm, '');
- fs.writeFileSync('src/Grammar.js', modified_grammar);
-
- logInfo('grammar done');
-}
-
-// Build sources into a file for publishing.
-async function build(minify = true) {
- const entry = path.resolve("src/JsSIP.js");
- const outfile = path.resolve("./dist", `jssip${minify ? '.min' : ''}.js`);
- const banner = `
- /*
- * JsSIP ${pkg.version}
- * ${pkg.description}
- * Copyright: 2012-${new Date().getFullYear()} ${pkg.contributors.join(' ')}
- * Homepage: ${pkg.homepage}
- * License: ${pkg.license}
- */`;
-
- await esbuild.build({
- entryPoints: [entry],
- outfile,
- bundle: true,
- minify,
- sourcemap: false,
- // https://esbuild.github.io/api/#global-name.
- format: "iife",
- globalName: "JsSIP",
- platform: "browser",
- target: ["es2015"],
- // Make the generated output a single line.
- supported: {
- "template-literal": false,
- },
- // Add banner.
- banner: {
- js: banner,
- },
- });
-}
-
-function executeCmd(command)
-{
- // eslint-disable-next-line no-console
- console.log(`npm-scripts.js [INFO] executing command: ${command}`);
-
- try
- {
- execSync(command, { stdio: [ 'ignore', process.stdout, process.stderr ] });
- }
- // eslint-disable-next-line no-unused-vars
- catch (error)
- {
- process.exit(1);
- }
-}
-
-function logInfo(...args)
-{
- // eslint-disable-next-line no-console
- console.log(`npm-scripts.mjs \x1b[36m[INFO] [${task}]\x1b[0m`, ...args);
-}
diff --git a/npm-scripts.mjs b/npm-scripts.mjs
new file mode 100644
index 0000000..306e538
--- /dev/null
+++ b/npm-scripts.mjs
@@ -0,0 +1,201 @@
+import esbuild from 'esbuild';
+import fs from 'fs';
+import path from 'path';
+import process from 'process';
+import { execSync } from 'child_process';
+import pkg from './package.json' with { type: 'json' };
+
+const task = process.argv.slice(2).join(' ');
+const taskArgs = process.argv.slice(3).join(' ');
+
+// Paths for ESLint to check. Converted to string for convenience.
+const ESLINT_PATHS = [
+ 'eslint.config.mjs',
+ // "jest.config.mjs",
+ 'npm-scripts.mjs',
+ 'src',
+ 'test',
+].join(' ');
+
+// Paths for ESLint to ignore. Converted to string argument for convenience.
+const ESLINT_IGNORE_PATTERN_ARGS = ['src/Grammar.pegs', 'src/Grammar.js']
+ .map(entry => `--ignore-pattern ${entry}`)
+ .join(' ');
+
+logInfo(`running task "${task}"`);
+logInfo(taskArgs ? `[args:"${taskArgs}"]` : '');
+
+void run();
+
+async function run() {
+ switch (task) {
+ case 'grammar': {
+ grammar();
+ break;
+ }
+
+ case 'lint': {
+ lint();
+ break;
+ }
+
+ case 'lint:fix': {
+ lint(true);
+ break;
+ }
+
+ case 'test': {
+ test();
+ break;
+ }
+
+ case 'build': {
+ buildTypescript();
+ await build(true /* minify */);
+ await build(false /* minify */);
+
+ break;
+ }
+
+ case 'typescript:build': {
+ buildTypescript();
+
+ break;
+ }
+
+ case 'release': {
+ lint();
+ test();
+ executeCmd(`git commit -am '${pkg.version}'`);
+ executeCmd(`git tag -a ${pkg.version} -m '${pkg.version}'`);
+ executeCmd('git push origin master && git push origin --tags');
+ executeCmd('npm publish');
+
+ // eslint-disable-next-line no-console
+ console.log('update tryit-jssip and JsSIP website');
+ break;
+ }
+
+ default: {
+ throw new TypeError(`unknown task "${task}"`);
+ }
+ }
+}
+
+function lint(fix = false) {
+ logInfo(`lint() [fix:${fix}]`);
+
+ executeCmd(
+ `eslint -c eslint.config.mjs --max-warnings 0 ${fix ? '--fix' : ''} ${ESLINT_PATHS} ${ESLINT_IGNORE_PATTERN_ARGS}`
+ );
+}
+
+function test() {
+ logInfo('test()');
+
+ // TODO: remove when tests are written in TS.
+ buildTypescript();
+ executeCmd('jest');
+}
+
+function grammar() {
+ logInfo('grammar()');
+
+ const local_pegjs = path.resolve('./node_modules/.bin/pegjs');
+ const Grammar_pegjs = path.resolve('src/Grammar.pegjs');
+ const Grammar_js = path.resolve('src/Grammar.js');
+
+ logInfo('compiling Grammar.pegjs into Grammar.js...');
+
+ executeCmd(`${local_pegjs} ${Grammar_pegjs} ${Grammar_js}`);
+
+ logInfo('grammar compiled');
+
+ // Modify the generated Grammar.js file with custom changes.
+ logInfo('applying custom changes to Grammar.js...');
+
+ const current_grammar = fs.readFileSync('src/Grammar.js').toString();
+ let modified_grammar = current_grammar.replace(
+ /throw new this\.SyntaxError\(([\s\S]*?)\);([\s\S]*?)}([\s\S]*?)return result;/,
+ 'new this.SyntaxError($1);\n return -1;$2}$3return data;'
+ );
+
+ modified_grammar = modified_grammar.replace(/\s+$/gm, '');
+ fs.writeFileSync('src/Grammar.js', modified_grammar);
+
+ logInfo('grammar done');
+}
+
+// Build sources into a file for publishing.
+async function build(minify = true) {
+ const entry = path.resolve('lib/JsSIP.js');
+ const outfile = path.resolve('./dist', `jssip${minify ? '.min' : ''}.js`);
+ const banner = `
+ /*
+ * JsSIP ${pkg.version}
+ * ${pkg.description}
+ * Copyright: 2012-${new Date().getFullYear()} ${pkg.contributors.join(' ')}
+ * Homepage: ${pkg.homepage}
+ * License: ${pkg.license}
+ */`;
+
+ await esbuild.build({
+ entryPoints: [entry],
+ outfile,
+ bundle: true,
+ minify,
+ sourcemap: false,
+ // https://esbuild.github.io/api/#global-name.
+ format: 'iife',
+ globalName: 'JsSIP',
+ platform: 'browser',
+ target: ['es2015'],
+ // Make the generated output a single line.
+ supported: {
+ 'template-literal': false,
+ },
+ // Add banner.
+ banner: {
+ js: banner,
+ },
+ });
+}
+
+function buildTypescript() {
+ logInfo('buildTypescript()');
+
+ deleteLib();
+
+ // Generate .js CommonJS files in lib/.
+ executeCmd(`tsc ${taskArgs}`);
+
+ // Copy manual .d.ts files to lib/ until code is moved to TS and declaration files
+ // are automatically created.
+ executeCmd("cpx 'src/**/**.d.ts' lib/");
+}
+
+function deleteLib() {
+ if (!fs.existsSync('lib')) {
+ return;
+ }
+
+ logInfo('deleteLib()');
+
+ fs.rmSync('lib', { recursive: true, force: true });
+}
+
+function executeCmd(command) {
+ // eslint-disable-next-line no-console
+ console.log(`npm-scripts.js [INFO] executing command: ${command}`);
+
+ try {
+ execSync(command, { stdio: ['ignore', process.stdout, process.stderr] });
+ } catch (error) {
+ process.exit(1);
+ }
+}
+
+function logInfo(...args) {
+ // eslint-disable-next-line no-console
+ console.log(`npm-scripts.mjs \x1b[36m[INFO] [${task}]\x1b[0m`, ...args);
+}
diff --git a/package.json b/package.json
index d03357a..603ef16 100644
--- a/package.json
+++ b/package.json
@@ -1,52 +1,67 @@
{
- "name": "jssip",
- "title": "JsSIP",
- "description": "The Javascript SIP library",
- "version": "3.12.0",
- "homepage": "https://jssip.net",
- "contributors": [
- "José Luis Millán <jmillan@aliax.net> (https://github.com/jmillan)",
- "Iñaki Baz Castillo <ibc@aliax.net> (https://inakibaz.me)"
- ],
- "types": "src/JsSIP.d.ts",
- "main": "src/JsSIP.js",
- "keywords": [
- "sip",
- "websocket",
- "webrtc",
- "node",
- "browser",
- "library"
- ],
- "license": "MIT",
- "repository": {
- "type": "git",
- "url": "https://github.com/versatica/JsSIP.git"
- },
- "bugs": {
- "url": "https://github.com/versatica/JsSIP/issues"
- },
- "dependencies": {
- "debug": "^4.3.1",
- "events": "^3.3.0",
- "sdp-transform": "^2.14.1"
- },
- "devDependencies": {
- "@eslint/eslintrc": "^3.3.3",
- "@eslint/js": "^9.39.2",
- "@types/debug": "^4.1.12",
- "@types/events": "^3.0.3",
- "esbuild": "^0.27.2",
- "eslint": "^9.39.1",
- "eslint-plugin-jest": "^29.12.1",
- "globals": "^17.0.0",
- "jest": "^30.2.0",
- "pegjs": "^0.7.0"
- },
- "scripts": {
- "lint": "node npm-scripts.js lint",
- "test": "node npm-scripts.js test",
- "build": "node npm-scripts.js build",
- "release": "node npm-scripts.js release"
- }
+ "name": "jssip",
+ "title": "JsSIP",
+ "description": "The Javascript SIP library",
+ "version": "3.12.0",
+ "homepage": "https://jssip.net",
+ "contributors": [
+ "José Luis Millán <jmillan@aliax.net> (https://github.com/jmillan)",
+ "Iñaki Baz Castillo <ibc@aliax.net> (https://inakibaz.me)"
+ ],
+ "types": "lib/JsSIP.d.ts",
+ "main": "lib/JsSIP.js",
+ "keywords": [
+ "sip",
+ "websocket",
+ "webrtc",
+ "node",
+ "browser",
+ "library"
+ ],
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/versatica/JsSIP.git"
+ },
+ "bugs": {
+ "url": "https://github.com/versatica/JsSIP/issues"
+ },
+ "files": [
+ "LICENSE",
+ "README.md",
+ "npm-scripts.mjs",
+ "lib"
+ ],
+ "scripts": {
+ "lint": "node npm-scripts.mjs lint",
+ "lint:fix": "node npm-scripts.mjs lint:fix",
+ "test": "node npm-scripts.mjs test",
+ "build": "node npm-scripts.mjs build",
+ "typescript:build": "node npm-scripts.mjs typescript:build",
+ "release": "node npm-scripts.js release"
+ },
+ "dependencies": {
+ "debug": "^4.3.1",
+ "events": "^3.3.0",
+ "sdp-transform": "^2.14.1"
+ },
+ "devDependencies": {
+ "@eslint/eslintrc": "^3.3.3",
+ "@eslint/js": "^9.39.2",
+ "@types/debug": "^4.1.12",
+ "@types/events": "^3.0.3",
+ "@types/node": "^25.0.10",
+ "cpx": "^1.5.0",
+ "esbuild": "^0.27.2",
+ "eslint": "^9.39.1",
+ "eslint-config-prettier": "^10.1.8",
+ "eslint-plugin-jest": "^29.12.1",
+ "eslint-plugin-prettier": "^5.5.5",
+ "globals": "^17.0.0",
+ "jest": "^30.2.0",
+ "pegjs": "^0.7.0",
+ "prettier": "^3.8.1",
+ "typescript": "^5.9.3",
+ "typescript-eslint": "^8.53.1"
+ }
}
diff --git a/src/Config.js b/src/Config.js
index 46f929e..64689ff 100644
--- a/src/Config.js
+++ b/src/Config.js
@@ -7,397 +7,308 @@ const Exceptions = require('./Exceptions');
// Default settings.
exports.settings = {
- // SIP authentication.
- authorization_user : null,
- password : null,
- realm : null,
- ha1 : null,
- authorization_jwt : null,
-
- // SIP account.
- display_name : null,
- uri : null,
- contact_uri : null,
-
- // SIP instance id (GRUU).
- instance_id : null,
-
- // Preloaded SIP Route header field.
- use_preloaded_route : false,
-
- // Session parameters.
- session_timers : true,
- session_timers_refresh_method : JsSIP_C.UPDATE,
- session_timers_force_refresher : false,
- no_answer_timeout : 60,
-
- // Registration parameters.
- register : true,
- register_expires : 600,
- register_from_tag_trail : '',
- registrar_server : null,
-
- // Connection options.
- sockets : null,
- connection_recovery_max_interval : JsSIP_C.CONNECTION_RECOVERY_MAX_INTERVAL,
- connection_recovery_min_interval : JsSIP_C.CONNECTION_RECOVERY_MIN_INTERVAL,
-
- // Global extra headers, to be added to every request and response
- extra_headers : null,
-
- /*
- * Host address.
- * Value to be set in Via sent_by and host part of Contact FQDN.
- */
- via_host : `${Utils.createRandomToken(12)}.invalid`
+ // SIP authentication.
+ authorization_user: null,
+ password: null,
+ realm: null,
+ ha1: null,
+ authorization_jwt: null,
+
+ // SIP account.
+ display_name: null,
+ uri: null,
+ contact_uri: null,
+
+ // SIP instance id (GRUU).
+ instance_id: null,
+
+ // Preloaded SIP Route header field.
+ use_preloaded_route: false,
+
+ // Session parameters.
+ session_timers: true,
+ session_timers_refresh_method: JsSIP_C.UPDATE,
+ session_timers_force_refresher: false,
+ no_answer_timeout: 60,
+
+ // Registration parameters.
+ register: true,
+ register_expires: 600,
+ register_from_tag_trail: '',
+ registrar_server: null,
+
+ // Connection options.
+ sockets: null,
+ connection_recovery_max_interval: JsSIP_C.CONNECTION_RECOVERY_MAX_INTERVAL,
+ connection_recovery_min_interval: JsSIP_C.CONNECTION_RECOVERY_MIN_INTERVAL,
+
+ // Global extra headers, to be added to every request and response
+ extra_headers: null,
+
+ /*
+ * Host address.
+ * Value to be set in Via sent_by and host part of Contact FQDN.
+ */
+ via_host: `${Utils.createRandomToken(12)}.invalid`,
};
// Configuration checks.
const checks = {
- mandatory : {
-
- sockets(sockets)
- {
- /* Allow defining sockets parameter as:
- * Socket: socket
- * Array of Socket: [socket1, socket2]
- * Array of Objects: [{socket: socket1, weight:1}, {socket: Socket2, weight:0}]
- * Array of Objects and Socket: [{socket: socket1}, socket2]
- */
- const _sockets = [];
-
- if (Socket.isSocket(sockets))
- {
- _sockets.push({ socket: sockets });
- }
- else if (Array.isArray(sockets) && sockets.length)
- {
- for (const socket of sockets)
- {
- if (Object.prototype.hasOwnProperty.call(socket, 'socket') &&
- Socket.isSocket(socket.socket))
- {
- _sockets.push(socket);
- }
- else if (Socket.isSocket(socket))
- {
- _sockets.push({ socket: socket });
- }
- }
- }
- else
- {
- return;
- }
-
- return _sockets;
- },
-
- uri(uri)
- {
- if (!/^sip:/i.test(uri))
- {
- uri = `${JsSIP_C.SIP}:${uri}`;
- }
- const parsed = URI.parse(uri);
-
- if (!parsed)
- {
- return;
- }
- else if (!parsed.user)
- {
- return;
- }
- else
- {
- return parsed;
- }
- }
- },
-
- optional : {
-
- authorization_user(authorization_user)
- {
- if (Grammar.parse(`"${authorization_user}"`, 'quoted_string') === -1)
- {
- return;
- }
- else
- {
- return authorization_user;
- }
- },
- authorization_jwt(authorization_jwt)
- {
- if (typeof authorization_jwt === 'string')
- {
- return authorization_jwt;
- }
- },
- user_agent(user_agent)
- {
- if (typeof user_agent === 'string')
- {
- return user_agent;
- }
- },
-
- connection_recovery_max_interval(connection_recovery_max_interval)
- {
- if (Utils.isDecimal(connection_recovery_max_interval))
- {
- const value = Number(connection_recovery_max_interval);
-
- if (value > 0)
- {
- return value;
- }
- }
- },
-
- connection_recovery_min_interval(connection_recovery_min_interval)
- {
- if (Utils.isDecimal(connection_recovery_min_interval))
- {
- const value = Number(connection_recovery_min_interval);
-
- if (value > 0)
- {
- return value;
- }
- }
- },
-
- contact_uri(contact_uri)
- {
- if (typeof contact_uri === 'string')
- {
- const uri = Grammar.parse(contact_uri, 'SIP_URI');
-
- if (uri !== -1)
- {
- return uri;
- }
- }
- },
-
- display_name(display_name)
- {
- return display_name;
- },
-
- instance_id(instance_id)
- {
- if ((/^uuid:/i.test(instance_id)))
- {
- instance_id = instance_id.substr(5);
- }
-
- if (Grammar.parse(instance_id, 'uuid') === -1)
- {
- return;
- }
- else
- {
- return instance_id;
- }
- },
-
- no_answer_timeout(no_answer_timeout)
- {
- if (Utils.isDecimal(no_answer_timeout))
- {
- const value = Number(no_answer_timeout);
-
- if (value > 0)
- {
- return value;
- }
- }
- },
-
- session_timers(session_timers)
- {
- if (typeof session_timers === 'boolean')
- {
- return session_timers;
- }
- },
-
- session_timers_refresh_method(method)
- {
- if (typeof method === 'string')
- {
- method = method.toUpperCase();
-
- if (method === JsSIP_C.INVITE || method === JsSIP_C.UPDATE)
- {
- return method;
- }
- }
- },
-
- session_timers_force_refresher(session_timers_force_refresher)
- {
- if (typeof session_timers_force_refresher === 'boolean')
- {
- return session_timers_force_refresher;
- }
- },
-
- password(password)
- {
- return String(password);
- },
-
- realm(realm)
- {
- return String(realm);
- },
-
- ha1(ha1)
- {
- return String(ha1);
- },
-
- register(register)
- {
- if (typeof register === 'boolean')
- {
- return register;
- }
- },
-
- register_expires(register_expires)
- {
- if (Utils.isDecimal(register_expires))
- {
- const value = Number(register_expires);
-
- if (value >= 0)
- {
- return value;
- }
- }
- },
-
- register_from_tag_trail(register_from_tag_trail)
- {
- if (typeof register_from_tag_trail === 'function')
- {
- return register_from_tag_trail;
- }
-
- return String(register_from_tag_trail);
- },
-
- registrar_server(registrar_server)
- {
- if (!/^sip:/i.test(registrar_server))
- {
- registrar_server = `${JsSIP_C.SIP}:${registrar_server}`;
- }
-
- const parsed = URI.parse(registrar_server);
-
- if (!parsed)
- {
- return;
- }
- else if (parsed.user)
- {
- return;
- }
- else
- {
- return parsed;
- }
- },
-
- use_preloaded_route(use_preloaded_route)
- {
- if (typeof use_preloaded_route === 'boolean')
- {
- return use_preloaded_route;
- }
- },
-
- extra_headers(extra_headers)
- {
- const _extraHeaders = [];
-
- if (Array.isArray(extra_headers) && extra_headers.length)
- {
- for (const header of extra_headers)
- {
- if (typeof header === 'string')
- {
- _extraHeaders.push(header);
- }
- }
- }
- else
- {
- return;
- }
-
- return _extraHeaders;
- }
- }
+ mandatory: {
+ sockets(sockets) {
+ /* Allow defining sockets parameter as:
+ * Socket: socket
+ * Array of Socket: [socket1, socket2]
+ * Array of Objects: [{socket: socket1, weight:1}, {socket: Socket2, weight:0}]
+ * Array of Objects and Socket: [{socket: socket1}, socket2]
+ */
+ const _sockets = [];
+
+ if (Socket.isSocket(sockets)) {
+ _sockets.push({ socket: sockets });
+ } else if (Array.isArray(sockets) && sockets.length) {
+ for (const socket of sockets) {
+ if (
+ Object.prototype.hasOwnProperty.call(socket, 'socket') &&
+ Socket.isSocket(socket.socket)
+ ) {
+ _sockets.push(socket);
+ } else if (Socket.isSocket(socket)) {
+ _sockets.push({ socket: socket });
+ }
+ }
+ } else {
+ return;
+ }
+
+ return _sockets;
+ },
+
+ uri(uri) {
+ if (!/^sip:/i.test(uri)) {
+ uri = `${JsSIP_C.SIP}:${uri}`;
+ }
+ const parsed = URI.parse(uri);
+
+ if (!parsed) {
+ return;
+ } else if (!parsed.user) {
+ return;
+ } else {
+ return parsed;
+ }
+ },
+ },
+
+ optional: {
+ authorization_user(authorization_user) {
+ if (Grammar.parse(`"${authorization_user}"`, 'quoted_string') === -1) {
+ return;
+ } else {
+ return authorization_user;
+ }
+ },
+ authorization_jwt(authorization_jwt) {
+ if (typeof authorization_jwt === 'string') {
+ return authorization_jwt;
+ }
+ },
+ user_agent(user_agent) {
+ if (typeof user_agent === 'string') {
+ return user_agent;
+ }
+ },
+
+ connection_recovery_max_interval(connection_recovery_max_interval) {
+ if (Utils.isDecimal(connection_recovery_max_interval)) {
+ const value = Number(connection_recovery_max_interval);
+
+ if (value > 0) {
+ return value;
+ }
+ }
+ },
+
+ connection_recovery_min_interval(connection_recovery_min_interval) {
+ if (Utils.isDecimal(connection_recovery_min_interval)) {
+ const value = Number(connection_recovery_min_interval);
+
+ if (value > 0) {
+ return value;
+ }
+ }
+ },
+
+ contact_uri(contact_uri) {
+ if (typeof contact_uri === 'string') {
+ const uri = Grammar.parse(contact_uri, 'SIP_URI');
+
+ if (uri !== -1) {
+ return uri;
+ }
+ }
+ },
+
+ display_name(display_name) {
+ return display_name;
+ },
+
+ instance_id(instance_id) {
+ if (/^uuid:/i.test(instance_id)) {
+ instance_id = instance_id.substr(5);
+ }
+
+ if (Grammar.parse(instance_id, 'uuid') === -1) {
+ return;
+ } else {
+ return instance_id;
+ }
+ },
+
+ no_answer_timeout(no_answer_timeout) {
+ if (Utils.isDecimal(no_answer_timeout)) {
+ const value = Number(no_answer_timeout);
+
+ if (value > 0) {
+ return value;
+ }
+ }
+ },
+
+ session_timers(session_timers) {
+ if (typeof session_timers === 'boolean') {
+ return session_timers;
+ }
+ },
+
+ session_timers_refresh_method(method) {
+ if (typeof method === 'string') {
+ method = method.toUpperCase();
+
+ if (method === JsSIP_C.INVITE || method === JsSIP_C.UPDATE) {
+ return method;
+ }
+ }
+ },
+
+ session_timers_force_refresher(session_timers_force_refresher) {
+ if (typeof session_timers_force_refresher === 'boolean') {
+ return session_timers_force_refresher;
+ }
+ },
+
+ password(password) {
+ return String(password);
+ },
+
+ realm(realm) {
+ return String(realm);
+ },
+
+ ha1(ha1) {
+ return String(ha1);
+ },
+
+ register(register) {
+ if (typeof register === 'boolean') {
+ return register;
+ }
+ },
+
+ register_expires(register_expires) {
+ if (Utils.isDecimal(register_expires)) {
+ const value = Number(register_expires);
+
+ if (value >= 0) {
+ return value;
+ }
+ }
+ },
+
+ register_from_tag_trail(register_from_tag_trail) {
+ if (typeof register_from_tag_trail === 'function') {
+ return register_from_tag_trail;
+ }
+
+ return String(register_from_tag_trail);
+ },
+
+ registrar_server(registrar_server) {
+ if (!/^sip:/i.test(registrar_server)) {
+ registrar_server = `${JsSIP_C.SIP}:${registrar_server}`;
+ }
+
+ const parsed = URI.parse(registrar_server);
+
+ if (!parsed) {
+ return;
+ } else if (parsed.user) {
+ return;
+ } else {
+ return parsed;
+ }
+ },
+
+ use_preloaded_route(use_preloaded_route) {
+ if (typeof use_preloaded_route === 'boolean') {
+ return use_preloaded_route;
+ }
+ },
+
+ extra_headers(extra_headers) {
+ const _extraHeaders = [];
+
+ if (Array.isArray(extra_headers) && extra_headers.length) {
+ for (const header of extra_headers) {
+ if (typeof header === 'string') {
+ _extraHeaders.push(header);
+ }
+ }
+ } else {
+ return;
+ }
+
+ return _extraHeaders;
+ },
+ },
};
-exports.load = (dst, src) =>
-{
- // Check Mandatory parameters.
- for (const parameter in checks.mandatory)
- {
- if (!src.hasOwnProperty(parameter))
- {
- throw new Exceptions.ConfigurationError(parameter);
- }
- else
- {
- const value = src[parameter];
- const checked_value = checks.mandatory[parameter](value);
-
- if (checked_value !== undefined)
- {
- dst[parameter] = checked_value;
- }
- else
- {
- throw new Exceptions.ConfigurationError(parameter, value);
- }
- }
- }
-
- // Check Optional parameters.
- for (const parameter in checks.optional)
- {
- if (src.hasOwnProperty(parameter))
- {
- const value = src[parameter];
-
- /* If the parameter value is null, empty string, undefined, empty array
- * or it's a number with NaN value, then apply its default value.
- */
- if (Utils.isEmpty(value))
- {
- continue;
- }
-
- const checked_value = checks.optional[parameter](value);
-
- if (checked_value !== undefined)
- {
- dst[parameter] = checked_value;
- }
- else
- {
- throw new Exceptions.ConfigurationError(parameter, value);
- }
- }
- }
+exports.load = (dst, src) => {
+ // Check Mandatory parameters.
+ for (const parameter in checks.mandatory) {
+ if (!src.hasOwnProperty(parameter)) {
+ throw new Exceptions.ConfigurationError(parameter);
+ } else {
+ const value = src[parameter];
+ const checked_value = checks.mandatory[parameter](value);
+
+ if (checked_value !== undefined) {
+ dst[parameter] = checked_value;
+ } else {
+ throw new Exceptions.ConfigurationError(parameter, value);
+ }
+ }
+ }
+
+ // Check Optional parameters.
+ for (const parameter in checks.optional) {
+ if (src.hasOwnProperty(parameter)) {
+ const value = src[parameter];
+
+ /* If the parameter value is null, empty string, undefined, empty array
+ * or it's a number with NaN value, then apply its default value.
+ */
+ if (Utils.isEmpty(value)) {
+ continue;
+ }
+
+ const checked_value = checks.optional[parameter](value);
+
+ if (checked_value !== undefined) {
+ dst[parameter] = checked_value;
+ } else {
+ throw new Exceptions.ConfigurationError(parameter, value);
+ }
+ }
+ }
};
diff --git a/src/Constants.d.ts b/src/Constants.d.ts
index 31297cc..b12d171 100644
--- a/src/Constants.d.ts
+++ b/src/Constants.d.ts
@@ -1,66 +1,68 @@
-export const USER_AGENT: string
-export const SIP = 'sip'
-export const SIPS = 'sips'
+export const USER_AGENT: string;
+export const SIP = 'sip';
+export const SIPS = 'sips';
export declare enum causes {
- CONNECTION_ERROR = 'Connection Error',
- REQUEST_TIMEOUT = 'Request Timeout',
- SIP_FAILURE_CODE = 'SIP Failure Code',
- INTERNAL_ERROR = 'Internal Error',
- BUSY = 'Busy',
- REJECTED = 'Rejected',
- REDIRECTED = 'Redirected',
- UNAVAILABLE = 'Unavailable',
- NOT_FOUND = 'Not Found',
- ADDRESS_INCOMPLETE = 'Address Incomplete',
- INCOMPATIBLE_SDP = 'Incompatible SDP',
- MISSING_SDP = 'Missing SDP',
- AUTHENTICATION_ERROR = 'Authentication Error',
- BYE = 'Terminated',
- WEBRTC_ERROR = 'WebRTC Error',
- CANCELED = 'Canceled',
- NO_ANSWER = 'No Answer',
- EXPIRES = 'Expires',
- NO_ACK = 'No ACK',
- DIALOG_ERROR = 'Dialog Error',
- USER_DENIED_MEDIA_ACCESS = 'User Denied Media Access',
- BAD_MEDIA_DESCRIPTION = 'Bad Media Description',
- RTP_TIMEOUT = 'RTP Timeout',
+ CONNECTION_ERROR = 'Connection Error',
+ REQUEST_TIMEOUT = 'Request Timeout',
+ SIP_FAILURE_CODE = 'SIP Failure Code',
+ INTERNAL_ERROR = 'Internal Error',
+ BUSY = 'Busy',
+ REJECTED = 'Rejected',
+ REDIRECTED = 'Redirected',
+ UNAVAILABLE = 'Unavailable',
+ NOT_FOUND = 'Not Found',
+ ADDRESS_INCOMPLETE = 'Address Incomplete',
+ INCOMPATIBLE_SDP = 'Incompatible SDP',
+ MISSING_SDP = 'Missing SDP',
+ AUTHENTICATION_ERROR = 'Authentication Error',
+ BYE = 'Terminated',
+ WEBRTC_ERROR = 'WebRTC Error',
+ CANCELED = 'Canceled',
+ NO_ANSWER = 'No Answer',
+ EXPIRES = 'Expires',
+ NO_ACK = 'No ACK',
+ DIALOG_ERROR = 'Dialog Error',
+ USER_DENIED_MEDIA_ACCESS = 'User Denied Media Access',
+ BAD_MEDIA_DESCRIPTION = 'Bad Media Description',
+ RTP_TIMEOUT = 'RTP Timeout',
}
export const SIP_ERROR_CAUSES: {
- REDIRECTED: [300, 301, 302, 305, 380],
- BUSY: [486, 600],
- REJECTED: [403, 603],
- NOT_FOUND: [404, 604],
- UNAVAILABLE: [480, 410, 408, 430],
- ADDRESS_INCOMPLETE: [484, 424],
- INCOMPATIBLE_SDP: [488, 606],
- AUTHENTICATION_ERROR: [401, 407]
-}
-export const ACK = 'ACK'
-export const BYE = 'BYE'
-export const CANCEL = 'CANCEL'
-export const INFO = 'INFO'
-export const INVITE = 'INVITE'
-export const MESSAGE = 'MESSAGE'
-export const NOTIFY = 'NOTIFY'
-export const OPTIONS = 'OPTIONS'
-export const REGISTER = 'REGISTER'
-export const REFER = 'REFER'
-export const UPDATE = 'UPDATE'
-export const SUBSCRIBE = 'SUBSCRIBE'
+ REDIRECTED: [300, 301, 302, 305, 380];
+ BUSY: [486, 600];
+ REJECTED: [403, 603];
+ NOT_FOUND: [404, 604];
+ UNAVAILABLE: [480, 410, 408, 430];
+ ADDRESS_INCOMPLETE: [484, 424];
+ INCOMPATIBLE_SDP: [488, 606];
+ AUTHENTICATION_ERROR: [401, 407];
+};
+export const ACK = 'ACK';
+export const BYE = 'BYE';
+export const CANCEL = 'CANCEL';
+export const INFO = 'INFO';
+export const INVITE = 'INVITE';
+export const MESSAGE = 'MESSAGE';
+export const NOTIFY = 'NOTIFY';
+export const OPTIONS = 'OPTIONS';
+export const REGISTER = 'REGISTER';
+export const REFER = 'REFER';
+export const UPDATE = 'UPDATE';
+export const SUBSCRIBE = 'SUBSCRIBE';
export declare enum DTMF_TRANSPORT {
- INFO = 'INFO',
- RFC2833 = 'RFC2833',
+ // eslint-disable-next-line no-shadow
+ INFO = 'INFO',
+ RFC2833 = 'RFC2833',
}
-export const REASON_PHRASE: Record<number, string>
-export const ALLOWED_METHODS = 'INVITE,ACK,CANCEL,BYE,UPDATE,MESSAGE,OPTIONS,REFER,INFO,NOTIFY,SUBSCRIBE'
-export const ACCEPTED_BODY_TYPES = 'application/sdp, application/dtmf-relay'
-export const MAX_FORWARDS = 69
-export const SESSION_EXPIRES = 90
-export const MIN_SESSION_EXPIRES = 60
-export const CONNECTION_RECOVERY_MAX_INTERVAL = 30
-export const CONNECTION_RECOVERY_MIN_INTERVAL = 2
+export const REASON_PHRASE: Record<number, string>;
+export const ALLOWED_METHODS =
+ 'INVITE,ACK,CANCEL,BYE,UPDATE,MESSAGE,OPTIONS,REFER,INFO,NOTIFY,SUBSCRIBE';
+export const ACCEPTED_BODY_TYPES = 'application/sdp, application/dtmf-relay';
+export const MAX_FORWARDS = 69;
+export const SESSION_EXPIRES = 90;
+export const MIN_SESSION_EXPIRES = 60;
+export const CONNECTION_RECOVERY_MAX_INTERVAL = 30;
+export const CONNECTION_RECOVERY_MIN_INTERVAL = 2;
diff --git a/src/Constants.js b/src/Constants.js
index bb10550..0212f56 100644
--- a/src/Constants.js
+++ b/src/Constants.js
@@ -1,159 +1,160 @@
const pkg = require('../package.json');
module.exports = {
- USER_AGENT : `${pkg.title} ${pkg.version}`,
+ USER_AGENT: `${pkg.title} ${pkg.version}`,
- // SIP scheme.
- SIP : 'sip',
- SIPS : 'sips',
+ // SIP scheme.
+ SIP: 'sip',
+ SIPS: 'sips',
- // End and Failure causes.
- causes : {
- // Generic error causes.
- CONNECTION_ERROR : 'Connection Error',
- REQUEST_TIMEOUT : 'Request Timeout',
- SIP_FAILURE_CODE : 'SIP Failure Code',
- INTERNAL_ERROR : 'Internal Error',
+ // End and Failure causes.
+ causes: {
+ // Generic error causes.
+ CONNECTION_ERROR: 'Connection Error',
+ REQUEST_TIMEOUT: 'Request Timeout',
+ SIP_FAILURE_CODE: 'SIP Failure Code',
+ INTERNAL_ERROR: 'Internal Error',
- // SIP error causes.
- BUSY : 'Busy',
- REJECTED : 'Rejected',
- REDIRECTED : 'Redirected',
- UNAVAILABLE : 'Unavailable',
- NOT_FOUND : 'Not Found',
- ADDRESS_INCOMPLETE : 'Address Incomplete',
- INCOMPATIBLE_SDP : 'Incompatible SDP',
- MISSING_SDP : 'Missing SDP',
- AUTHENTICATION_ERROR : 'Authentication Error',
+ // SIP error causes.
+ BUSY: 'Busy',
+ REJECTED: 'Rejected',
+ REDIRECTED: 'Redirected',
+ UNAVAILABLE: 'Unavailable',
+ NOT_FOUND: 'Not Found',
+ ADDRESS_INCOMPLETE: 'Address Incomplete',
+ INCOMPATIBLE_SDP: 'Incompatible SDP',
+ MISSING_SDP: 'Missing SDP',
+ AUTHENTICATION_ERROR: 'Authentication Error',
- // Session error causes.
- BYE : 'Terminated',
- WEBRTC_ERROR : 'WebRTC Error',
- CANCELED : 'Canceled',
- NO_ANSWER : 'No Answer',
- EXPIRES : 'Expires',
- NO_ACK : 'No ACK',
- DIALOG_ERROR : 'Dialog Error',
- USER_DENIED_MEDIA_ACCESS : 'User Denied Media Access',
- BAD_MEDIA_DESCRIPTION : 'Bad Media Description',
- RTP_TIMEOUT : 'RTP Timeout'
- },
+ // Session error causes.
+ BYE: 'Terminated',
+ WEBRTC_ERROR: 'WebRTC Error',
+ CANCELED: 'Canceled',
+ NO_ANSWER: 'No Answer',
+ EXPIRES: 'Expires',
+ NO_ACK: 'No ACK',
+ DIALOG_ERROR: 'Dialog Error',
+ USER_DENIED_MEDIA_ACCESS: 'User Denied Media Access',
+ BAD_MEDIA_DESCRIPTION: 'Bad Media Description',
+ RTP_TIMEOUT: 'RTP Timeout',
+ },
- SIP_ERROR_CAUSES : {
- REDIRECTED : [ 300, 301, 302, 305, 380 ],
- BUSY : [ 486, 600 ],
- REJECTED : [ 403, 603 ],
- NOT_FOUND : [ 404, 604 ],
- UNAVAILABLE : [ 480, 410, 408, 430 ],
- ADDRESS_INCOMPLETE : [ 484, 424 ],
- INCOMPATIBLE_SDP : [ 488, 606 ],
- AUTHENTICATION_ERROR : [ 401, 407 ]
- },
+ SIP_ERROR_CAUSES: {
+ REDIRECTED: [300, 301, 302, 305, 380],
+ BUSY: [486, 600],
+ REJECTED: [403, 603],
+ NOT_FOUND: [404, 604],
+ UNAVAILABLE: [480, 410, 408, 430],
+ ADDRESS_INCOMPLETE: [484, 424],
+ INCOMPATIBLE_SDP: [488, 606],
+ AUTHENTICATION_ERROR: [401, 407],
+ },
- // SIP Methods.
- ACK : 'ACK',
- BYE : 'BYE',
- CANCEL : 'CANCEL',
- INFO : 'INFO',
- INVITE : 'INVITE',
- MESSAGE : 'MESSAGE',
- NOTIFY : 'NOTIFY',
- OPTIONS : 'OPTIONS',
- REGISTER : 'REGISTER',
- REFER : 'REFER',
- UPDATE : 'UPDATE',
- SUBSCRIBE : 'SUBSCRIBE',
+ // SIP Methods.
+ ACK: 'ACK',
+ BYE: 'BYE',
+ CANCEL: 'CANCEL',
+ INFO: 'INFO',
+ INVITE: 'INVITE',
+ MESSAGE: 'MESSAGE',
+ NOTIFY: 'NOTIFY',
+ OPTIONS: 'OPTIONS',
+ REGISTER: 'REGISTER',
+ REFER: 'REFER',
+ UPDATE: 'UPDATE',
+ SUBSCRIBE: 'SUBSCRIBE',
- // DTMF transport methods.
- DTMF_TRANSPORT : {
- INFO : 'INFO',
- RFC2833 : 'RFC2833'
- },
+ // DTMF transport methods.
+ DTMF_TRANSPORT: {
+ INFO: 'INFO',
+ RFC2833: 'RFC2833',
+ },
- /* SIP Response Reasons
- * DOC: https://www.iana.org/assignments/sip-parameters
- * Copied from https://github.com/versatica/OverSIP/blob/master/lib/oversip/sip/constants.rb#L7
- */
- REASON_PHRASE : {
- 100 : 'Trying',
- 180 : 'Ringing',
- 181 : 'Call Is Being Forwarded',
- 182 : 'Queued',
- 183 : 'Session Progress',
- 199 : 'Early Dialog Terminated', // draft-ietf-sipcore-199
- 200 : 'OK',
- 202 : 'Accepted', // RFC 3265
- 204 : 'No Notification', // RFC 5839
- 300 : 'Multiple Choices',
- 301 : 'Moved Permanently',
- 302 : 'Moved Temporarily',
- 305 : 'Use Proxy',
- 380 : 'Alternative Service',
- 400 : 'Bad Request',
- 401 : 'Unauthorized',
- 402 : 'Payment Required',
- 403 : 'Forbidden',
- 404 : 'Not Found',
- 405 : 'Method Not Allowed',
- 406 : 'Not Acceptable',
- 407 : 'Proxy Authentication Required',
- 408 : 'Request Timeout',
- 410 : 'Gone',
- 412 : 'Conditional Request Failed', // RFC 3903
- 413 : 'Request Entity Too Large',
- 414 : 'Request-URI Too Long',
- 415 : 'Unsupported Media Type',
- 416 : 'Unsupported URI Scheme',
- 417 : 'Unknown Resource-Priority', // RFC 4412
- 420 : 'Bad Extension',
- 421 : 'Extension Required',
- 422 : 'Session Interval Too Small', // RFC 4028
- 423 : 'Interval Too Brief',
- 424 : 'Bad Location Information', // RFC 6442
- 428 : 'Use Identity Header', // RFC 4474
- 429 : 'Provide Referrer Identity', // RFC 3892
- 430 : 'Flow Failed', // RFC 5626
- 433 : 'Anonymity Disallowed', // RFC 5079
- 436 : 'Bad Identity-Info', // RFC 4474
- 437 : 'Unsupported Certificate', // RFC 4744
- 438 : 'Invalid Identity Header', // RFC 4744
- 439 : 'First Hop Lacks Outbound Support', // RFC 5626
- 440 : 'Max-Breadth Exceeded', // RFC 5393
- 469 : 'Bad Info Package', // draft-ietf-sipcore-info-events
- 470 : 'Consent Needed', // RFC 5360
- 478 : 'Unresolvable Destination', // Custom code copied from Kamailio.
- 480 : 'Temporarily Unavailable',
- 481 : 'Call/Transaction Does Not Exist',
- 482 : 'Loop Detected',
- 483 : 'Too Many Hops',
- 484 : 'Address Incomplete',
- 485 : 'Ambiguous',
- 486 : 'Busy Here',
- 487 : 'Request Terminated',
- 488 : 'Not Acceptable Here',
- 489 : 'Bad Event', // RFC 3265
- 491 : 'Request Pending',
- 493 : 'Undecipherable',
- 494 : 'Security Agreement Required', // RFC 3329
- 500 : 'JsSIP Internal Error',
- 501 : 'Not Implemented',
- 502 : 'Bad Gateway',
- 503 : 'Service Unavailable',
- 504 : 'Server Time-out',
- 505 : 'Version Not Supported',
- 513 : 'Message Too Large',
- 580 : 'Precondition Failure', // RFC 3312
- 600 : 'Busy Everywhere',
- 603 : 'Decline',
- 604 : 'Does Not Exist Anywhere',
- 606 : 'Not Acceptable'
- },
+ /* SIP Response Reasons
+ * DOC: https://www.iana.org/assignments/sip-parameters
+ * Copied from https://github.com/versatica/OverSIP/blob/master/lib/oversip/sip/constants.rb#L7
+ */
+ REASON_PHRASE: {
+ 100: 'Trying',
+ 180: 'Ringing',
+ 181: 'Call Is Being Forwarded',
+ 182: 'Queued',
+ 183: 'Session Progress',
+ 199: 'Early Dialog Terminated', // draft-ietf-sipcore-199
+ 200: 'OK',
+ 202: 'Accepted', // RFC 3265
+ 204: 'No Notification', // RFC 5839
+ 300: 'Multiple Choices',
+ 301: 'Moved Permanently',
+ 302: 'Moved Temporarily',
+ 305: 'Use Proxy',
+ 380: 'Alternative Service',
+ 400: 'Bad Request',
+ 401: 'Unauthorized',
+ 402: 'Payment Required',
+ 403: 'Forbidden',
+ 404: 'Not Found',
+ 405: 'Method Not Allowed',
+ 406: 'Not Acceptable',
+ 407: 'Proxy Authentication Required',
+ 408: 'Request Timeout',
+ 410: 'Gone',
+ 412: 'Conditional Request Failed', // RFC 3903
+ 413: 'Request Entity Too Large',
+ 414: 'Request-URI Too Long',
+ 415: 'Unsupported Media Type',
+ 416: 'Unsupported URI Scheme',
+ 417: 'Unknown Resource-Priority', // RFC 4412
+ 420: 'Bad Extension',
+ 421: 'Extension Required',
+ 422: 'Session Interval Too Small', // RFC 4028
+ 423: 'Interval Too Brief',
+ 424: 'Bad Location Information', // RFC 6442
+ 428: 'Use Identity Header', // RFC 4474
+ 429: 'Provide Referrer Identity', // RFC 3892
+ 430: 'Flow Failed', // RFC 5626
+ 433: 'Anonymity Disallowed', // RFC 5079
+ 436: 'Bad Identity-Info', // RFC 4474
+ 437: 'Unsupported Certificate', // RFC 4744
+ 438: 'Invalid Identity Header', // RFC 4744
+ 439: 'First Hop Lacks Outbound Support', // RFC 5626
+ 440: 'Max-Breadth Exceeded', // RFC 5393
+ 469: 'Bad Info Package', // draft-ietf-sipcore-info-events
+ 470: 'Consent Needed', // RFC 5360
+ 478: 'Unresolvable Destination', // Custom code copied from Kamailio.
+ 480: 'Temporarily Unavailable',
+ 481: 'Call/Transaction Does Not Exist',
+ 482: 'Loop Detected',
+ 483: 'Too Many Hops',
+ 484: 'Address Incomplete',
+ 485: 'Ambiguous',
+ 486: 'Busy Here',
+ 487: 'Request Terminated',
+ 488: 'Not Acceptable Here',
+ 489: 'Bad Event', // RFC 3265
+ 491: 'Request Pending',
+ 493: 'Undecipherable',
+ 494: 'Security Agreement Required', // RFC 3329
+ 500: 'JsSIP Internal Error',
+ 501: 'Not Implemented',
+ 502: 'Bad Gateway',
+ 503: 'Service Unavailable',
+ 504: 'Server Time-out',
+ 505: 'Version Not Supported',
+ 513: 'Message Too Large',
+ 580: 'Precondition Failure', // RFC 3312
+ 600: 'Busy Everywhere',
+ 603: 'Decline',
+ 604: 'Does Not Exist Anywhere',
+ 606: 'Not Acceptable',
+ },
- ALLOWED_METHODS : 'INVITE,ACK,CANCEL,BYE,UPDATE,MESSAGE,OPTIONS,REFER,INFO,NOTIFY,SUBSCRIBE',
- ACCEPTED_BODY_TYPES : 'application/sdp, application/dtmf-relay',
- MAX_FORWARDS : 69,
- SESSION_EXPIRES : 90,
- MIN_SESSION_EXPIRES : 60,
- CONNECTION_RECOVERY_MAX_INTERVAL : 30,
- CONNECTION_RECOVERY_MIN_INTERVAL : 2
+ ALLOWED_METHODS:
+ 'INVITE,ACK,CANCEL,BYE,UPDATE,MESSAGE,OPTIONS,REFER,INFO,NOTIFY,SUBSCRIBE',
+ ACCEPTED_BODY_TYPES: 'application/sdp, application/dtmf-relay',
+ MAX_FORWARDS: 69,
+ SESSION_EXPIRES: 90,
+ MIN_SESSION_EXPIRES: 60,
+ CONNECTION_RECOVERY_MAX_INTERVAL: 30,
+ CONNECTION_RECOVERY_MIN_INTERVAL: 2,
};
diff --git a/src/Dialog.js b/src/Dialog.js
index 8ec47a6..c8f68cc 100644
--- a/src/Dialog.js
+++ b/src/Dialog.js
@@ -8,329 +8,300 @@ const Utils = require('./Utils');
const logger = new Logger('Dialog');
const C = {
- // Dialog states.
- STATUS_EARLY : 1,
- STATUS_CONFIRMED : 2,
- STATUS_TERMINATED : 3
+ // Dialog states.
+ STATUS_EARLY: 1,
+ STATUS_CONFIRMED: 2,
+ STATUS_TERMINATED: 3,
};
// RFC 3261 12.1.
-module.exports = class Dialog
-{
- // Expose C object.
- static get C()
- {
- return C;
- }
-
- constructor(owner, message, type, state = C.STATUS_CONFIRMED)
- {
- this._owner = owner;
- this._ua = owner._ua;
-
- this._uac_pending_reply = false;
- this._uas_pending_reply = false;
-
- if (!message.hasHeader('contact'))
- {
- return {
- error : 'unable to create a Dialog without Contact header field'
- };
- }
-
- if (message instanceof SIPMessage.IncomingResponse)
- {
- state = (message.status_code < 200) ? C.STATUS_EARLY : C.STATUS_CONFIRMED;
- }
-
- const contact = message.parseHeader('contact');
-
- // RFC 3261 12.1.1.
- if (type === 'UAS')
- {
- this._id = {
- call_id : message.call_id,
- local_tag : message.to_tag,
- remote_tag : message.from_tag,
- toString()
- {
- return this.call_id + this.local_tag + this.remote_tag;
- }
- };
- this._state = state;
- this._remote_seqnum = message.cseq;
- this._local_uri = message.parseHeader('to').uri;
- this._remote_uri = message.parseHeader('from').uri;
- this._remote_target = contact.uri;
- this._route_set = message.getHeaders('record-route');
- this.incoming_ack_seqnum = message.cseq;
- this.outgoing_ack_seqnum = null;
- }
- // RFC 3261 12.1.2.
- else if (type === 'UAC')
- {
- this._id = {
- call_id : message.call_id,
- local_tag : message.from_tag,
- remote_tag : message.to_tag,
- toString()
- {
- return this.call_id + this.local_tag + this.remote_tag;
- }
- };
- this._state = state;
- this._local_seqnum = message.cseq;
- this._local_uri = message.parseHeader('from').uri;
- this._remote_uri = message.parseHeader('to').uri;
- this._remote_target = contact.uri;
- this._route_set = message.getHeaders('record-route').reverse();
- this.incoming_ack_seqnum = null;
- this.outgoing_ack_seqnum = this._local_seqnum;
-
- }
-
- this._ua.newDialog(this);
- logger.debug(`new ${type} dialog created with status ${this._state === C.STATUS_EARLY ? 'EARLY': 'CONFIRMED'}`);
- }
-
- get id()
- {
- return this._id;
- }
-
- get local_seqnum()
- {
- return this._local_seqnum;
- }
-
- set local_seqnum(num)
- {
- this._local_seqnum = num;
- }
-
- get owner()
- {
- return this._owner;
- }
-
- get uac_pending_reply()
- {
- return this._uac_pending_reply;
- }
-
- set uac_pending_reply(pending)
- {
- this._uac_pending_reply = pending;
- }
-
- get uas_pending_reply()
- {
- return this._uas_pending_reply;
- }
-
- isTerminated()
- {
- return this._status === C.STATUS_TERMINATED;
- }
-
- update(message, type)
- {
- this._state = C.STATUS_CONFIRMED;
-
- logger.debug(`dialog ${this._id.toString()} changed to CONFIRMED state`);
-
- if (type === 'UAC')
- {
- // RFC 3261 13.2.2.4.
- this._route_set = message.getHeaders('record-route').reverse();
- }
- }
-
- terminate()
- {
- logger.debug(`dialog ${this._id.toString()} deleted`);
-
- this._ua.destroyDialog(this);
- this._state = C.STATUS_TERMINATED;
- }
-
- sendRequest(method, options = {})
- {
- const extraHeaders = Utils.cloneArray(options.extraHeaders);
- const eventHandlers = Utils.cloneObject(options.eventHandlers);
- const body = options.body || null;
- const request = this._createRequest(method, extraHeaders, body);
-
- // Increase the local CSeq on authentication.
- eventHandlers.onAuthenticated = () =>
- {
- this._local_seqnum += 1;
-
- // In case of re-INVITE store outgoing ack_seqnum for its CANCEL or ACK.
- if (request.method === JsSIP_C.INVITE)
- {
- this._outgoing_ack_seqnum = this._local_seqnum;
- }
- };
-
- const request_sender = new Dialog_RequestSender(this, request, eventHandlers);
-
- request_sender.send();
-
- // Return the instance of OutgoingRequest.
- return request;
- }
-
- receiveRequest(request)
- {
- // Check in-dialog request.
- if (!this._checkInDialogRequest(request))
- {
- return;
- }
-
- // ACK received. Cleanup this._ack_seqnum.
- if (request.method === JsSIP_C.ACK && this.incoming_ack_seqnum !== null)
- {
- this.incoming_ack_seqnum = null;
- }
- // INVITE received. Set this._ack_seqnum.
- else if (request.method === JsSIP_C.INVITE)
- {
- this.incoming_ack_seqnum = request.cseq;
- }
-
- this._owner.receiveRequest(request);
- }
-
- // RFC 3261 12.2.1.1.
- _createRequest(method, extraHeaders, body)
- {
- extraHeaders = Utils.cloneArray(extraHeaders);
-
- if (!this._local_seqnum) { this._local_seqnum = Math.floor(Math.random() * 10000); }
-
- // CANCEL and ACK must use the same sequence number as the INVITE.
- const cseq = (method === JsSIP_C.CANCEL || method === JsSIP_C.ACK) ?
- this.outgoing_ack_seqnum :
- this._local_seqnum += 1;
-
- // In case of re-INVITE store ack_seqnum for future CANCEL or ACK.
- if (method === JsSIP_C.INVITE)
- {
- this.outgoing_ack_seqnum = cseq;
- }
-
- const request = new SIPMessage.OutgoingRequest(
- method,
- this._remote_target,
- this._ua, {
- 'cseq' : cseq,
- 'call_id' : this._id.call_id,
- 'from_uri' : this._local_uri,
- 'from_tag' : this._id.local_tag,
- 'to_uri' : this._remote_uri,
- 'to_tag' : this._id.remote_tag,
- 'route_set' : this._route_set
- }, extraHeaders, body);
-
- return request;
- }
-
- // RFC 3261 12.2.2.
- _checkInDialogRequest(request)
- {
-
- if (!this._remote_seqnum)
- {
- this._remote_seqnum = request.cseq;
- }
- else if (request.cseq < this._remote_seqnum)
- {
- if (request.method === JsSIP_C.ACK)
- {
- // We are not expecting any ACK with lower seqnum than the current one.
- // Or this is not the ACK we are waiting for.
- if (this.incoming_ack_seqnum === null ||
- request.cseq !== this.incoming_ack_seqnum)
- {
- return false;
- }
- }
- else
- {
- request.reply(500);
-
- return false;
- }
- }
- else if (request.cseq > this._remote_seqnum)
- {
- this._remote_seqnum = request.cseq;
- }
-
- // RFC3261 14.2 Modifying an Existing Session -UAS BEHAVIOR-.
- if (request.method === JsSIP_C.INVITE ||
- (request.method === JsSIP_C.UPDATE && request.body))
- {
- if (this._uac_pending_reply === true)
- {
- request.reply(491);
- }
- else if (this._uas_pending_reply === true)
- {
- const retryAfter = (Math.random() * 10 | 0) + 1;
-
- request.reply(500, null, [ `Retry-After:${retryAfter}` ]);
-
- return false;
- }
- else
- {
- this._uas_pending_reply = true;
-
- const stateChanged = () =>
- {
- if (request.server_transaction.state === Transactions.C.STATUS_ACCEPTED ||
- request.server_transaction.state === Transactions.C.STATUS_COMPLETED ||
- request.server_transaction.state === Transactions.C.STATUS_TERMINATED)
- {
-
- request.server_transaction.removeListener('stateChanged', stateChanged);
- this._uas_pending_reply = false;
- }
- };
-
- request.server_transaction.on('stateChanged', stateChanged);
- }
-
- // RFC3261 12.2.2 Replace the dialog`s remote target URI if the request is accepted.
- if (request.hasHeader('contact'))
- {
- request.server_transaction.on('stateChanged', () =>
- {
- if (request.server_transaction.state === Transactions.C.STATUS_ACCEPTED)
- {
- this._remote_target = request.parseHeader('contact').uri;
- }
- });
- }
- }
- else if (request.method === JsSIP_C.NOTIFY)
- {
- // RFC6665 3.2 Replace the dialog`s remote target URI if the request is accepted.
- if (request.hasHeader('contact'))
- {
- request.server_transaction.on('stateChanged', () =>
- {
- if (request.server_transaction.state === Transactions.C.STATUS_COMPLETED)
- {
- this._remote_target = request.parseHeader('contact').uri;
- }
- });
- }
- }
-
- return true;
- }
+module.exports = class Dialog {
+ // Expose C object.
+ static get C() {
+ return C;
+ }
+
+ constructor(owner, message, type, state = C.STATUS_CONFIRMED) {
+ this._owner = owner;
+ this._ua = owner._ua;
+
+ this._uac_pending_reply = false;
+ this._uas_pending_reply = false;
+
+ if (!message.hasHeader('contact')) {
+ return {
+ error: 'unable to create a Dialog without Contact header field',
+ };
+ }
+
+ if (message instanceof SIPMessage.IncomingResponse) {
+ state = message.status_code < 200 ? C.STATUS_EARLY : C.STATUS_CONFIRMED;
+ }
+
+ const contact = message.parseHeader('contact');
+
+ // RFC 3261 12.1.1.
+ if (type === 'UAS') {
+ this._id = {
+ call_id: message.call_id,
+ local_tag: message.to_tag,
+ remote_tag: message.from_tag,
+ toString() {
+ return this.call_id + this.local_tag + this.remote_tag;
+ },
+ };
+ this._state = state;
+ this._remote_seqnum = message.cseq;
+ this._local_uri = message.parseHeader('to').uri;
+ this._remote_uri = message.parseHeader('from').uri;
+ this._remote_target = contact.uri;
+ this._route_set = message.getHeaders('record-route');
+ this.incoming_ack_seqnum = message.cseq;
+ this.outgoing_ack_seqnum = null;
+ }
+ // RFC 3261 12.1.2.
+ else if (type === 'UAC') {
+ this._id = {
+ call_id: message.call_id,
+ local_tag: message.from_tag,
+ remote_tag: message.to_tag,
+ toString() {
+ return this.call_id + this.local_tag + this.remote_tag;
+ },
+ };
+ this._state = state;
+ this._local_seqnum = message.cseq;
+ this._local_uri = message.parseHeader('from').uri;
+ this._remote_uri = message.parseHeader('to').uri;
+ this._remote_target = contact.uri;
+ this._route_set = message.getHeaders('record-route').reverse();
+ this.incoming_ack_seqnum = null;
+ this.outgoing_ack_seqnum = this._local_seqnum;
+ }
+
+ this._ua.newDialog(this);
+ logger.debug(
+ `new ${type} dialog created with status ${this._state === C.STATUS_EARLY ? 'EARLY' : 'CONFIRMED'}`
+ );
+ }
+
+ get id() {
+ return this._id;
+ }
+
+ get local_seqnum() {
+ return this._local_seqnum;
+ }
+
+ set local_seqnum(num) {
+ this._local_seqnum = num;
+ }
+
+ get owner() {
+ return this._owner;
+ }
+
+ get uac_pending_reply() {
+ return this._uac_pending_reply;
+ }
+
+ set uac_pending_reply(pending) {
+ this._uac_pending_reply = pending;
+ }
+
+ get uas_pending_reply() {
+ return this._uas_pending_reply;
+ }
+
+ isTerminated() {
+ return this._status === C.STATUS_TERMINATED;
+ }
+
+ update(message, type) {
+ this._state = C.STATUS_CONFIRMED;
+
+ logger.debug(`dialog ${this._id.toString()} changed to CONFIRMED state`);
+
+ if (type === 'UAC') {
+ // RFC 3261 13.2.2.4.
+ this._route_set = message.getHeaders('record-route').reverse();
+ }
+ }
+
+ terminate() {
+ logger.debug(`dialog ${this._id.toString()} deleted`);
+
+ this._ua.destroyDialog(this);
+ this._state = C.STATUS_TERMINATED;
+ }
+
+ sendRequest(method, options = {}) {
+ const extraHeaders = Utils.cloneArray(options.extraHeaders);
+ const eventHandlers = Utils.cloneObject(options.eventHandlers);
+ const body = options.body || null;
+ const request = this._createRequest(method, extraHeaders, body);
+
+ // Increase the local CSeq on authentication.
+ eventHandlers.onAuthenticated = () => {
+ this._local_seqnum += 1;
+
+ // In case of re-INVITE store outgoing ack_seqnum for its CANCEL or ACK.
+ if (request.method === JsSIP_C.INVITE) {
+ this._outgoing_ack_seqnum = this._local_seqnum;
+ }
+ };
+
+ const request_sender = new Dialog_RequestSender(
+ this,
+ request,
+ eventHandlers
+ );
+
+ request_sender.send();
+
+ // Return the instance of OutgoingRequest.
+ return request;
+ }
+
+ receiveRequest(request) {
+ // Check in-dialog request.
+ if (!this._checkInDialogRequest(request)) {
+ return;
+ }
+
+ // ACK received. Cleanup this._ack_seqnum.
+ if (request.method === JsSIP_C.ACK && this.incoming_ack_seqnum !== null) {
+ this.incoming_ack_seqnum = null;
+ }
+ // INVITE received. Set this._ack_seqnum.
+ else if (request.method === JsSIP_C.INVITE) {
+ this.incoming_ack_seqnum = request.cseq;
+ }
+
+ this._owner.receiveRequest(request);
+ }
+
+ // RFC 3261 12.2.1.1.
+ _createRequest(method, extraHeaders, body) {
+ extraHeaders = Utils.cloneArray(extraHeaders);
+
+ if (!this._local_seqnum) {
+ this._local_seqnum = Math.floor(Math.random() * 10000);
+ }
+
+ // CANCEL and ACK must use the same sequence number as the INVITE.
+ const cseq =
+ method === JsSIP_C.CANCEL || method === JsSIP_C.ACK
+ ? this.outgoing_ack_seqnum
+ : (this._local_seqnum += 1);
+
+ // In case of re-INVITE store ack_seqnum for future CANCEL or ACK.
+ if (method === JsSIP_C.INVITE) {
+ this.outgoing_ack_seqnum = cseq;
+ }
+
+ const request = new SIPMessage.OutgoingRequest(
+ method,
+ this._remote_target,
+ this._ua,
+ {
+ cseq: cseq,
+ call_id: this._id.call_id,
+ from_uri: this._local_uri,
+ from_tag: this._id.local_tag,
+ to_uri: this._remote_uri,
+ to_tag: this._id.remote_tag,
+ route_set: this._route_set,
+ },
+ extraHeaders,
+ body
+ );
+
+ return request;
+ }
+
+ // RFC 3261 12.2.2.
+ _checkInDialogRequest(request) {
+ if (!this._remote_seqnum) {
+ this._remote_seqnum = request.cseq;
+ } else if (request.cseq < this._remote_seqnum) {
+ if (request.method === JsSIP_C.ACK) {
+ // We are not expecting any ACK with lower seqnum than the current one.
+ // Or this is not the ACK we are waiting for.
+ if (
+ this.incoming_ack_seqnum === null ||
+ request.cseq !== this.incoming_ack_seqnum
+ ) {
+ return false;
+ }
+ } else {
+ request.reply(500);
+
+ return false;
+ }
+ } else if (request.cseq > this._remote_seqnum) {
+ this._remote_seqnum = request.cseq;
+ }
+
+ // RFC3261 14.2 Modifying an Existing Session -UAS BEHAVIOR-.
+ if (
+ request.method === JsSIP_C.INVITE ||
+ (request.method === JsSIP_C.UPDATE && request.body)
+ ) {
+ if (this._uac_pending_reply === true) {
+ request.reply(491);
+ } else if (this._uas_pending_reply === true) {
+ const retryAfter = ((Math.random() * 10) | 0) + 1;
+
+ request.reply(500, null, [`Retry-After:${retryAfter}`]);
+
+ return false;
+ } else {
+ this._uas_pending_reply = true;
+
+ const stateChanged = () => {
+ if (
+ request.server_transaction.state ===
+ Transactions.C.STATUS_ACCEPTED ||
+ request.server_transaction.state ===
+ Transactions.C.STATUS_COMPLETED ||
+ request.server_transaction.state ===
+ Transactions.C.STATUS_TERMINATED
+ ) {
+ request.server_transaction.removeListener(
+ 'stateChanged',
+ stateChanged
+ );
+ this._uas_pending_reply = false;
+ }
+ };
+
+ request.server_transaction.on('stateChanged', stateChanged);
+ }
+
+ // RFC3261 12.2.2 Replace the dialog`s remote target URI if the request is accepted.
+ if (request.hasHeader('contact')) {
+ request.server_transaction.on('stateChanged', () => {
+ if (
+ request.server_transaction.state === Transactions.C.STATUS_ACCEPTED
+ ) {
+ this._remote_target = request.parseHeader('contact').uri;
+ }
+ });
+ }
+ } else if (request.method === JsSIP_C.NOTIFY) {
+ // RFC6665 3.2 Replace the dialog`s remote target URI if the request is accepted.
+ if (request.hasHeader('contact')) {
+ request.server_transaction.on('stateChanged', () => {
+ if (
+ request.server_transaction.state === Transactions.C.STATUS_COMPLETED
+ ) {
+ this._remote_target = request.parseHeader('contact').uri;
+ }
+ });
+ }
+ }
+
+ return true;
+ }
};
diff --git a/src/Dialog/RequestSender.js b/src/Dialog/RequestSender.js
index 88d1b7f..adeeb39 100644
--- a/src/Dialog/RequestSender.js
+++ b/src/Dialog/RequestSender.js
@@ -4,123 +4,110 @@ const RequestSender = require('../RequestSender');
// Default event handlers.
const EventHandlers = {
- onRequestTimeout : () => {},
- onTransportError : () => {},
- onSuccessResponse : () => {},
- onErrorResponse : () => {},
- onAuthenticated : () => {},
- onDialogError : () => {}
+ onRequestTimeout: () => {},
+ onTransportError: () => {},
+ onSuccessResponse: () => {},
+ onErrorResponse: () => {},
+ onAuthenticated: () => {},
+ onDialogError: () => {},
};
-module.exports = class DialogRequestSender
-{
- constructor(dialog, request, eventHandlers)
- {
- this._dialog = dialog;
- this._ua = dialog._ua;
- this._request = request;
- this._eventHandlers = eventHandlers;
+module.exports = class DialogRequestSender {
+ constructor(dialog, request, eventHandlers) {
+ this._dialog = dialog;
+ this._ua = dialog._ua;
+ this._request = request;
+ this._eventHandlers = eventHandlers;
- // RFC3261 14.1 Modifying an Existing Session. UAC Behavior.
- this._reattempt = false;
- this._reattemptTimer = null;
+ // RFC3261 14.1 Modifying an Existing Session. UAC Behavior.
+ this._reattempt = false;
+ this._reattemptTimer = null;
- // Define the undefined handlers.
- for (const handler in EventHandlers)
- {
- if (Object.prototype.hasOwnProperty.call(EventHandlers, handler))
- {
- if (!this._eventHandlers[handler])
- {
- this._eventHandlers[handler] = EventHandlers[handler];
- }
- }
- }
- }
+ // Define the undefined handlers.
+ for (const handler in EventHandlers) {
+ if (Object.prototype.hasOwnProperty.call(EventHandlers, handler)) {
+ if (!this._eventHandlers[handler]) {
+ this._eventHandlers[handler] = EventHandlers[handler];
+ }
+ }
+ }
+ }
- get request()
- {
- return this._request;
- }
+ get request() {
+ return this._request;
+ }
- send()
- {
- const request_sender = new RequestSender(this._ua, this._request, {
- onRequestTimeout : () =>
- {
- this._eventHandlers.onRequestTimeout();
- },
- onTransportError : () =>
- {
- this._eventHandlers.onTransportError();
- },
- onAuthenticated : (request) =>
- {
- this._eventHandlers.onAuthenticated(request);
- },
- onReceiveResponse : (response) =>
- {
- this._receiveResponse(response);
- }
- });
+ send() {
+ const request_sender = new RequestSender(this._ua, this._request, {
+ onRequestTimeout: () => {
+ this._eventHandlers.onRequestTimeout();
+ },
+ onTransportError: () => {
+ this._eventHandlers.onTransportError();
+ },
+ onAuthenticated: request => {
+ this._eventHandlers.onAuthenticated(request);
+ },
+ onReceiveResponse: response => {
+ this._receiveResponse(response);
+ },
+ });
- request_sender.send();
+ request_sender.send();
- // RFC3261 14.2 Modifying an Existing Session -UAC BEHAVIOR-.
- if ((this._request.method === JsSIP_C.INVITE ||
- (this._request.method === JsSIP_C.UPDATE && this._request.body)) &&
- request_sender.clientTransaction.state !== Transactions.C.STATUS_TERMINATED)
- {
- this._dialog.uac_pending_reply = true;
+ // RFC3261 14.2 Modifying an Existing Session -UAC BEHAVIOR-.
+ if (
+ (this._request.method === JsSIP_C.INVITE ||
+ (this._request.method === JsSIP_C.UPDATE && this._request.body)) &&
+ request_sender.clientTransaction.state !==
+ Transactions.C.STATUS_TERMINATED
+ ) {
+ this._dialog.uac_pending_reply = true;
- const stateChanged = () =>
- {
- if (request_sender.clientTransaction.state === Transactions.C.STATUS_ACCEPTED ||
- request_sender.clientTransaction.state === Transactions.C.STATUS_COMPLETED ||
- request_sender.clientTransaction.state === Transactions.C.STATUS_TERMINATED)
- {
- request_sender.clientTransaction.removeListener('stateChanged', stateChanged);
- this._dialog.uac_pending_reply = false;
- }
- };
+ const stateChanged = () => {
+ if (
+ request_sender.clientTransaction.state ===
+ Transactions.C.STATUS_ACCEPTED ||
+ request_sender.clientTransaction.state ===
+ Transactions.C.STATUS_COMPLETED ||
+ request_sender.clientTransaction.state ===
+ Transactions.C.STATUS_TERMINATED
+ ) {
+ request_sender.clientTransaction.removeListener(
+ 'stateChanged',
+ stateChanged
+ );
+ this._dialog.uac_pending_reply = false;
+ }
+ };
- request_sender.clientTransaction.on('stateChanged', stateChanged);
- }
- }
+ request_sender.clientTransaction.on('stateChanged', stateChanged);
+ }
+ }
- _receiveResponse(response)
- {
- // RFC3261 12.2.1.2 408 or 481 is received for a request within a dialog.
- if (response.status_code === 408 || response.status_code === 481)
- {
- this._eventHandlers.onDialogError(response);
- }
- else if (response.method === JsSIP_C.INVITE && response.status_code === 491)
- {
- if (this._reattempt)
- {
- this._eventHandlers.onErrorResponse(response);
- }
- else
- {
- this._request.cseq = this._dialog.local_seqnum += 1;
- this._reattemptTimer = setTimeout(() =>
- {
- if (!this._dialog.isTerminated())
- {
- this._reattempt = true;
- this.send();
- }
- }, 1000);
- }
- }
- else if (response.status_code >= 200 && response.status_code < 300)
- {
- this._eventHandlers.onSuccessResponse(response);
- }
- else if (response.status_code >= 300)
- {
- this._eventHandlers.onErrorResponse(response);
- }
- }
+ _receiveResponse(response) {
+ // RFC3261 12.2.1.2 408 or 481 is received for a request within a dialog.
+ if (response.status_code === 408 || response.status_code === 481) {
+ this._eventHandlers.onDialogError(response);
+ } else if (
+ response.method === JsSIP_C.INVITE &&
+ response.status_code === 491
+ ) {
+ if (this._reattempt) {
+ this._eventHandlers.onErrorResponse(response);
+ } else {
+ this._request.cseq = this._dialog.local_seqnum += 1;
+ this._reattemptTimer = setTimeout(() => {
+ if (!this._dialog.isTerminated()) {
+ this._reattempt = true;
+ this.send();
+ }
+ }, 1000);
+ }
+ } else if (response.status_code >= 200 && response.status_code < 300) {
+ this._eventHandlers.onSuccessResponse(response);
+ } else if (response.status_code >= 300) {
+ this._eventHandlers.onErrorResponse(response);
+ }
+ }
};
diff --git a/src/DigestAuthentication.js b/src/DigestAuthentication.js
index 44b439d..4758cad 100644
--- a/src/DigestAuthentication.js
+++ b/src/DigestAuthentication.js
@@ -3,232 +3,225 @@ const Utils = require('./Utils');
const logger = new Logger('DigestAuthentication');
-module.exports = class DigestAuthentication
-{
- constructor(credentials)
- {
- this._credentials = credentials;
- this._cnonce = null;
- this._nc = 0;
- this._ncHex = '00000000';
- this._algorithm = null;
- this._realm = null;
- this._nonce = null;
- this._opaque = null;
- this._stale = null;
- this._qop = null;
- this._method = null;
- this._uri = null;
- this._ha1 = null;
- this._response = null;
- }
-
- get(parameter)
- {
- switch (parameter)
- {
- case 'realm':
- return this._realm;
-
- case 'ha1':
- return this._ha1;
-
- default:
- logger.warn('get() | cannot get "%s" parameter', parameter);
-
- return undefined;
- }
- }
-
- /**
- * Performs Digest authentication given a SIP request and the challenge
- * received in a response to that request.
- * Returns true if auth was successfully generated, false otherwise.
- */
- authenticate({ method, ruri, body }, challenge, cnonce = null /* test interface */)
- {
- this._algorithm = challenge.algorithm;
- this._realm = challenge.realm;
- this._nonce = challenge.nonce;
- this._opaque = challenge.opaque;
- this._stale = challenge.stale;
-
- if (this._algorithm)
- {
- if (this._algorithm !== 'MD5')
- {
- logger.warn('authenticate() | challenge with Digest algorithm different than "MD5", authentication aborted');
-
- return false;
- }
- }
- else
- {
- this._algorithm = 'MD5';
- }
-
- if (!this._nonce)
- {
- logger.warn('authenticate() | challenge without Digest nonce, authentication aborted');
-
- return false;
- }
-
- if (!this._realm)
- {
- logger.warn('authenticate() | challenge without Digest realm, authentication aborted');
-
- return false;
- }
-
- // If no plain SIP password is provided.
- if (!this._credentials.password)
- {
- // If ha1 is not provided we cannot authenticate.
- if (!this._credentials.ha1)
- {
- logger.warn('authenticate() | no plain SIP password nor ha1 provided, authentication aborted');
-
- return false;
- }
-
- // If the realm does not match the stored realm we cannot authenticate.
- if (this._credentials.realm !== this._realm)
- {
- logger.warn('authenticate() | no plain SIP password, and stored `realm` does not match the given `realm`, cannot authenticate [stored:"%s", given:"%s"]', this._credentials.realm, this._realm);
-
- return false;
- }
- }
-
- // 'qop' can contain a list of values (Array). Let's choose just one.
- if (challenge.qop)
- {
- if (challenge.qop.indexOf('auth-int') > -1)
- {
- this._qop = 'auth-int';
- }
- else if (challenge.qop.indexOf('auth') > -1)
- {
- this._qop = 'auth';
- }
- else
- {
- // Otherwise 'qop' is present but does not contain 'auth' or 'auth-int', so abort here.
- logger.warn('authenticate() | challenge without Digest qop different than "auth" or "auth-int", authentication aborted');
-
- return false;
- }
- }
- else
- {
- this._qop = null;
- }
-
- // Fill other attributes.
-
- this._method = method;
- this._uri = ruri;
- this._cnonce = cnonce || Utils.createRandomToken(12);
- this._nc += 1;
- const hex = Number(this._nc).toString(16);
-
- this._ncHex = '00000000'.substr(0, 8-hex.length) + hex;
-
- // Nc-value = 8LHEX. Max value = 'FFFFFFFF'.
- if (this._nc === 4294967296)
- {
- this._nc = 1;
- this._ncHex = '00000001';
- }
-
- // Calculate the Digest "response" value.
-
- // If we have plain SIP password then regenerate ha1.
- if (this._credentials.password)
- {
- // HA1 = MD5(A1) = MD5(username:realm:password).
- this._ha1 = Utils.calculateMD5(`${this._credentials.username}:${this._realm}:${this._credentials.password}`);
- }
- // Otherwise reuse the stored ha1.
- else
- {
- this._ha1 = this._credentials.ha1;
- }
-
- let a2;
- let ha2;
-
- if (this._qop === 'auth')
- {
- // HA2 = MD5(A2) = MD5(method:digestURI).
- a2 = `${this._method}:${this._uri}`;
- ha2 = Utils.calculateMD5(a2);
-
- logger.debug('authenticate() | using qop=auth [a2:"%s"]', a2);
-
- // Response = MD5(HA1:nonce:nonceCount:credentialsNonce:qop:HA2).
- this._response = Utils.calculateMD5(`${this._ha1}:${this._nonce}:${this._ncHex}:${this._cnonce}:auth:${ha2}`);
-
- }
- else if (this._qop === 'auth-int')
- {
- // HA2 = MD5(A2) = MD5(method:digestURI:MD5(entityBody)).
- a2 = `${this._method}:${this._uri}:${Utils.calculateMD5(body ? body : '')}`;
- ha2 = Utils.calculateMD5(a2);
-
- logger.debug('authenticate() | using qop=auth-int [a2:"%s"]', a2);
-
- // Response = MD5(HA1:nonce:nonceCount:credentialsNonce:qop:HA2).
- this._response = Utils.calculateMD5(`${this._ha1}:${this._nonce}:${this._ncHex}:${this._cnonce}:auth-int:${ha2}`);
-
- }
- else if (this._qop === null)
- {
- // HA2 = MD5(A2) = MD5(method:digestURI).
- a2 = `${this._method}:${this._uri}`;
- ha2 = Utils.calculateMD5(a2);
-
- logger.debug('authenticate() | using qop=null [a2:"%s"]', a2);
-
- // Response = MD5(HA1:nonce:HA2).
- this._response = Utils.calculateMD5(`${this._ha1}:${this._nonce}:${ha2}`);
- }
-
- logger.debug('authenticate() | response generated');
-
- return true;
- }
-
- /**
- * Return the Proxy-Authorization or WWW-Authorization header value.
- */
- toString()
- {
- const auth_params = [];
-
- if (!this._response)
- {
- throw new Error('response field does not exist, cannot generate Authorization header');
- }
-
- auth_params.push(`algorithm=${this._algorithm}`);
- auth_params.push(`username="${this._credentials.username}"`);
- auth_params.push(`realm="${this._realm}"`);
- auth_params.push(`nonce="${this._nonce}"`);
- auth_params.push(`uri="${this._uri}"`);
- auth_params.push(`response="${this._response}"`);
- if (this._opaque)
- {
- auth_params.push(`opaque="${this._opaque}"`);
- }
- if (this._qop)
- {
- auth_params.push(`qop=${this._qop}`);
- auth_params.push(`cnonce="${this._cnonce}"`);
- auth_params.push(`nc=${this._ncHex}`);
- }
-
- return `Digest ${auth_params.join(', ')}`;
- }
+module.exports = class DigestAuthentication {
+ constructor(credentials) {
+ this._credentials = credentials;
+ this._cnonce = null;
+ this._nc = 0;
+ this._ncHex = '00000000';
+ this._algorithm = null;
+ this._realm = null;
+ this._nonce = null;
+ this._opaque = null;
+ this._stale = null;
+ this._qop = null;
+ this._method = null;
+ this._uri = null;
+ this._ha1 = null;
+ this._response = null;
+ }
+
+ get(parameter) {
+ switch (parameter) {
+ case 'realm': {
+ return this._realm;
+ }
+
+ case 'ha1': {
+ return this._ha1;
+ }
+
+ default: {
+ logger.warn('get() | cannot get "%s" parameter', parameter);
+
+ return undefined;
+ }
+ }
+ }
+
+ /**
+ * Performs Digest authentication given a SIP request and the challenge
+ * received in a response to that request.
+ * Returns true if auth was successfully generated, false otherwise.
+ */
+ authenticate(
+ { method, ruri, body },
+ challenge,
+ cnonce = null /* test interface */
+ ) {
+ this._algorithm = challenge.algorithm;
+ this._realm = challenge.realm;
+ this._nonce = challenge.nonce;
+ this._opaque = challenge.opaque;
+ this._stale = challenge.stale;
+
+ if (this._algorithm) {
+ if (this._algorithm !== 'MD5') {
+ logger.warn(
+ 'authenticate() | challenge with Digest algorithm different than "MD5", authentication aborted'
+ );
+
+ return false;
+ }
+ } else {
+ this._algorithm = 'MD5';
+ }
+
+ if (!this._nonce) {
+ logger.warn(
+ 'authenticate() | challenge without Digest nonce, authentication aborted'
+ );
+
+ return false;
+ }
+
+ if (!this._realm) {
+ logger.warn(
+ 'authenticate() | challenge without Digest realm, authentication aborted'
+ );
+
+ return false;
+ }
+
+ // If no plain SIP password is provided.
+ if (!this._credentials.password) {
+ // If ha1 is not provided we cannot authenticate.
+ if (!this._credentials.ha1) {
+ logger.warn(
+ 'authenticate() | no plain SIP password nor ha1 provided, authentication aborted'
+ );
+
+ return false;
+ }
+
+ // If the realm does not match the stored realm we cannot authenticate.
+ if (this._credentials.realm !== this._realm) {
+ logger.warn(
+ 'authenticate() | no plain SIP password, and stored `realm` does not match the given `realm`, cannot authenticate [stored:"%s", given:"%s"]',
+ this._credentials.realm,
+ this._realm
+ );
+
+ return false;
+ }
+ }
+
+ // 'qop' can contain a list of values (Array). Let's choose just one.
+ if (challenge.qop) {
+ if (challenge.qop.indexOf('auth-int') > -1) {
+ this._qop = 'auth-int';
+ } else if (challenge.qop.indexOf('auth') > -1) {
+ this._qop = 'auth';
+ } else {
+ // Otherwise 'qop' is present but does not contain 'auth' or 'auth-int', so abort here.
+ logger.warn(
+ 'authenticate() | challenge without Digest qop different than "auth" or "auth-int", authentication aborted'
+ );
+
+ return false;
+ }
+ } else {
+ this._qop = null;
+ }
+
+ // Fill other attributes.
+
+ this._method = method;
+ this._uri = ruri;
+ this._cnonce = cnonce || Utils.createRandomToken(12);
+ this._nc += 1;
+ const hex = Number(this._nc).toString(16);
+
+ this._ncHex = '00000000'.substr(0, 8 - hex.length) + hex;
+
+ // Nc-value = 8LHEX. Max value = 'FFFFFFFF'.
+ if (this._nc === 4294967296) {
+ this._nc = 1;
+ this._ncHex = '00000001';
+ }
+
+ // Calculate the Digest "response" value.
+
+ // If we have plain SIP password then regenerate ha1.
+ if (this._credentials.password) {
+ // HA1 = MD5(A1) = MD5(username:realm:password).
+ this._ha1 = Utils.calculateMD5(
+ `${this._credentials.username}:${this._realm}:${this._credentials.password}`
+ );
+ }
+ // Otherwise reuse the stored ha1.
+ else {
+ this._ha1 = this._credentials.ha1;
+ }
+
+ let a2;
+ let ha2;
+
+ if (this._qop === 'auth') {
+ // HA2 = MD5(A2) = MD5(method:digestURI).
+ a2 = `${this._method}:${this._uri}`;
+ ha2 = Utils.calculateMD5(a2);
+
+ logger.debug('authenticate() | using qop=auth [a2:"%s"]', a2);
+
+ // Response = MD5(HA1:nonce:nonceCount:credentialsNonce:qop:HA2).
+ this._response = Utils.calculateMD5(
+ `${this._ha1}:${this._nonce}:${this._ncHex}:${this._cnonce}:auth:${ha2}`
+ );
+ } else if (this._qop === 'auth-int') {
+ // HA2 = MD5(A2) = MD5(method:digestURI:MD5(entityBody)).
+ a2 = `${this._method}:${this._uri}:${Utils.calculateMD5(body ? body : '')}`;
+ ha2 = Utils.calculateMD5(a2);
+
+ logger.debug('authenticate() | using qop=auth-int [a2:"%s"]', a2);
+
+ // Response = MD5(HA1:nonce:nonceCount:credentialsNonce:qop:HA2).
+ this._response = Utils.calculateMD5(
+ `${this._ha1}:${this._nonce}:${this._ncHex}:${this._cnonce}:auth-int:${ha2}`
+ );
+ } else if (this._qop === null) {
+ // HA2 = MD5(A2) = MD5(method:digestURI).
+ a2 = `${this._method}:${this._uri}`;
+ ha2 = Utils.calculateMD5(a2);
+
+ logger.debug('authenticate() | using qop=null [a2:"%s"]', a2);
+
+ // Response = MD5(HA1:nonce:HA2).
+ this._response = Utils.calculateMD5(`${this._ha1}:${this._nonce}:${ha2}`);
+ }
+
+ logger.debug('authenticate() | response generated');
+
+ return true;
+ }
+
+ /**
+ * Return the Proxy-Authorization or WWW-Authorization header value.
+ */
+ toString() {
+ const auth_params = [];
+
+ if (!this._response) {
+ throw new Error(
+ 'response field does not exist, cannot generate Authorization header'
+ );
+ }
+
+ auth_params.push(`algorithm=${this._algorithm}`);
+ auth_params.push(`username="${this._credentials.username}"`);
+ auth_params.push(`realm="${this._realm}"`);
+ auth_params.push(`nonce="${this._nonce}"`);
+ auth_params.push(`uri="${this._uri}"`);
+ auth_params.push(`response="${this._response}"`);
+ if (this._opaque) {
+ auth_params.push(`opaque="${this._opaque}"`);
+ }
+ if (this._qop) {
+ auth_params.push(`qop=${this._qop}`);
+ auth_params.push(`cnonce="${this._cnonce}"`);
+ auth_params.push(`nc=${this._ncHex}`);
+ }
+
+ return `Digest ${auth_params.join(', ')}`;
+ }
};
diff --git a/src/Exceptions.d.ts b/src/Exceptions.d.ts
index 03c9fd8..818bf4a 100644
--- a/src/Exceptions.d.ts
+++ b/src/Exceptions.d.ts
@@ -1,24 +1,26 @@
declare class BaseError extends Error {
- code: number
+ code: number;
}
export class ConfigurationError extends BaseError {
- parameter: string
- value: any
+ parameter: string;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ value: any;
- constructor(parameter: string, value?: any);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ constructor(parameter: string, value?: any);
}
export class InvalidStateError extends BaseError {
- status: number
+ status: number;
- constructor(status: number);
+ constructor(status: number);
}
export class NotSupportedError extends BaseError {
- constructor(message: string);
+ constructor(message: string);
}
export class NotReadyError extends BaseError {
- constructor(message: string);
+ constructor(message: string);
}
diff --git a/src/Exceptions.js b/src/Exceptions.js
index 70492c4..00d9ebc 100644
--- a/src/Exceptions.js
+++ b/src/Exceptions.js
@@ -1,59 +1,51 @@
-class ConfigurationError extends Error
-{
- constructor(parameter, value)
- {
- super();
-
- this.code = 1;
- this.name = 'CONFIGURATION_ERROR';
- this.parameter = parameter;
- this.value = value;
- this.message = (!this.value)?
- `Missing parameter: ${this.parameter}` :
- `Invalid value ${JSON.stringify(this.value)} for parameter "${this.parameter}"`;
- }
+class ConfigurationError extends Error {
+ constructor(parameter, value) {
+ super();
+
+ this.code = 1;
+ this.name = 'CONFIGURATION_ERROR';
+ this.parameter = parameter;
+ this.value = value;
+ this.message = !this.value
+ ? `Missing parameter: ${this.parameter}`
+ : `Invalid value ${JSON.stringify(this.value)} for parameter "${this.parameter}"`;
+ }
}
-class InvalidStateError extends Error
-{
- constructor(status)
- {
- super();
-
- this.code = 2;
- this.name = 'INVALID_STATE_ERROR';
- this.status = status;
- this.message = `Invalid status: ${status}`;
- }
+class InvalidStateError extends Error {
+ constructor(status) {
+ super();
+
+ this.code = 2;
+ this.name = 'INVALID_STATE_ERROR';
+ this.status = status;
+ this.message = `Invalid status: ${status}`;
+ }
}
-class NotSupportedError extends Error
-{
- constructor(message)
- {
- super();
+class NotSupportedError extends Error {
+ constructor(message) {
+ super();
- this.code = 3;
- this.name = 'NOT_SUPPORTED_ERROR';
- this.message = message;
- }
+ this.code = 3;
+ this.name = 'NOT_SUPPORTED_ERROR';
+ this.message = message;
+ }
}
-class NotReadyError extends Error
-{
- constructor(message)
- {
- super();
+class NotReadyError extends Error {
+ constructor(message) {
+ super();
- this.code = 4;
- this.name = 'NOT_READY_ERROR';
- this.message = message;
- }
+ this.code = 4;
+ this.name = 'NOT_READY_ERROR';
+ this.message = message;
+ }
}
module.exports = {
- ConfigurationError,
- InvalidStateError,
- NotSupportedError,
- NotReadyError
+ ConfigurationError,
+ InvalidStateError,
+ NotSupportedError,
+ NotReadyError,
};
diff --git a/src/Grammar.d.ts b/src/Grammar.d.ts
index 6ac7501..3baa70c 100644
--- a/src/Grammar.d.ts
+++ b/src/Grammar.d.ts
@@ -1,5 +1,7 @@
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Grammar = any;
export function parse(input: string, startRule?: string): Grammar;
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function toSource(): any;
diff --git a/src/JsSIP.d.ts b/src/JsSIP.d.ts
index 42edaff..e9a896e 100644
--- a/src/JsSIP.d.ts
+++ b/src/JsSIP.d.ts
@@ -1,18 +1,18 @@
-import {Debug} from 'debug'
+import { Debug } from 'debug';
-import * as C from './Constants'
-import * as Exceptions from './Exceptions'
-import * as Grammar from './Grammar'
-import * as Utils from './Utils'
+import * as C from './Constants';
+import * as Exceptions from './Exceptions';
+import * as Grammar from './Grammar';
+import * as Utils from './Utils';
export { C, Exceptions, Grammar, Utils };
-export {UA} from './UA'
-export {URI} from './URI'
-export {NameAddrHeader} from './NameAddrHeader'
-export {WebSocketInterface} from './WebSocketInterface'
-export {Socket, WeightedSocket} from './Socket'
+export { UA } from './UA';
+export { URI } from './URI';
+export { NameAddrHeader } from './NameAddrHeader';
+export { WebSocketInterface } from './WebSocketInterface';
+export { Socket, WeightedSocket } from './Socket';
-export const debug: Debug
-export const name: string
-export const version: string
+export const debug: Debug;
+export const name: string;
+export const version: string;
diff --git a/src/JsSIP.js b/src/JsSIP.js
index bb9fa24..39481d1 100644
--- a/src/JsSIP.js
+++ b/src/JsSIP.js
@@ -16,17 +16,21 @@ debug('version %s', pkg.version);
* Expose the JsSIP module.
*/
module.exports = {
- C,
- Exceptions,
- Utils,
- UA,
- URI,
- NameAddrHeader,
- WebSocketInterface,
- Grammar,
- RTCSession,
- // Expose the debug module.
- debug : require('debug'),
- get name() { return pkg.title; },
- get version() { return pkg.version; }
+ C,
+ Exceptions,
+ Utils,
+ UA,
+ URI,
+ NameAddrHeader,
+ WebSocketInterface,
+ Grammar,
+ RTCSession,
+ // Expose the debug module.
+ debug: require('debug'),
+ get name() {
+ return pkg.title;
+ },
+ get version() {
+ return pkg.version;
+ },
};
diff --git a/src/Logger.js b/src/Logger.js
index 4feeceb..4abb8e7 100644
--- a/src/Logger.js
+++ b/src/Logger.js
@@ -2,41 +2,33 @@ const debug = require('debug');
const APP_NAME = 'JsSIP';
-module.exports = class Logger
-{
- constructor(prefix)
- {
- if (prefix)
- {
- this._debug = debug.default(`${APP_NAME}:${prefix}`);
- this._warn = debug.default(`${APP_NAME}:WARN:${prefix}`);
- this._error = debug.default(`${APP_NAME}:ERROR:${prefix}`);
- }
- else
- {
- this._debug = debug.default(APP_NAME);
- this._warn = debug.default(`${APP_NAME}:WARN`);
- this._error = debug.default(`${APP_NAME}:ERROR`);
- }
- /* eslint-disable no-console */
- this._debug.log = console.info.bind(console);
- this._warn.log = console.warn.bind(console);
- this._error.log = console.error.bind(console);
- /* eslint-enable no-console */
- }
+module.exports = class Logger {
+ constructor(prefix) {
+ if (prefix) {
+ this._debug = debug.default(`${APP_NAME}:${prefix}`);
+ this._warn = debug.default(`${APP_NAME}:WARN:${prefix}`);
+ this._error = debug.default(`${APP_NAME}:ERROR:${prefix}`);
+ } else {
+ this._debug = debug.default(APP_NAME);
+ this._warn = debug.default(`${APP_NAME}:WARN`);
+ this._error = debug.default(`${APP_NAME}:ERROR`);
+ }
+ /* eslint-disable no-console */
+ this._debug.log = console.info.bind(console);
+ this._warn.log = console.warn.bind(console);
+ this._error.log = console.error.bind(console);
+ /* eslint-enable no-console */
+ }
- get debug()
- {
- return this._debug;
- }
+ get debug() {
+ return this._debug;
+ }
- get warn()
- {
- return this._warn;
- }
+ get warn() {
+ return this._warn;
+ }
- get error()
- {
- return this._error;
- }
+ get error() {
+ return this._error;
+ }
};
diff --git a/src/Message.d.ts b/src/Message.d.ts
index 8ee3685..e0cc24b 100644
--- a/src/Message.d.ts
+++ b/src/Message.d.ts
@@ -1,46 +1,55 @@
-import {EventEmitter} from 'events'
-
-import {ExtraHeaders, Originator, OutgoingListener, SessionDirection, TerminateOptions} from './RTCSession'
-import {IncomingResponse} from './SIPMessage'
-import {NameAddrHeader} from './NameAddrHeader'
-import {causes} from './Constants';
+import { EventEmitter } from 'events';
+
+import {
+ ExtraHeaders,
+ Originator,
+ OutgoingListener,
+ SessionDirection,
+ TerminateOptions,
+} from './RTCSession';
+import { IncomingResponse } from './SIPMessage';
+import { NameAddrHeader } from './NameAddrHeader';
+import { causes } from './Constants';
export interface AcceptOptions extends ExtraHeaders {
- body?: string;
+ body?: string;
}
export interface MessageFailedEvent {
- originator: Originator;
- response: IncomingResponse;
- cause?: causes;
+ originator: Originator;
+ response: IncomingResponse;
+ cause?: causes;
}
export type MessageFailedListener = (event: MessageFailedEvent) => void;
export interface MessageEventMap {
- succeeded: OutgoingListener;
- failed: MessageFailedListener;
+ succeeded: OutgoingListener;
+ failed: MessageFailedListener;
}
export interface SendMessageOptions extends ExtraHeaders {
- contentType?: string;
- eventHandlers?: Partial<MessageEventMap>;
- fromUserName?: string;
- fromDisplayName?: string;
+ contentType?: string;
+ eventHandlers?: Partial<MessageEventMap>;
+ fromUserName?: string;
+ fromDisplayName?: string;
}
export class Message extends EventEmitter {
- get direction(): SessionDirection;
+ get direction(): SessionDirection;
- get local_identity(): NameAddrHeader;
+ get local_identity(): NameAddrHeader;
- get remote_identity(): NameAddrHeader;
+ get remote_identity(): NameAddrHeader;
- send(target: string, body: string, options?: SendMessageOptions): void;
+ send(target: string, body: string, options?: SendMessageOptions): void;
- accept(options: AcceptOptions): void;
+ accept(options: AcceptOptions): void;
- reject(options: TerminateOptions): void;
+ reject(options: TerminateOptions): void;
- on<T extends keyof MessageEventMap>(type: T, listener: MessageEventMap[T]): this;
+ on<T extends keyof MessageEventMap>(
+ type: T,
+ listener: MessageEventMap[T]
+ ): this;
}
diff --git a/src/Message.js b/src/Message.js
index daadbc5..801f823 100644
--- a/src/Message.js
+++ b/src/Message.js
@@ -9,287 +9,264 @@ const URI = require('./URI');
const logger = new Logger('Message');
-module.exports = class Message extends EventEmitter
-{
- constructor(ua)
- {
- super();
-
- this._ua = ua;
- this._request = null;
- this._closed = false;
-
- this._direction = null;
- this._local_identity = null;
- this._remote_identity = null;
-
- // Whether an incoming message has been replied.
- this._is_replied = false;
-
- // Custom message empty object for high level use.
- this._data = {};
- }
-
- get direction()
- {
- return this._direction;
- }
-
- get local_identity()
- {
- return this._local_identity;
- }
-
- get remote_identity()
- {
- return this._remote_identity;
- }
-
- send(target, body, options = {})
- {
- const originalTarget = target;
-
- if (target === undefined || body === undefined)
- {
- throw new TypeError('Not enough arguments');
- }
-
- // Check target validity.
- target = this._ua.normalizeTarget(target);
- if (!target)
- {
- throw new TypeError(`Invalid target: ${originalTarget}`);
- }
-
- // Get call options.
- const extraHeaders = Utils.cloneArray(options.extraHeaders);
- const eventHandlers = Utils.cloneObject(options.eventHandlers);
- const contentType = options.contentType || 'text/plain';
-
- const requestParams = {};
-
- if (options.fromUserName)
- {
- requestParams.from_uri = new URI('sip', options.fromUserName, this._ua.configuration.uri.host);
-
- extraHeaders.push(`P-Preferred-Identity: ${this._ua.configuration.uri.toString()}`);
- }
-
- if (options.fromDisplayName)
- {
- requestParams.from_display_name = options.fromDisplayName;
- }
-
- // Set event handlers.
- for (const event in eventHandlers)
- {
- if (Object.prototype.hasOwnProperty.call(eventHandlers, event))
- {
- this.on(event, eventHandlers[event]);
- }
- }
-
- extraHeaders.push(`Content-Type: ${contentType}`);
-
- this._request = new SIPMessage.OutgoingRequest(
- JsSIP_C.MESSAGE, target, this._ua, requestParams, extraHeaders);
-
- if (body)
- {
- this._request.body = body;
- }
-
- const request_sender = new RequestSender(this._ua, this._request, {
- onRequestTimeout : () =>
- {
- this._onRequestTimeout();
- },
- onTransportError : () =>
- {
- this._onTransportError();
- },
- onReceiveResponse : (response) =>
- {
- this._receiveResponse(response);
- }
- });
-
- this._newMessage('local', this._request);
-
- request_sender.send();
- }
-
- init_incoming(request)
- {
- this._request = request;
-
- this._newMessage('remote', request);
-
- // Reply with a 200 OK if the user didn't reply.
- if (!this._is_replied)
- {
- this._is_replied = true;
- request.reply(200);
- }
-
- this._close();
- }
-
- /**
- * Accept the incoming Message
- * Only valid for incoming Messages
- */
- accept(options = {})
- {
- const extraHeaders = Utils.cloneArray(options.extraHeaders);
- const body = options.body;
-
- if (this._direction !== 'incoming')
- {
- throw new Exceptions.NotSupportedError('"accept" not supported for outgoing Message');
- }
-
- if (this._is_replied)
- {
- throw new Error('incoming Message already replied');
- }
-
- this._is_replied = true;
- this._request.reply(200, null, extraHeaders, body);
- }
-
- /**
- * Reject the incoming Message
- * Only valid for incoming Messages
- */
- reject(options = {})
- {
- const status_code = options.status_code || 480;
- const reason_phrase = options.reason_phrase;
- const extraHeaders = Utils.cloneArray(options.extraHeaders);
- const body = options.body;
-
- if (this._direction !== 'incoming')
- {
- throw new Exceptions.NotSupportedError('"reject" not supported for outgoing Message');
- }
-
- if (this._is_replied)
- {
- throw new Error('incoming Message already replied');
- }
-
- if (status_code < 300 || status_code >= 700)
- {
- throw new TypeError(`Invalid status_code: ${status_code}`);
- }
-
- this._is_replied = true;
- this._request.reply(status_code, reason_phrase, extraHeaders, body);
- }
-
- _receiveResponse(response)
- {
- if (this._closed)
- {
- return;
- }
- switch (true)
- {
- case /^1[0-9]{2}$/.test(response.status_code):
- // Ignore provisional responses.
- break;
-
- case /^2[0-9]{2}$/.test(response.status_code):
- this._succeeded('remote', response);
- break;
-
- default:
- {
- const cause = Utils.sipErrorCause(response.status_code);
-
- this._failed('remote', response, cause);
- break;
- }
- }
- }
-
- _onRequestTimeout()
- {
- if (this._closed)
- {
- return;
- }
- this._failed('system', null, JsSIP_C.causes.REQUEST_TIMEOUT);
- }
-
- _onTransportError()
- {
- if (this._closed)
- {
- return;
- }
- this._failed('system', null, JsSIP_C.causes.CONNECTION_ERROR);
- }
-
- _close()
- {
- this._closed = true;
- this._ua.destroyMessage(this);
- }
-
- /**
- * Internal Callbacks
- */
-
- _newMessage(originator, request)
- {
- if (originator === 'remote')
- {
- this._direction = 'incoming';
- this._local_identity = request.to;
- this._remote_identity = request.from;
- }
- else if (originator === 'local')
- {
- this._direction = 'outgoing';
- this._local_identity = request.from;
- this._remote_identity = request.to;
- }
-
- this._ua.newMessage(this, {
- originator,
- message : this,
- request
- });
- }
-
- _failed(originator, response, cause)
- {
- logger.debug('MESSAGE failed');
-
- this._close();
-
- logger.debug('emit "failed"');
-
- this.emit('failed', {
- originator,
- response : response || null,
- cause
- });
- }
-
- _succeeded(originator, response)
- {
- logger.debug('MESSAGE succeeded');
-
- this._close();
-
- logger.debug('emit "succeeded"');
-
- this.emit('succeeded', {
- originator,
- response
- });
- }
+module.exports = class Message extends EventEmitter {
+ constructor(ua) {
+ super();
+
+ this._ua = ua;
+ this._request = null;
+ this._closed = false;
+
+ this._direction = null;
+ this._local_identity = null;
+ this._remote_identity = null;
+
+ // Whether an incoming message has been replied.
+ this._is_replied = false;
+
+ // Custom message empty object for high level use.
+ this._data = {};
+ }
+
+ get direction() {
+ return this._direction;
+ }
+
+ get local_identity() {
+ return this._local_identity;
+ }
+
+ get remote_identity() {
+ return this._remote_identity;
+ }
+
+ send(target, body, options = {}) {
+ const originalTarget = target;
+
+ if (target === undefined || body === undefined) {
+ throw new TypeError('Not enough arguments');
+ }
+
+ // Check target validity.
+ target = this._ua.normalizeTarget(target);
+ if (!target) {
+ throw new TypeError(`Invalid target: ${originalTarget}`);
+ }
+
+ // Get call options.
+ const extraHeaders = Utils.cloneArray(options.extraHeaders);
+ const eventHandlers = Utils.cloneObject(options.eventHandlers);
+ const contentType = options.contentType || 'text/plain';
+
+ const requestParams = {};
+
+ if (options.fromUserName) {
+ requestParams.from_uri = new URI(
+ 'sip',
+ options.fromUserName,
+ this._ua.configuration.uri.host
+ );
+
+ extraHeaders.push(
+ `P-Preferred-Identity: ${this._ua.configuration.uri.toString()}`
+ );
+ }
+
+ if (options.fromDisplayName) {
+ requestParams.from_display_name = options.fromDisplayName;
+ }
+
+ // Set event handlers.
+ for (const event in eventHandlers) {
+ if (Object.prototype.hasOwnProperty.call(eventHandlers, event)) {
+ this.on(event, eventHandlers[event]);
+ }
+ }
+
+ extraHeaders.push(`Content-Type: ${contentType}`);
+
+ this._request = new SIPMessage.OutgoingRequest(
+ JsSIP_C.MESSAGE,
+ target,
+ this._ua,
+ requestParams,
+ extraHeaders
+ );
+
+ if (body) {
+ this._request.body = body;
+ }
+
+ const request_sender = new RequestSender(this._ua, this._request, {
+ onRequestTimeout: () => {
+ this._onRequestTimeout();
+ },
+ onTransportError: () => {
+ this._onTransportError();
+ },
+ onReceiveResponse: response => {
+ this._receiveResponse(response);
+ },
+ });
+
+ this._newMessage('local', this._request);
+
+ request_sender.send();
+ }
+
+ init_incoming(request) {
+ this._request = request;
+
+ this._newMessage('remote', request);
+
+ // Reply with a 200 OK if the user didn't reply.
+ if (!this._is_replied) {
+ this._is_replied = true;
+ request.reply(200);
+ }
+
+ this._close();
+ }
+
+ /**
+ * Accept the incoming Message
+ * Only valid for incoming Messages
+ */
+ accept(options = {}) {
+ const extraHeaders = Utils.cloneArray(options.extraHeaders);
+ const body = options.body;
+
+ if (this._direction !== 'incoming') {
+ throw new Exceptions.NotSupportedError(
+ '"accept" not supported for outgoing Message'
+ );
+ }
+
+ if (this._is_replied) {
+ throw new Error('incoming Message already replied');
+ }
+
+ this._is_replied = true;
+ this._request.reply(200, null, extraHeaders, body);
+ }
+
+ /**
+ * Reject the incoming Message
+ * Only valid for incoming Messages
+ */
+ reject(options = {}) {
+ const status_code = options.status_code || 480;
+ const reason_phrase = options.reason_phrase;
+ const extraHeaders = Utils.cloneArray(options.extraHeaders);
+ const body = options.body;
+
+ if (this._direction !== 'incoming') {
+ throw new Exceptions.NotSupportedError(
+ '"reject" not supported for outgoing Message'
+ );
+ }
+
+ if (this._is_replied) {
+ throw new Error('incoming Message already replied');
+ }
+
+ if (status_code < 300 || status_code >= 700) {
+ throw new TypeError(`Invalid status_code: ${status_code}`);
+ }
+
+ this._is_replied = true;
+ this._request.reply(status_code, reason_phrase, extraHeaders, body);
+ }
+
+ _receiveResponse(response) {
+ if (this._closed) {
+ return;
+ }
+ switch (true) {
+ case /^1[0-9]{2}$/.test(response.status_code): {
+ // Ignore provisional responses.
+ break;
+ }
+
+ case /^2[0-9]{2}$/.test(response.status_code): {
+ this._succeeded('remote', response);
+ break;
+ }
+
+ default: {
+ const cause = Utils.sipErrorCause(response.status_code);
+
+ this._failed('remote', response, cause);
+ break;
+ }
+ }
+ }
+
+ _onRequestTimeout() {
+ if (this._closed) {
+ return;
+ }
+ this._failed('system', null, JsSIP_C.causes.REQUEST_TIMEOUT);
+ }
+
+ _onTransportError() {
+ if (this._closed) {
+ return;
+ }
+ this._failed('system', null, JsSIP_C.causes.CONNECTION_ERROR);
+ }
+
+ _close() {
+ this._closed = true;
+ this._ua.destroyMessage(this);
+ }
+
+ /**
+ * Internal Callbacks
+ */
+
+ _newMessage(originator, request) {
+ if (originator === 'remote') {
+ this._direction = 'incoming';
+ this._local_identity = request.to;
+ this._remote_identity = request.from;
+ } else if (originator === 'local') {
+ this._direction = 'outgoing';
+ this._local_identity = request.from;
+ this._remote_identity = request.to;
+ }
+
+ this._ua.newMessage(this, {
+ originator,
+ message: this,
+ request,
+ });
+ }
+
+ _failed(originator, response, cause) {
+ logger.debug('MESSAGE failed');
+
+ this._close();
+
+ logger.debug('emit "failed"');
+
+ this.emit('failed', {
+ originator,
+ response: response || null,
+ cause,
+ });
+ }
+
+ _succeeded(originator, response) {
+ logger.debug('MESSAGE succeeded');
+
+ this._close();
+
+ logger.debug('emit "succeeded"');
+
+ this.emit('succeeded', {
+ originator,
+ response,
+ });
+ }
};
diff --git a/src/NameAddrHeader.d.ts b/src/NameAddrHeader.d.ts
index 6529c10..59e15cb 100644
--- a/src/NameAddrHeader.d.ts
+++ b/src/NameAddrHeader.d.ts
@@ -1,27 +1,28 @@
-import {Parameters, URI} from './URI'
-import {Grammar} from './Grammar'
+import { Parameters, URI } from './URI';
+import { Grammar } from './Grammar';
export class NameAddrHeader {
- get display_name(): string;
- set display_name(value: string);
+ get display_name(): string;
+ set display_name(value: string);
- get uri(): URI;
+ get uri(): URI;
- constructor(uri: URI, display_name?: string, parameters?: Parameters);
+ constructor(uri: URI, display_name?: string, parameters?: Parameters);
- setParam(key: string, value?: string): void;
+ setParam(key: string, value?: string): void;
- getParam<T = any>(key: string): T;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ getParam<T = any>(key: string): T;
- hasParam(key: string): boolean;
+ hasParam(key: string): boolean;
- deleteParam(key: string): void;
+ deleteParam(key: string): void;
- clearParams(): void;
+ clearParams(): void;
- clone(): this;
+ clone(): this;
- toString(): string;
+ toString(): string;
- static parse(uri: string): Grammar | undefined;
+ static parse(uri: string): Grammar | undefined;
}
diff --git a/src/NameAddrHeader.js b/src/NameAddrHeader.js
index dd4e454..8119331 100644
--- a/src/NameAddrHeader.js
+++ b/src/NameAddrHeader.js
@@ -1,139 +1,118 @@
const URI = require('./URI');
const Grammar = require('./Grammar');
-module.exports = class NameAddrHeader
-{
- /**
- * Parse the given string and returns a NameAddrHeader instance or undefined if
- * it is an invalid NameAddrHeader.
- */
- static parse(name_addr_header)
- {
- name_addr_header = Grammar.parse(name_addr_header, 'Name_Addr_Header');
-
- if (name_addr_header !== -1)
- {
- return name_addr_header;
- }
- else
- {
- return undefined;
- }
- }
-
- constructor(uri, display_name, parameters)
- {
- // Checks.
- if (!uri || !(uri instanceof URI))
- {
- throw new TypeError('missing or invalid "uri" parameter');
- }
-
- // Initialize parameters.
- this._uri = uri;
- this._parameters = {};
- this.display_name = display_name;
-
- for (const param in parameters)
- {
- if (Object.prototype.hasOwnProperty.call(parameters, param))
- {
- this.setParam(param, parameters[param]);
- }
- }
- }
-
- get uri()
- {
- return this._uri;
- }
-
- get display_name()
- {
- return this._display_name;
- }
-
- set display_name(value)
- {
- this._display_name = (value === 0) ? '0' : value;
- }
-
- setParam(key, value)
- {
- if (key)
- {
- this._parameters[key.toLowerCase()] = (typeof value === 'undefined' || value === null) ? null : value.toString();
- }
- }
-
- getParam(key)
- {
- if (key)
- {
- return this._parameters[key.toLowerCase()];
- }
- }
-
- hasParam(key)
- {
- if (key)
- {
- return (this._parameters.hasOwnProperty(key.toLowerCase()) && true) || false;
- }
- }
-
- deleteParam(parameter)
- {
- parameter = parameter.toLowerCase();
- if (this._parameters.hasOwnProperty(parameter))
- {
- const value = this._parameters[parameter];
-
- delete this._parameters[parameter];
-
- return value;
- }
- }
-
- clearParams()
- {
- this._parameters = {};
- }
-
- clone()
- {
- return new NameAddrHeader(
- this._uri.clone(),
- this._display_name,
- JSON.parse(JSON.stringify(this._parameters)));
- }
-
- _quote(str)
- {
- return str
- .replace(/\\/g, '\\\\')
- .replace(/"/g, '\\"');
- }
-
- toString()
- {
- let body = this._display_name ? `"${this._quote(this._display_name)}" ` : '';
-
- body += `<${this._uri.toString()}>`;
-
- for (const parameter in this._parameters)
- {
- if (Object.prototype.hasOwnProperty.call(this._parameters, parameter))
- {
- body += `;${parameter}`;
-
- if (this._parameters[parameter] !== null)
- {
- body += `=${this._parameters[parameter]}`;
- }
- }
- }
-
- return body;
- }
+module.exports = class NameAddrHeader {
+ /**
+ * Parse the given string and returns a NameAddrHeader instance or undefined if
+ * it is an invalid NameAddrHeader.
+ */
+ static parse(name_addr_header) {
+ name_addr_header = Grammar.parse(name_addr_header, 'Name_Addr_Header');
+
+ if (name_addr_header !== -1) {
+ return name_addr_header;
+ } else {
+ return undefined;
+ }
+ }
+
+ constructor(uri, display_name, parameters) {
+ // Checks.
+ if (!uri || !(uri instanceof URI)) {
+ throw new TypeError('missing or invalid "uri" parameter');
+ }
+
+ // Initialize parameters.
+ this._uri = uri;
+ this._parameters = {};
+ this.display_name = display_name;
+
+ for (const param in parameters) {
+ if (Object.prototype.hasOwnProperty.call(parameters, param)) {
+ this.setParam(param, parameters[param]);
+ }
+ }
+ }
+
+ get uri() {
+ return this._uri;
+ }
+
+ get display_name() {
+ return this._display_name;
+ }
+
+ set display_name(value) {
+ this._display_name = value === 0 ? '0' : value;
+ }
+
+ setParam(key, value) {
+ if (key) {
+ this._parameters[key.toLowerCase()] =
+ typeof value === 'undefined' || value === null
+ ? null
+ : value.toString();
+ }
+ }
+
+ getParam(key) {
+ if (key) {
+ return this._parameters[key.toLowerCase()];
+ }
+ }
+
+ hasParam(key) {
+ if (key) {
+ return (
+ (this._parameters.hasOwnProperty(key.toLowerCase()) && true) || false
+ );
+ }
+ }
+
+ deleteParam(parameter) {
+ parameter = parameter.toLowerCase();
+ if (this._parameters.hasOwnProperty(parameter)) {
+ const value = this._parameters[parameter];
+
+ delete this._parameters[parameter];
+
+ return value;
+ }
+ }
+
+ clearParams() {
+ this._parameters = {};
+ }
+
+ clone() {
+ return new NameAddrHeader(
+ this._uri.clone(),
+ this._display_name,
+ JSON.parse(JSON.stringify(this._parameters))
+ );
+ }
+
+ _quote(str) {
+ return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
+ }
+
+ toString() {
+ let body = this._display_name
+ ? `"${this._quote(this._display_name)}" `
+ : '';
+
+ body += `<${this._uri.toString()}>`;
+
+ for (const parameter in this._parameters) {
+ if (Object.prototype.hasOwnProperty.call(this._parameters, parameter)) {
+ body += `;${parameter}`;
+
+ if (this._parameters[parameter] !== null) {
+ body += `=${this._parameters[parameter]}`;
+ }
+ }
+ }
+
+ return body;
+ }
};
diff --git a/src/Notifier.d.ts b/src/Notifier.d.ts
index aa863ea..85d2bbb 100644
--- a/src/Notifier.d.ts
+++ b/src/Notifier.d.ts
@@ -1,40 +1,52 @@
-import {EventEmitter} from 'events'
-import {IncomingRequest} from './SIPMessage'
-import {UA} from './UA'
+import { EventEmitter } from 'events';
+import { IncomingRequest } from './SIPMessage';
+import { UA } from './UA';
declare enum NotifierTerminationReason {
- NOTIFY_RESPONSE_TIMEOUT = 0,
- NOTIFY_TRANSPORT_ERROR = 1,
- NOTIFY_NON_OK_RESPONSE = 2,
- NOTIFY_AUTHENTICATION_FAILED = 3,
- FINAL_NOTIFY_SENT = 4,
- UNSUBSCRIBE_RECEIVED = 5,
- SUBSCRIPTION_EXPIRED = 6
+ NOTIFY_RESPONSE_TIMEOUT = 0,
+ NOTIFY_TRANSPORT_ERROR = 1,
+ NOTIFY_NON_OK_RESPONSE = 2,
+ NOTIFY_AUTHENTICATION_FAILED = 3,
+ FINAL_NOTIFY_SENT = 4,
+ UNSUBSCRIBE_RECEIVED = 5,
+ SUBSCRIPTION_EXPIRED = 6,
}
export interface MessageEventMap {
- terminated: [terminationCode: NotifierTerminationReason];
- subscribe: [isUnsubscribe: boolean, request: IncomingRequest, body: string | undefined, contentType: string | undefined];
- expired: [];
+ terminated: [terminationCode: NotifierTerminationReason];
+ subscribe: [
+ isUnsubscribe: boolean,
+ request: IncomingRequest,
+ body: string | undefined,
+ contentType: string | undefined,
+ ];
+ expired: [];
}
interface NotifierOptions {
- extraHeaders?: Array<string>;
- allowEvents?: string;
- pending?: boolean;
- defaultExpires?: number;
+ extraHeaders?: string[];
+ allowEvents?: string;
+ pending?: boolean;
+ defaultExpires?: number;
}
export class Notifier extends EventEmitter<MessageEventMap> {
- constructor(ua: UA, subscribe: IncomingRequest, contentType: string, options: NotifierOptions)
- start(): void;
- setActiveState(): void;
- notify(body?: string): void;
- terminate(body?: string, reason?: string, retryAfter?: number): void;
- get state(): string;
- get id(): string;
- set data(_data: any);
- get data(): any;
- static get C(): typeof NotifierTerminationReason;
- get C(): typeof NotifierTerminationReason;
+ constructor(
+ ua: UA,
+ subscribe: IncomingRequest,
+ contentType: string,
+ options: NotifierOptions
+ );
+ start(): void;
+ setActiveState(): void;
+ notify(body?: string): void;
+ terminate(body?: string, reason?: string, retryAfter?: number): void;
+ get state(): string;
+ get id(): string;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ set data(_data: any);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ get data(): any;
+ static get C(): typeof NotifierTerminationReason;
+ get C(): typeof NotifierTerminationReason;
}
diff --git a/src/Notifier.js b/src/Notifier.js
index 529db4a..deea8bb 100644
--- a/src/Notifier.js
+++ b/src/Notifier.js
@@ -11,459 +11,420 @@ const logger = new Logger('Notifier');
* Termination codes.
*/
const C = {
- // Termination codes.
- NOTIFY_RESPONSE_TIMEOUT : 0,
- NOTIFY_TRANSPORT_ERROR : 1,
- NOTIFY_NON_OK_RESPONSE : 2,
- NOTIFY_AUTHENTICATION_FAILED : 3,
- FINAL_NOTIFY_SENT : 4,
- UNSUBSCRIBE_RECEIVED : 5,
- SUBSCRIPTION_EXPIRED : 6,
-
- // Notifer states.
- STATE_PENDING : 0,
- STATE_ACTIVE : 1,
- STATE_TERMINATED : 2,
-
- // RFC 6665 3.1.1, default expires value.
- DEFAULT_EXPIRES_SEC : 900
+ // Termination codes.
+ NOTIFY_RESPONSE_TIMEOUT: 0,
+ NOTIFY_TRANSPORT_ERROR: 1,
+ NOTIFY_NON_OK_RESPONSE: 2,
+ NOTIFY_AUTHENTICATION_FAILED: 3,
+ FINAL_NOTIFY_SENT: 4,
+ UNSUBSCRIBE_RECEIVED: 5,
+ SUBSCRIPTION_EXPIRED: 6,
+
+ // Notifer states.
+ STATE_PENDING: 0,
+ STATE_ACTIVE: 1,
+ STATE_TERMINATED: 2,
+
+ // RFC 6665 3.1.1, default expires value.
+ DEFAULT_EXPIRES_SEC: 900,
};
/**
* RFC 6665 Notifier implementation.
*/
-module.exports = class Notifier extends EventEmitter
-{
- /**
- * Expose C object.
- */
- static get C()
- {
- return C;
- }
-
- static init_incoming(request, callback)
- {
- try
- {
- Notifier.checkSubscribe(request);
- }
- catch (error)
- {
- logger.warn('Notifier.init_incoming: invalid request. Error: ', error.message);
-
- request.reply(405);
-
- return;
- }
-
- callback();
- }
-
- static checkSubscribe(subscribe)
- {
- if (!subscribe)
- {
- throw new TypeError('Not enough arguments. Missing subscribe request');
- }
- if (subscribe.method !== JsSIP_C.SUBSCRIBE)
- {
- throw new TypeError('Invalid method for Subscribe request');
- }
- if (!subscribe.hasHeader('contact'))
- {
- throw new TypeError('Missing Contact header in subscribe request');
- }
- if (!subscribe.hasHeader('event'))
- {
- throw new TypeError('Missing Event header in subscribe request');
- }
- const expires = subscribe.getHeader('expires');
-
- if (expires)
- {
- const parsed_expires = parseInt(expires);
-
- if (!Utils.isDecimal(parsed_expires) || parsed_expires < 0)
- {
- throw new TypeError('Invalid Expires header field in subscribe request');
- }
- }
- }
-
- /**
- * @param {UA} ua - JsSIP User Agent instance.
- * @param {IncomingRequest} subscribe - Subscribe request.
- * @param {string} contentType - Content-Type header value.
- * @param {NotifierOptions} options - Optional parameters.
- * @param {Array<string>} extraHeaders - Additional SIP headers.
- * @param {string} allowEvents - Allow-Events header value.
- * @param {boolean} pending - Set initial dialog state as "pending".
- * @param {number} defaultExpires - Default expires value (seconds).
- */
- constructor(ua, subscribe, contentType, {
- extraHeaders, allowEvents, pending, defaultExpires
- })
- {
- logger.debug('new');
-
- super();
-
- if (!contentType)
- {
- throw new TypeError('Not enough arguments. Missing contentType');
- }
-
- Notifier.checkSubscribe(subscribe);
-
- const eventName = subscribe.getHeader('event');
-
- this._ua = ua;
- this._initial_subscribe = subscribe;
- this._expires_timestamp = null;
- this._expires_timer = null;
- this._defaultExpires = defaultExpires || C.DEFAULT_EXPIRES_SEC;
-
- // Notifier state: pending, active, terminated.
- this._state = pending ? C.STATE_PENDING : C.STATE_ACTIVE;
-
- this._content_type = contentType;
- this._headers = Utils.cloneArray(extraHeaders);
- this._headers.push(`Event: ${eventName}`);
-
- // Use contact from extraHeaders or create it.
- this._contact = this._headers.find((header) => header.startsWith('Contact'));
- if (!this._contact)
- {
- this._contact = `Contact: ${this._ua._contact.toString()}`;
-
- this._headers.push(this._contact);
- }
-
- if (allowEvents)
- {
- this._headers.push(`Allow-Events: ${allowEvents}`);
- }
-
- this._target = subscribe.from.uri.user;
- subscribe.to_tag = Utils.newTag();
-
- // Custom session empty object for high level use.
- this._data = {};
- }
-
- // Expose Notifier constants as a property of the Notifier instance.
- get C()
- {
- return C;
- }
-
- /**
- * Get dialog state.
- */
- get state()
- {
- return this._state;
- }
-
- /**
- * Get dialog id.
- */
- get id()
- {
- return this._dialog ? this._dialog.id : null;
- }
-
- get data()
- {
- return this._data;
- }
-
- set data(_data)
- {
- this._data = _data;
- }
-
- /**
- * Dialog callback.
- * Called also for initial subscribe.
- * Supported RFC 6665 4.4.3: initial fetch subscribe (with expires: 0).
- */
- receiveRequest(request)
- {
- if (request.method !== JsSIP_C.SUBSCRIBE)
- {
- request.reply(405);
-
- return;
- }
-
- this._setExpires(request);
-
- // Create dialog for normal and fetch-subscribe.
- if (!this._dialog)
- {
- this._dialog = new Dialog(this, request, 'UAS');
- }
-
- request.reply(200, null, [ `Expires: ${this._expires}`, `${this._contact}` ]);
-
- const body = request.body;
- const content_type = request.getHeader('content-type');
- const is_unsubscribe = this._expires === 0;
-
- if (!is_unsubscribe)
- {
- this._setExpiresTimer();
- }
-
- logger.debug('emit "subscribe"');
-
- this.emit('subscribe', is_unsubscribe, request, body, content_type);
-
- if (is_unsubscribe)
- {
- this._terminateDialog(C.UNSUBSCRIBE_RECEIVED);
- }
- }
-
- /**
- * User API
- */
-
- /**
- * Call this method after creating the Notifier instance and setting the event handlers.
- */
- start()
- {
- logger.debug('start()');
-
- if (this._state === C.STATE_TERMINATED)
- {
- throw new Exceptions.InvalidStateError(this._state);
- }
-
- this.receiveRequest(this._initial_subscribe);
- }
-
- /**
- * Switch pending dialog state to active.
- */
- setActiveState()
- {
- logger.debug('setActiveState()');
-
- if (this._state === C.STATE_TERMINATED)
- {
- throw new Exceptions.InvalidStateError(this._state);
- }
-
- if (this._state === C.STATE_PENDING)
- {
- this._state = C.STATE_ACTIVE;
- }
- }
-
- /**
- * Send the initial and subsequent notify request.
- * @param {string} body - notify request body.
- */
- notify(body=null)
- {
- logger.debug('notify()');
-
- if (this._state === C.STATE_TERMINATED)
- {
- throw new Exceptions.InvalidStateError(this._state);
- }
-
- const expires = Math.floor((this._expires_timestamp - new Date().getTime()) / 1000);
-
- // expires_timer is about to trigger. Clean up the timer and terminate.
- if (expires <= 0)
- {
- if (!this._expires_timer)
- {
- logger.error('expires timer is not set');
- }
-
- clearTimeout(this._expires_timer);
-
- this.terminate(body, 'timeout');
- }
- else
- {
- this._sendNotify([ `;expires=${expires}` ], body);
- }
- }
-
- /**
- * Terminate. (Send the final NOTIFY request).
- *
- * @param {string} body - Notify message body.
- * @param {string} reason - Set Subscription-State reason parameter.
- * @param {number} retryAfter - Set Subscription-State retry-after parameter.
- */
- terminate(body = null, reason = null, retryAfter = null)
- {
- logger.debug('terminate()');
-
- if (this._state === C.STATE_TERMINATED)
- {
- return;
- }
-
- const subsStateParameters = [];
-
- if (reason)
- {
- subsStateParameters.push(`;reason=${reason}`);
- }
-
- if (retryAfter !== null)
- {
- subsStateParameters.push(`;retry-after=${retryAfter}`);
- }
-
- this._sendNotify(subsStateParameters, body, null, 'terminated');
-
- this._terminateDialog(reason === 'timeout' ? C.SUBSCRIPTION_EXPIRED : C.FINAL_NOTIFY_SENT);
- }
-
- /**
- * Private API
- */
-
- _terminateDialog(termination_code)
- {
- if (this._state === C.STATE_TERMINATED)
- {
- return;
- }
- this._state = C.STATE_TERMINATED;
-
- clearTimeout(this._expires_timer);
- if (this._dialog)
- {
- this._dialog.terminate();
- this._dialog = null;
- }
- logger.debug(`emit "terminated" code=${termination_code}`);
-
- this.emit('terminated', termination_code);
- }
-
- _setExpires(request)
- {
- if (request.hasHeader('expires'))
- {
- this._expires = parseInt(request.getHeader('expires'));
- }
- else
- {
- this._expires = this._defaultExpires;
- logger.debug(`missing Expires header field, default value set: ${this._expires}`);
- }
- }
-
- /**
- * @param {Array<string>} subsStateParams subscription state parameters.
- * @param {String} body Notify body
- * @param {Array<string>} extraHeaders
- */
- _sendNotify(subsStateParameters, body=null, extraHeaders=null, state=null)
- {
- // Prevent send notify after final notify.
- if (this._state === C.STATE_TERMINATED)
- {
- logger.warn('final notify already sent');
-
- return;
- }
-
- // Build Subscription-State header with parameters.
- let subsState = `Subscription-State: ${state || this._parseState()}`;
-
- for (const param of subsStateParameters)
- {
- subsState += param;
- }
-
- let headers = Utils.cloneArray(this._headers);
-
- headers.push(subsState);
-
- if (extraHeaders)
- {
- headers = headers.concat(extraHeaders);
- }
-
- if (body)
- {
- headers.push(`Content-Type: ${this._content_type}`);
- }
-
- this._dialog.sendRequest(JsSIP_C.NOTIFY, {
- body,
- extraHeaders : headers,
- eventHandlers : {
- onRequestTimeout : () =>
- {
- this._terminateDialog(C.NOTIFY_RESPONSE_TIMEOUT);
- },
- onTransportError : () =>
- {
- this._terminateDialog(C.NOTIFY_TRANSPORT_ERROR);
- },
- onErrorResponse : (response) =>
- {
- if (response.status_code === 401 || response.status_code === 407)
- {
- this._terminateDialog(C.NOTIFY_AUTHENTICATION_FAILED);
- }
- else
- {
- this._terminateDialog(C.NOTIFY_NON_OK_RESPONSE);
- }
- },
- onDialogError : () =>
- {
- this._terminateDialog(C.NOTIFY_NON_OK_RESPONSE);
- }
- }
- });
- }
-
- _setExpiresTimer()
- {
- this._expires_timestamp = new Date().getTime() + (this._expires * 1000);
-
- clearTimeout(this._expires_timer);
- this._expires_timer = setTimeout(() =>
- {
- if (this._state === C.STATE_TERMINATED)
- {
- return;
- }
-
- logger.debug('emit "expired"');
-
- // Client can hook into the 'expired' event and call terminate to send a custom notify.
- this.emit('expired');
-
- // This will be no-op if the client already called `terminate()`.
- this.terminate(null, 'timeout');
- }, this._expires * 1000);
- }
-
- _parseState()
- {
- switch (this._state)
- {
- case C.STATE_PENDING: return 'pending';
- case C.STATE_ACTIVE: return 'active';
- case C.STATE_TERMINATED: return 'terminated';
- default: throw new TypeError('wrong state value');
- }
- }
+module.exports = class Notifier extends EventEmitter {
+ /**
+ * Expose C object.
+ */
+ static get C() {
+ return C;
+ }
+
+ static init_incoming(request, callback) {
+ try {
+ Notifier.checkSubscribe(request);
+ } catch (error) {
+ logger.warn(
+ 'Notifier.init_incoming: invalid request. Error: ',
+ error.message
+ );
+
+ request.reply(405);
+
+ return;
+ }
+
+ callback();
+ }
+
+ static checkSubscribe(subscribe) {
+ if (!subscribe) {
+ throw new TypeError('Not enough arguments. Missing subscribe request');
+ }
+ if (subscribe.method !== JsSIP_C.SUBSCRIBE) {
+ throw new TypeError('Invalid method for Subscribe request');
+ }
+ if (!subscribe.hasHeader('contact')) {
+ throw new TypeError('Missing Contact header in subscribe request');
+ }
+ if (!subscribe.hasHeader('event')) {
+ throw new TypeError('Missing Event header in subscribe request');
+ }
+ const expires = subscribe.getHeader('expires');
+
+ if (expires) {
+ const parsed_expires = parseInt(expires);
+
+ if (!Utils.isDecimal(parsed_expires) || parsed_expires < 0) {
+ throw new TypeError(
+ 'Invalid Expires header field in subscribe request'
+ );
+ }
+ }
+ }
+
+ /**
+ * @param {UA} ua - JsSIP User Agent instance.
+ * @param {IncomingRequest} subscribe - Subscribe request.
+ * @param {string} contentType - Content-Type header value.
+ * @param {NotifierOptions} options - Optional parameters.
+ * @param {Array<string>} extraHeaders - Additional SIP headers.
+ * @param {string} allowEvents - Allow-Events header value.
+ * @param {boolean} pending - Set initial dialog state as "pending".
+ * @param {number} defaultExpires - Default expires value (seconds).
+ */
+ constructor(
+ ua,
+ subscribe,
+ contentType,
+ { extraHeaders, allowEvents, pending, defaultExpires }
+ ) {
+ logger.debug('new');
+
+ super();
+
+ if (!contentType) {
+ throw new TypeError('Not enough arguments. Missing contentType');
+ }
+
+ Notifier.checkSubscribe(subscribe);
+
+ const eventName = subscribe.getHeader('event');
+
+ this._ua = ua;
+ this._initial_subscribe = subscribe;
+ this._expires_timestamp = null;
+ this._expires_timer = null;
+ this._defaultExpires = defaultExpires || C.DEFAULT_EXPIRES_SEC;
+
+ // Notifier state: pending, active, terminated.
+ this._state = pending ? C.STATE_PENDING : C.STATE_ACTIVE;
+
+ this._content_type = contentType;
+ this._headers = Utils.cloneArray(extraHeaders);
+ this._headers.push(`Event: ${eventName}`);
+
+ // Use contact from extraHeaders or create it.
+ this._contact = this._headers.find(header => header.startsWith('Contact'));
+ if (!this._contact) {
+ this._contact = `Contact: ${this._ua._contact.toString()}`;
+
+ this._headers.push(this._contact);
+ }
+
+ if (allowEvents) {
+ this._headers.push(`Allow-Events: ${allowEvents}`);
+ }
+
+ this._target = subscribe.from.uri.user;
+ subscribe.to_tag = Utils.newTag();
+
+ // Custom session empty object for high level use.
+ this._data = {};
+ }
+
+ // Expose Notifier constants as a property of the Notifier instance.
+ get C() {
+ return C;
+ }
+
+ /**
+ * Get dialog state.
+ */
+ get state() {
+ return this._state;
+ }
+
+ /**
+ * Get dialog id.
+ */
+ get id() {
+ return this._dialog ? this._dialog.id : null;
+ }
+
+ get data() {
+ return this._data;
+ }
+
+ set data(_data) {
+ this._data = _data;
+ }
+
+ /**
+ * Dialog callback.
+ * Called also for initial subscribe.
+ * Supported RFC 6665 4.4.3: initial fetch subscribe (with expires: 0).
+ */
+ receiveRequest(request) {
+ if (request.method !== JsSIP_C.SUBSCRIBE) {
+ request.reply(405);
+
+ return;
+ }
+
+ this._setExpires(request);
+
+ // Create dialog for normal and fetch-subscribe.
+ if (!this._dialog) {
+ this._dialog = new Dialog(this, request, 'UAS');
+ }
+
+ request.reply(200, null, [`Expires: ${this._expires}`, `${this._contact}`]);
+
+ const body = request.body;
+ const content_type = request.getHeader('content-type');
+ const is_unsubscribe = this._expires === 0;
+
+ if (!is_unsubscribe) {
+ this._setExpiresTimer();
+ }
+
+ logger.debug('emit "subscribe"');
+
+ this.emit('subscribe', is_unsubscribe, request, body, content_type);
+
+ if (is_unsubscribe) {
+ this._terminateDialog(C.UNSUBSCRIBE_RECEIVED);
+ }
+ }
+
+ /**
+ * User API
+ */
+
+ /**
+ * Call this method after creating the Notifier instance and setting the event handlers.
+ */
+ start() {
+ logger.debug('start()');
+
+ if (this._state === C.STATE_TERMINATED) {
+ throw new Exceptions.InvalidStateError(this._state);
+ }
+
+ this.receiveRequest(this._initial_subscribe);
+ }
+
+ /**
+ * Switch pending dialog state to active.
+ */
+ setActiveState() {
+ logger.debug('setActiveState()');
+
+ if (this._state === C.STATE_TERMINATED) {
+ throw new Exceptions.InvalidStateError(this._state);
+ }
+
+ if (this._state === C.STATE_PENDING) {
+ this._state = C.STATE_ACTIVE;
+ }
+ }
+
+ /**
+ * Send the initial and subsequent notify request.
+ * @param {string} body - notify request body.
+ */
+ notify(body = null) {
+ logger.debug('notify()');
+
+ if (this._state === C.STATE_TERMINATED) {
+ throw new Exceptions.InvalidStateError(this._state);
+ }
+
+ const expires = Math.floor(
+ (this._expires_timestamp - new Date().getTime()) / 1000
+ );
+
+ // expires_timer is about to trigger. Clean up the timer and terminate.
+ if (expires <= 0) {
+ if (!this._expires_timer) {
+ logger.error('expires timer is not set');
+ }
+
+ clearTimeout(this._expires_timer);
+
+ this.terminate(body, 'timeout');
+ } else {
+ this._sendNotify([`;expires=${expires}`], body);
+ }
+ }
+
+ /**
+ * Terminate. (Send the final NOTIFY request).
+ *
+ * @param {string} body - Notify message body.
+ * @param {string} reason - Set Subscription-State reason parameter.
+ * @param {number} retryAfter - Set Subscription-State retry-after parameter.
+ */
+ terminate(body = null, reason = null, retryAfter = null) {
+ logger.debug('terminate()');
+
+ if (this._state === C.STATE_TERMINATED) {
+ return;
+ }
+
+ const subsStateParameters = [];
+
+ if (reason) {
+ subsStateParameters.push(`;reason=${reason}`);
+ }
+
+ if (retryAfter !== null) {
+ subsStateParameters.push(`;retry-after=${retryAfter}`);
+ }
+
+ this._sendNotify(subsStateParameters, body, null, 'terminated');
+
+ this._terminateDialog(
+ reason === 'timeout' ? C.SUBSCRIPTION_EXPIRED : C.FINAL_NOTIFY_SENT
+ );
+ }
+
+ /**
+ * Private API
+ */
+
+ _terminateDialog(termination_code) {
+ if (this._state === C.STATE_TERMINATED) {
+ return;
+ }
+ this._state = C.STATE_TERMINATED;
+
+ clearTimeout(this._expires_timer);
+ if (this._dialog) {
+ this._dialog.terminate();
+ this._dialog = null;
+ }
+ logger.debug(`emit "terminated" code=${termination_code}`);
+
+ this.emit('terminated', termination_code);
+ }
+
+ _setExpires(request) {
+ if (request.hasHeader('expires')) {
+ this._expires = parseInt(request.getHeader('expires'));
+ } else {
+ this._expires = this._defaultExpires;
+ logger.debug(
+ `missing Expires header field, default value set: ${this._expires}`
+ );
+ }
+ }
+
+ /**
+ * @param {Array<string>} subsStateParams subscription state parameters.
+ * @param {String} body Notify body
+ * @param {Array<string>} extraHeaders
+ */
+ _sendNotify(
+ subsStateParameters,
+ body = null,
+ extraHeaders = null,
+ state = null
+ ) {
+ // Prevent send notify after final notify.
+ if (this._state === C.STATE_TERMINATED) {
+ logger.warn('final notify already sent');
+
+ return;
+ }
+
+ // Build Subscription-State header with parameters.
+ let subsState = `Subscription-State: ${state || this._parseState()}`;
+
+ for (const param of subsStateParameters) {
+ subsState += param;
+ }
+
+ let headers = Utils.cloneArray(this._headers);
+
+ headers.push(subsState);
+
+ if (extraHeaders) {
+ headers = headers.concat(extraHeaders);
+ }
+
+ if (body) {
+ headers.push(`Content-Type: ${this._content_type}`);
+ }
+
+ this._dialog.sendRequest(JsSIP_C.NOTIFY, {
+ body,
+ extraHeaders: headers,
+ eventHandlers: {
+ onRequestTimeout: () => {
+ this._terminateDialog(C.NOTIFY_RESPONSE_TIMEOUT);
+ },
+ onTransportError: () => {
+ this._terminateDialog(C.NOTIFY_TRANSPORT_ERROR);
+ },
+ onErrorResponse: response => {
+ if (response.status_code === 401 || response.status_code === 407) {
+ this._terminateDialog(C.NOTIFY_AUTHENTICATION_FAILED);
+ } else {
+ this._terminateDialog(C.NOTIFY_NON_OK_RESPONSE);
+ }
+ },
+ onDialogError: () => {
+ this._terminateDialog(C.NOTIFY_NON_OK_RESPONSE);
+ },
+ },
+ });
+ }
+
+ _setExpiresTimer() {
+ this._expires_timestamp = new Date().getTime() + this._expires * 1000;
+
+ clearTimeout(this._expires_timer);
+ this._expires_timer = setTimeout(() => {
+ if (this._state === C.STATE_TERMINATED) {
+ return;
+ }
+
+ logger.debug('emit "expired"');
+
+ // Client can hook into the 'expired' event and call terminate to send a custom notify.
+ this.emit('expired');
+
+ // This will be no-op if the client already called `terminate()`.
+ this.terminate(null, 'timeout');
+ }, this._expires * 1000);
+ }
+
+ _parseState() {
+ switch (this._state) {
+ case C.STATE_PENDING: {
+ return 'pending';
+ }
+ case C.STATE_ACTIVE: {
+ return 'active';
+ }
+ case C.STATE_TERMINATED: {
+ return 'terminated';
+ }
+ default: {
+ throw new TypeError('wrong state value');
+ }
+ }
+ }
};
diff --git a/src/Options.js b/src/Options.js
index 86b2888..f01c35d 100644
--- a/src/Options.js
+++ b/src/Options.js
@@ -8,273 +8,246 @@ const Exceptions = require('./Exceptions');
const logger = new Logger('Options');
-module.exports = class Options extends EventEmitter
-{
- constructor(ua)
- {
- super();
-
- this._ua = ua;
- this._request = null;
- this._closed = false;
-
- this._direction = null;
- this._local_identity = null;
- this._remote_identity = null;
-
- // Whether an incoming message has been replied.
- this._is_replied = false;
-
- // Custom message empty object for high level use.
- this._data = {};
- }
-
- get direction()
- {
- return this._direction;
- }
-
- get local_identity()
- {
- return this._local_identity;
- }
-
- get remote_identity()
- {
- return this._remote_identity;
- }
-
- send(target, body, options = {})
- {
- const originalTarget = target;
-
- if (target === undefined)
- {
- throw new TypeError('A target is required for OPTIONS');
- }
-
- // Check target validity.
- target = this._ua.normalizeTarget(target);
- if (!target)
- {
- throw new TypeError(`Invalid target: ${originalTarget}`);
- }
-
- // Get call options.
- const extraHeaders = Utils.cloneArray(options.extraHeaders);
- const eventHandlers = Utils.cloneObject(options.eventHandlers);
- const contentType = options.contentType || 'application/sdp';
-
- // Set event handlers.
- for (const event in eventHandlers)
- {
- if (Object.prototype.hasOwnProperty.call(eventHandlers, event))
- {
- this.on(event, eventHandlers[event]);
- }
- }
-
- extraHeaders.push(`Content-Type: ${contentType}`);
-
- this._request = new SIPMessage.OutgoingRequest(
- JsSIP_C.OPTIONS, target, this._ua, null, extraHeaders);
-
- if (body)
- {
- this._request.body = body;
- }
-
- const request_sender = new RequestSender(this._ua, this._request, {
- onRequestTimeout : () =>
- {
- this._onRequestTimeout();
- },
- onTransportError : () =>
- {
- this._onTransportError();
- },
- onReceiveResponse : (response) =>
- {
- this._receiveResponse(response);
- }
- });
-
- this._newOptions('local', this._request);
-
- request_sender.send();
- }
-
- init_incoming(request)
- {
- this._request = request;
-
- this._newOptions('remote', request);
-
- // Reply with a 200 OK if the user didn't reply.
- if (!this._is_replied)
- {
- this._is_replied = true;
- request.reply(200);
- }
-
- this._close();
- }
-
- /**
- * Accept the incoming Options
- * Only valid for incoming Options
- */
- accept(options = {})
- {
- const extraHeaders = Utils.cloneArray(options.extraHeaders);
- const body = options.body;
-
- if (this._direction !== 'incoming')
- {
- throw new Exceptions.NotSupportedError('"accept" not supported for outgoing Options');
- }
-
- if (this._is_replied)
- {
- throw new Error('incoming Options already replied');
- }
-
- this._is_replied = true;
- this._request.reply(200, null, extraHeaders, body);
- }
-
- /**
- * Reject the incoming Options
- * Only valid for incoming Options
- */
- reject(options = {})
- {
- const status_code = options.status_code || 480;
- const reason_phrase = options.reason_phrase;
- const extraHeaders = Utils.cloneArray(options.extraHeaders);
- const body = options.body;
-
- if (this._direction !== 'incoming')
- {
- throw new Exceptions.NotSupportedError('"reject" not supported for outgoing Options');
- }
-
- if (this._is_replied)
- {
- throw new Error('incoming Options already replied');
- }
-
- if (status_code < 300 || status_code >= 700)
- {
- throw new TypeError(`Invalid status_code: ${status_code}`);
- }
-
- this._is_replied = true;
- this._request.reply(status_code, reason_phrase, extraHeaders, body);
- }
-
- _receiveResponse(response)
- {
- if (this._closed)
- {
- return;
- }
- switch (true)
- {
- case /^1[0-9]{2}$/.test(response.status_code):
- // Ignore provisional responses.
- break;
-
- case /^2[0-9]{2}$/.test(response.status_code):
- this._succeeded('remote', response);
- break;
-
- default:
- {
- const cause = Utils.sipErrorCause(response.status_code);
-
- this._failed('remote', response, cause);
- break;
- }
- }
- }
-
- _onRequestTimeout()
- {
- if (this._closed)
- {
- return;
- }
- this._failed('system', null, JsSIP_C.causes.REQUEST_TIMEOUT);
- }
-
- _onTransportError()
- {
- if (this._closed)
- {
- return;
- }
- this._failed('system', null, JsSIP_C.causes.CONNECTION_ERROR);
- }
-
- _close()
- {
- this._closed = true;
- this._ua.destroyMessage(this);
- }
-
- /**
- * Internal Callbacks
- */
-
- _newOptions(originator, request)
- {
- if (originator === 'remote')
- {
- this._direction = 'incoming';
- this._local_identity = request.to;
- this._remote_identity = request.from;
- }
- else if (originator === 'local')
- {
- this._direction = 'outgoing';
- this._local_identity = request.from;
- this._remote_identity = request.to;
- }
-
- this._ua.newOptions(this, {
- originator,
- message : this,
- request
- });
- }
-
- _failed(originator, response, cause)
- {
- logger.debug('OPTIONS failed');
-
- this._close();
-
- logger.debug('emit "failed"');
-
- this.emit('failed', {
- originator,
- response : response || null,
- cause
- });
- }
-
- _succeeded(originator, response)
- {
- logger.debug('OPTIONS succeeded');
-
- this._close();
-
- logger.debug('emit "succeeded"');
-
- this.emit('succeeded', {
- originator,
- response
- });
- }
+module.exports = class Options extends EventEmitter {
+ constructor(ua) {
+ super();
+
+ this._ua = ua;
+ this._request = null;
+ this._closed = false;
+
+ this._direction = null;
+ this._local_identity = null;
+ this._remote_identity = null;
+
+ // Whether an incoming message has been replied.
+ this._is_replied = false;
+
+ // Custom message empty object for high level use.
+ this._data = {};
+ }
+
+ get direction() {
+ return this._direction;
+ }
+
+ get local_identity() {
+ return this._local_identity;
+ }
+
+ get remote_identity() {
+ return this._remote_identity;
+ }
+
+ send(target, body, options = {}) {
+ const originalTarget = target;
+
+ if (target === undefined) {
+ throw new TypeError('A target is required for OPTIONS');
+ }
+
+ // Check target validity.
+ target = this._ua.normalizeTarget(target);
+ if (!target) {
+ throw new TypeError(`Invalid target: ${originalTarget}`);
+ }
+
+ // Get call options.
+ const extraHeaders = Utils.cloneArray(options.extraHeaders);
+ const eventHandlers = Utils.cloneObject(options.eventHandlers);
+ const contentType = options.contentType || 'application/sdp';
+
+ // Set event handlers.
+ for (const event in eventHandlers) {
+ if (Object.prototype.hasOwnProperty.call(eventHandlers, event)) {
+ this.on(event, eventHandlers[event]);
+ }
+ }
+
+ extraHeaders.push(`Content-Type: ${contentType}`);
+
+ this._request = new SIPMessage.OutgoingRequest(
+ JsSIP_C.OPTIONS,
+ target,
+ this._ua,
+ null,
+ extraHeaders
+ );
+
+ if (body) {
+ this._request.body = body;
+ }
+
+ const request_sender = new RequestSender(this._ua, this._request, {
+ onRequestTimeout: () => {
+ this._onRequestTimeout();
+ },
+ onTransportError: () => {
+ this._onTransportError();
+ },
+ onReceiveResponse: response => {
+ this._receiveResponse(response);
+ },
+ });
+
+ this._newOptions('local', this._request);
+
+ request_sender.send();
+ }
+
+ init_incoming(request) {
+ this._request = request;
+
+ this._newOptions('remote', request);
+
+ // Reply with a 200 OK if the user didn't reply.
+ if (!this._is_replied) {
+ this._is_replied = true;
+ request.reply(200);
+ }
+
+ this._close();
+ }
+
+ /**
+ * Accept the incoming Options
+ * Only valid for incoming Options
+ */
+ accept(options = {}) {
+ const extraHeaders = Utils.cloneArray(options.extraHeaders);
+ const body = options.body;
+
+ if (this._direction !== 'incoming') {
+ throw new Exceptions.NotSupportedError(
+ '"accept" not supported for outgoing Options'
+ );
+ }
+
+ if (this._is_replied) {
+ throw new Error('incoming Options already replied');
+ }
+
+ this._is_replied = true;
+ this._request.reply(200, null, extraHeaders, body);
+ }
+
+ /**
+ * Reject the incoming Options
+ * Only valid for incoming Options
+ */
+ reject(options = {}) {
+ const status_code = options.status_code || 480;
+ const reason_phrase = options.reason_phrase;
+ const extraHeaders = Utils.cloneArray(options.extraHeaders);
+ const body = options.body;
+
+ if (this._direction !== 'incoming') {
+ throw new Exceptions.NotSupportedError(
+ '"reject" not supported for outgoing Options'
+ );
+ }
+
+ if (this._is_replied) {
+ throw new Error('incoming Options already replied');
+ }
+
+ if (status_code < 300 || status_code >= 700) {
+ throw new TypeError(`Invalid status_code: ${status_code}`);
+ }
+
+ this._is_replied = true;
+ this._request.reply(status_code, reason_phrase, extraHeaders, body);
+ }
+
+ _receiveResponse(response) {
+ if (this._closed) {
+ return;
+ }
+ switch (true) {
+ case /^1[0-9]{2}$/.test(response.status_code): {
+ // Ignore provisional responses.
+ break;
+ }
+
+ case /^2[0-9]{2}$/.test(response.status_code): {
+ this._succeeded('remote', response);
+ break;
+ }
+
+ default: {
+ const cause = Utils.sipErrorCause(response.status_code);
+
+ this._failed('remote', response, cause);
+ break;
+ }
+ }
+ }
+
+ _onRequestTimeout() {
+ if (this._closed) {
+ return;
+ }
+ this._failed('system', null, JsSIP_C.causes.REQUEST_TIMEOUT);
+ }
+
+ _onTransportError() {
+ if (this._closed) {
+ return;
+ }
+ this._failed('system', null, JsSIP_C.causes.CONNECTION_ERROR);
+ }
+
+ _close() {
+ this._closed = true;
+ this._ua.destroyMessage(this);
+ }
+
+ /**
+ * Internal Callbacks
+ */
+
+ _newOptions(originator, request) {
+ if (originator === 'remote') {
+ this._direction = 'incoming';
+ this._local_identity = request.to;
+ this._remote_identity = request.from;
+ } else if (originator === 'local') {
+ this._direction = 'outgoing';
+ this._local_identity = request.from;
+ this._remote_identity = request.to;
+ }
+
+ this._ua.newOptions(this, {
+ originator,
+ message: this,
+ request,
+ });
+ }
+
+ _failed(originator, response, cause) {
+ logger.debug('OPTIONS failed');
+
+ this._close();
+
+ logger.debug('emit "failed"');
+
+ this.emit('failed', {
+ originator,
+ response: response || null,
+ cause,
+ });
+ }
+
+ _succeeded(originator, response) {
+ logger.debug('OPTIONS succeeded');
+
+ this._close();
+
+ logger.debug('emit "succeeded"');
+
+ this.emit('succeeded', {
+ originator,
+ response,
+ });
+ }
};
diff --git a/src/Parser.js b/src/Parser.js
index e7a5090..79013d9 100644
--- a/src/Parser.js
+++ b/src/Parser.js
@@ -7,312 +7,297 @@ const logger = new Logger('Parser');
/**
* Parse SIP Message
*/
-exports.parseMessage = (data, ua) =>
-{
- let message;
- let bodyStart;
- let headerEnd = data.indexOf('\r\n');
+exports.parseMessage = (data, ua) => {
+ let message;
+ let bodyStart;
+ let headerEnd = data.indexOf('\r\n');
- if (headerEnd === -1)
- {
- logger.warn('parseMessage() | no CRLF found, not a SIP message');
+ if (headerEnd === -1) {
+ logger.warn('parseMessage() | no CRLF found, not a SIP message');
- return;
- }
+ return;
+ }
- // Parse first line. Check if it is a Request or a Reply.
- const firstLine = data.substring(0, headerEnd);
- let parsed = Grammar.parse(firstLine, 'Request_Response');
+ // Parse first line. Check if it is a Request or a Reply.
+ const firstLine = data.substring(0, headerEnd);
+ let parsed = Grammar.parse(firstLine, 'Request_Response');
- if (parsed === -1)
- {
- logger.warn(`parseMessage() | error parsing first line of SIP message: "${firstLine}"`);
+ if (parsed === -1) {
+ logger.warn(
+ `parseMessage() | error parsing first line of SIP message: "${firstLine}"`
+ );
- return;
- }
- else if (!parsed.status_code)
- {
- message = new SIPMessage.IncomingRequest(ua);
- message.method = parsed.method;
- message.ruri = parsed.uri;
- }
- else
- {
- message = new SIPMessage.IncomingResponse();
- message.status_code = parsed.status_code;
- message.reason_phrase = parsed.reason_phrase;
- }
+ return;
+ } else if (!parsed.status_code) {
+ message = new SIPMessage.IncomingRequest(ua);
+ message.method = parsed.method;
+ message.ruri = parsed.uri;
+ } else {
+ message = new SIPMessage.IncomingResponse();
+ message.status_code = parsed.status_code;
+ message.reason_phrase = parsed.reason_phrase;
+ }
- message.data = data;
- let headerStart = headerEnd + 2;
+ message.data = data;
+ let headerStart = headerEnd + 2;
- /* Loop over every line in data. Detect the end of each header and parse
- * it or simply add to the headers collection.
- */
- while (true)
- {
- headerEnd = getHeader(data, headerStart);
+ /* Loop over every line in data. Detect the end of each header and parse
+ * it or simply add to the headers collection.
+ */
+ while (true) {
+ headerEnd = getHeader(data, headerStart);
- // The SIP message has normally finished.
- if (headerEnd === -2)
- {
- bodyStart = headerStart + 2;
- break;
- }
- // Data.indexOf returned -1 due to a malformed message.
- else if (headerEnd === -1)
- {
- logger.warn('parseMessage() | malformed message');
+ // The SIP message has normally finished.
+ if (headerEnd === -2) {
+ bodyStart = headerStart + 2;
+ break;
+ }
+ // Data.indexOf returned -1 due to a malformed message.
+ else if (headerEnd === -1) {
+ logger.warn('parseMessage() | malformed message');
- return;
- }
+ return;
+ }
- parsed = parseHeader(message, data, headerStart, headerEnd);
+ parsed = parseHeader(message, data, headerStart, headerEnd);
- if (parsed !== true)
- {
- logger.warn('parseMessage() |', parsed.error);
+ if (parsed !== true) {
+ logger.warn('parseMessage() |', parsed.error);
- return;
- }
+ return;
+ }
- headerStart = headerEnd + 2;
- }
+ headerStart = headerEnd + 2;
+ }
- /* RFC3261 18.3.
- * If there are additional bytes in the transport packet
- * beyond the end of the body, they MUST be discarded.
- */
- if (message.hasHeader('content-length'))
- {
- const contentLength = message.getHeader('content-length');
+ /* RFC3261 18.3.
+ * If there are additional bytes in the transport packet
+ * beyond the end of the body, they MUST be discarded.
+ */
+ if (message.hasHeader('content-length')) {
+ const contentLength = message.getHeader('content-length');
- message.body = data.substr(bodyStart, contentLength);
- }
- else
- {
- message.body = data.substring(bodyStart);
- }
+ message.body = data.substr(bodyStart, contentLength);
+ } else {
+ message.body = data.substring(bodyStart);
+ }
- return message;
+ return message;
};
/**
* Extract and parse every header of a SIP message.
*/
-function getHeader(data, headerStart)
-{
- // 'start' position of the header.
- let start = headerStart;
- // 'end' position of the header.
- let end = 0;
- // 'partial end' position of the header.
- let partialEnd = 0;
+function getHeader(data, headerStart) {
+ // 'start' position of the header.
+ let start = headerStart;
+ // 'end' position of the header.
+ let end = 0;
+ // 'partial end' position of the header.
+ let partialEnd = 0;
- // End of message.
- if (data.substring(start, start + 2).match(/(^\r\n)/))
- {
- return -2;
- }
+ // End of message.
+ if (data.substring(start, start + 2).match(/(^\r\n)/)) {
+ return -2;
+ }
- while (end === 0)
- {
- // Partial End of Header.
- partialEnd = data.indexOf('\r\n', start);
+ while (end === 0) {
+ // Partial End of Header.
+ partialEnd = data.indexOf('\r\n', start);
- // 'indexOf' returns -1 if the value to be found never occurs.
- if (partialEnd === -1)
- {
- return partialEnd;
- }
+ // 'indexOf' returns -1 if the value to be found never occurs.
+ if (partialEnd === -1) {
+ return partialEnd;
+ }
- if (!data.substring(partialEnd + 2, partialEnd + 4).match(/(^\r\n)/) && data.charAt(partialEnd + 2).match(/(^\s+)/))
- {
- // Not the end of the message. Continue from the next position.
- start = partialEnd + 2;
- }
- else
- {
- end = partialEnd;
- }
- }
+ if (
+ !data.substring(partialEnd + 2, partialEnd + 4).match(/(^\r\n)/) &&
+ data.charAt(partialEnd + 2).match(/(^\s+)/)
+ ) {
+ // Not the end of the message. Continue from the next position.
+ start = partialEnd + 2;
+ } else {
+ end = partialEnd;
+ }
+ }
- return end;
+ return end;
}
-function parseHeader(message, data, headerStart, headerEnd)
-{
- let parsed;
- const hcolonIndex = data.indexOf(':', headerStart);
- const headerName = data.substring(headerStart, hcolonIndex).trim();
- const headerValue = data.substring(hcolonIndex + 1, headerEnd).trim();
+function parseHeader(message, data, headerStart, headerEnd) {
+ let parsed;
+ const hcolonIndex = data.indexOf(':', headerStart);
+ const headerName = data.substring(headerStart, hcolonIndex).trim();
+ const headerValue = data.substring(hcolonIndex + 1, headerEnd).trim();
- // If header-field is well-known, parse it.
- switch (headerName.toLowerCase())
- {
- case 'via':
- case 'v':
- message.addHeader('via', headerValue);
- if (message.getHeaders('via').length === 1)
- {
- parsed = message.parseHeader('Via');
- if (parsed)
- {
- message.via = parsed;
- message.via_branch = parsed.branch;
- }
- }
- else
- {
- parsed = 0;
- }
- break;
- case 'from':
- case 'f':
- message.setHeader('from', headerValue);
- parsed = message.parseHeader('from');
- if (parsed)
- {
- message.from = parsed;
- message.from_tag = parsed.getParam('tag');
- }
- break;
- case 'to':
- case 't':
- message.setHeader('to', headerValue);
- parsed = message.parseHeader('to');
- if (parsed)
- {
- message.to = parsed;
- message.to_tag = parsed.getParam('tag');
- }
- break;
- case 'record-route':
- parsed = Grammar.parse(headerValue, 'Record_Route');
+ // If header-field is well-known, parse it.
+ switch (headerName.toLowerCase()) {
+ case 'via':
+ case 'v': {
+ message.addHeader('via', headerValue);
+ if (message.getHeaders('via').length === 1) {
+ parsed = message.parseHeader('Via');
+ if (parsed) {
+ message.via = parsed;
+ message.via_branch = parsed.branch;
+ }
+ } else {
+ parsed = 0;
+ }
+ break;
+ }
+ case 'from':
+ case 'f': {
+ message.setHeader('from', headerValue);
+ parsed = message.parseHeader('from');
+ if (parsed) {
+ message.from = parsed;
+ message.from_tag = parsed.getParam('tag');
+ }
+ break;
+ }
+ case 'to':
+ case 't': {
+ message.setHeader('to', headerValue);
+ parsed = message.parseHeader('to');
+ if (parsed) {
+ message.to = parsed;
+ message.to_tag = parsed.getParam('tag');
+ }
+ break;
+ }
+ case 'record-route': {
+ parsed = Grammar.parse(headerValue, 'Record_Route');
- if (parsed === -1)
- {
- parsed = undefined;
- }
- else
- {
- for (const header of parsed)
- {
- message.addHeader('record-route', headerValue.substring(header.possition, header.offset));
- message.headers['Record-Route'][message.getHeaders('record-route').length - 1].parsed = header.parsed;
- }
- }
- break;
- case 'call-id':
- case 'i':
- message.setHeader('call-id', headerValue);
- parsed = message.parseHeader('call-id');
- if (parsed)
- {
- message.call_id = headerValue;
- }
- break;
- case 'contact':
- case 'm':
- parsed = Grammar.parse(headerValue, 'Contact');
+ if (parsed === -1) {
+ parsed = undefined;
+ } else {
+ for (const header of parsed) {
+ message.addHeader(
+ 'record-route',
+ headerValue.substring(header.possition, header.offset)
+ );
+ message.headers['Record-Route'][
+ message.getHeaders('record-route').length - 1
+ ].parsed = header.parsed;
+ }
+ }
+ break;
+ }
+ case 'call-id':
+ case 'i': {
+ message.setHeader('call-id', headerValue);
+ parsed = message.parseHeader('call-id');
+ if (parsed) {
+ message.call_id = headerValue;
+ }
+ break;
+ }
+ case 'contact':
+ case 'm': {
+ parsed = Grammar.parse(headerValue, 'Contact');
- if (parsed === -1)
- {
- parsed = undefined;
- }
- else
- {
- for (const header of parsed)
- {
- message.addHeader('contact', headerValue.substring(header.possition, header.offset));
- message.headers.Contact[message.getHeaders('contact').length - 1].parsed = header.parsed;
- }
- }
- break;
- case 'content-length':
- case 'l':
- message.setHeader('content-length', headerValue);
- parsed = message.parseHeader('content-length');
- break;
- case 'content-type':
- case 'c':
- message.setHeader('content-type', headerValue);
- parsed = message.parseHeader('content-type');
- break;
- case 'cseq':
- message.setHeader('cseq', headerValue);
- parsed = message.parseHeader('cseq');
- if (parsed)
- {
- message.cseq = parsed.value;
- }
- if (message instanceof SIPMessage.IncomingResponse)
- {
- message.method = parsed.method;
- }
- break;
- case 'max-forwards':
- message.setHeader('max-forwards', headerValue);
- parsed = message.parseHeader('max-forwards');
- break;
- case 'www-authenticate':
- message.setHeader('www-authenticate', headerValue);
- parsed = message.parseHeader('www-authenticate');
- break;
- case 'proxy-authenticate':
- message.setHeader('proxy-authenticate', headerValue);
- parsed = message.parseHeader('proxy-authenticate');
- break;
- case 'session-expires':
- case 'x':
- message.setHeader('session-expires', headerValue);
- parsed = message.parseHeader('session-expires');
- if (parsed)
- {
- message.session_expires = parsed.expires;
- message.session_expires_refresher = parsed.refresher;
- }
- break;
- case 'refer-to':
- case 'r':
- message.setHeader('refer-to', headerValue);
- parsed = message.parseHeader('refer-to');
- if (parsed)
- {
- message.refer_to = parsed;
- }
- break;
- case 'replaces':
- message.setHeader('replaces', headerValue);
- parsed = message.parseHeader('replaces');
- if (parsed)
- {
- message.replaces = parsed;
- }
- break;
- case 'event':
- case 'o':
- message.setHeader('event', headerValue);
- parsed = message.parseHeader('event');
- if (parsed)
- {
- message.event = parsed;
- }
- break;
- default:
- // Do not parse this header.
- message.addHeader(headerName, headerValue);
- parsed = 0;
- }
+ if (parsed === -1) {
+ parsed = undefined;
+ } else {
+ for (const header of parsed) {
+ message.addHeader(
+ 'contact',
+ headerValue.substring(header.possition, header.offset)
+ );
+ message.headers.Contact[
+ message.getHeaders('contact').length - 1
+ ].parsed = header.parsed;
+ }
+ }
+ break;
+ }
+ case 'content-length':
+ case 'l': {
+ message.setHeader('content-length', headerValue);
+ parsed = message.parseHeader('content-length');
+ break;
+ }
+ case 'content-type':
+ case 'c': {
+ message.setHeader('content-type', headerValue);
+ parsed = message.parseHeader('content-type');
+ break;
+ }
+ case 'cseq': {
+ message.setHeader('cseq', headerValue);
+ parsed = message.parseHeader('cseq');
+ if (parsed) {
+ message.cseq = parsed.value;
+ }
+ if (message instanceof SIPMessage.IncomingResponse) {
+ message.method = parsed.method;
+ }
+ break;
+ }
+ case 'max-forwards': {
+ message.setHeader('max-forwards', headerValue);
+ parsed = message.parseHeader('max-forwards');
+ break;
+ }
+ case 'www-authenticate': {
+ message.setHeader('www-authenticate', headerValue);
+ parsed = message.parseHeader('www-authenticate');
+ break;
+ }
+ case 'proxy-authenticate': {
+ message.setHeader('proxy-authenticate', headerValue);
+ parsed = message.parseHeader('proxy-authenticate');
+ break;
+ }
+ case 'session-expires':
+ case 'x': {
+ message.setHeader('session-expires', headerValue);
+ parsed = message.parseHeader('session-expires');
+ if (parsed) {
+ message.session_expires = parsed.expires;
+ message.session_expires_refresher = parsed.refresher;
+ }
+ break;
+ }
+ case 'refer-to':
+ case 'r': {
+ message.setHeader('refer-to', headerValue);
+ parsed = message.parseHeader('refer-to');
+ if (parsed) {
+ message.refer_to = parsed;
+ }
+ break;
+ }
+ case 'replaces': {
+ message.setHeader('replaces', headerValue);
+ parsed = message.parseHeader('replaces');
+ if (parsed) {
+ message.replaces = parsed;
+ }
+ break;
+ }
+ case 'event':
+ case 'o': {
+ message.setHeader('event', headerValue);
+ parsed = message.parseHeader('event');
+ if (parsed) {
+ message.event = parsed;
+ }
+ break;
+ }
+ default: {
+ // Do not parse this header.
+ message.addHeader(headerName, headerValue);
+ parsed = 0;
+ }
+ }
- if (parsed === undefined)
- {
- return {
- error : `error parsing header "${headerName}"`
- };
- }
- else
- {
- return true;
- }
+ if (parsed === undefined) {
+ return {
+ error: `error parsing header "${headerName}"`,
+ };
+ } else {
+ return true;
+ }
}
diff --git a/src/RTCSession.d.ts b/src/RTCSession.d.ts
index fb68910..4e03d92 100644
--- a/src/RTCSession.d.ts
+++ b/src/RTCSession.d.ts
@@ -1,183 +1,190 @@
-import {EventEmitter} from 'events'
-
-import {IncomingRequest, IncomingResponse, OutgoingRequest} from './SIPMessage'
-import {NameAddrHeader} from './NameAddrHeader'
-import {URI} from './URI'
-import {UA} from './UA'
-import {causes, DTMF_TRANSPORT} from './Constants'
+import { EventEmitter } from 'events';
+
+import {
+ IncomingRequest,
+ IncomingResponse,
+ OutgoingRequest,
+} from './SIPMessage';
+import { NameAddrHeader } from './NameAddrHeader';
+import { URI } from './URI';
+import { UA } from './UA';
+import { causes, DTMF_TRANSPORT } from './Constants';
interface RTCPeerConnectionDeprecated extends RTCPeerConnection {
- /**
- * @deprecated
- * @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/getRemoteStreams
- */
- getRemoteStreams(): MediaStream[];
+ /**
+ * @deprecated
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/getRemoteStreams
+ */
+ getRemoteStreams(): MediaStream[];
}
export declare enum SessionDirection {
- INCOMING = 'incoming',
- OUTGOING = 'outgoing',
+ INCOMING = 'incoming',
+ OUTGOING = 'outgoing',
}
export declare enum Originator {
- LOCAL = 'local',
- REMOTE = 'remote',
- SYSTEM = 'system',
+ LOCAL = 'local',
+ REMOTE = 'remote',
+ SYSTEM = 'system',
}
// options
export interface ExtraHeaders {
- extraHeaders?: string[];
+ extraHeaders?: string[];
}
export interface AnswerOptions extends ExtraHeaders {
- mediaConstraints?: MediaStreamConstraints;
- mediaStream?: MediaStream;
- pcConfig?: RTCConfiguration;
- rtcConstraints?: object;
- rtcAnswerConstraints?: RTCOfferOptions;
- rtcOfferConstraints?: RTCOfferOptions;
- sessionTimersExpires?: number;
+ mediaConstraints?: MediaStreamConstraints;
+ mediaStream?: MediaStream;
+ pcConfig?: RTCConfiguration;
+ rtcConstraints?: object;
+ rtcAnswerConstraints?: RTCOfferOptions;
+ rtcOfferConstraints?: RTCOfferOptions;
+ sessionTimersExpires?: number;
}
export interface RejectOptions extends ExtraHeaders {
- status_code?: number;
- reason_phrase?: string;
+ status_code?: number;
+ reason_phrase?: string;
}
export interface TerminateOptions extends RejectOptions {
- body?: string;
- cause?: causes | string;
+ body?: string;
+ cause?: causes | string;
}
export interface ReferOptions extends ExtraHeaders {
- eventHandlers?: any;
- replaces?: RTCSession;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ eventHandlers?: any;
+ replaces?: RTCSession;
}
export interface OnHoldResult {
- local: boolean;
- remote: boolean;
+ local: boolean;
+ remote: boolean;
}
export interface DTMFOptions extends ExtraHeaders {
- duration?: number;
- interToneGap?: number;
- transportType?: DTMF_TRANSPORT;
+ duration?: number;
+ interToneGap?: number;
+ transportType?: DTMF_TRANSPORT;
}
export interface HoldOptions extends ExtraHeaders {
- useUpdate?: boolean;
+ useUpdate?: boolean;
}
export interface RenegotiateOptions extends HoldOptions {
- rtcOfferConstraints?: RTCOfferOptions;
+ rtcOfferConstraints?: RTCOfferOptions;
}
// events
export interface DTMF extends EventEmitter {
- tone: string;
- duration: number;
+ tone: string;
+ duration: number;
}
export interface Info extends EventEmitter {
- contentType: string;
- body: string;
+ contentType: string;
+ body: string;
}
export interface PeerConnectionEvent {
- peerconnection: RTCPeerConnectionDeprecated;
+ peerconnection: RTCPeerConnectionDeprecated;
}
export interface ConnectingEvent {
- request: IncomingRequest | OutgoingRequest;
+ request: IncomingRequest | OutgoingRequest;
}
export interface SendingEvent {
- request: OutgoingRequest
+ request: OutgoingRequest;
}
export interface IncomingEvent {
- originator: Originator.LOCAL;
+ originator: Originator.LOCAL;
}
export interface EndEvent {
- originator: Originator;
- message: IncomingRequest | IncomingResponse;
- cause: string;
+ originator: Originator;
+ message: IncomingRequest | IncomingResponse;
+ cause: string;
}
export interface IncomingDTMFEvent {
- originator: Originator.REMOTE;
- dtmf: DTMF;
- request: IncomingRequest;
+ originator: Originator.REMOTE;
+ dtmf: DTMF;
+ request: IncomingRequest;
}
export interface OutgoingDTMFEvent {
- originator: Originator.LOCAL;
- dtmf: DTMF;
- request: OutgoingRequest;
+ originator: Originator.LOCAL;
+ dtmf: DTMF;
+ request: OutgoingRequest;
}
export interface IncomingInfoEvent {
- originator: Originator.REMOTE;
- info: Info;
- request: IncomingRequest;
+ originator: Originator.REMOTE;
+ info: Info;
+ request: IncomingRequest;
}
export interface OutgoingInfoEvent {
- originator: Originator.LOCAL;
- info: Info;
- request: OutgoingRequest;
+ originator: Originator.LOCAL;
+ info: Info;
+ request: OutgoingRequest;
}
export interface HoldEvent {
- originator: Originator
+ originator: Originator;
}
export interface ReInviteEvent {
- request: IncomingRequest;
- callback?: VoidFunction;
- reject: (options?: RejectOptions) => void;
+ request: IncomingRequest;
+ callback?: VoidFunction;
+ reject: (options?: RejectOptions) => void;
}
export interface ReferEvent {
- request: IncomingRequest;
- accept: Function;
- reject: VoidFunction;
+ request: IncomingRequest;
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
+ accept: Function;
+ reject: VoidFunction;
}
export interface SDPEvent {
- originator: Originator;
- type: string;
- sdp: string;
+ originator: Originator;
+ type: string;
+ sdp: string;
}
export interface IceCandidateEvent {
- candidate: RTCIceCandidate;
- ready: VoidFunction;
+ candidate: RTCIceCandidate;
+ ready: VoidFunction;
}
export interface OutgoingEvent {
- originator: Originator.REMOTE;
- response: IncomingResponse;
+ originator: Originator.REMOTE;
+ response: IncomingResponse;
}
export interface OutgoingAckEvent {
- originator: Originator.LOCAL;
+ originator: Originator.LOCAL;
}
export interface IncomingAckEvent {
- originator: Originator.REMOTE;
- ack: IncomingRequest;
+ originator: Originator.REMOTE;
+ ack: IncomingRequest;
}
export interface MediaStreamTypes {
- audio?: boolean;
- video?: boolean;
+ audio?: boolean;
+ video?: boolean;
}
// listener
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type GenericErrorListener = (error: any) => void;
export type PeerConnectionListener = (event: PeerConnectionEvent) => void;
export type ConnectingListener = (event: ConnectingEvent) => void;
@@ -187,7 +194,9 @@ export type OutgoingListener = (event: OutgoingEvent) => void;
export type IncomingConfirmedListener = (event: IncomingAckEvent) => void;
export type OutgoingConfirmedListener = (event: OutgoingAckEvent) => void;
export type CallListener = IncomingListener | OutgoingListener;
-export type ConfirmedListener = IncomingConfirmedListener | OutgoingConfirmedListener;
+export type ConfirmedListener =
+ | IncomingConfirmedListener
+ | OutgoingConfirmedListener;
export type EndListener = (event: EndEvent) => void;
export type IncomingDTMFListener = (event: IncomingDTMFEvent) => void;
export type OutgoingDTMFListener = (event: OutgoingDTMFEvent) => void;
@@ -204,107 +213,112 @@ export type SDPListener = (event: SDPEvent) => void;
export type IceCandidateListener = (event: IceCandidateEvent) => void;
export interface RTCSessionEventMap {
- 'peerconnection': PeerConnectionListener;
- 'connecting': ConnectingListener;
- 'sending': SendingListener;
- 'progress': CallListener;
- 'accepted': CallListener;
- 'confirmed': ConfirmedListener;
- 'ended': EndListener;
- 'failed': EndListener;
- 'newDTMF': DTMFListener;
- 'newInfo': InfoListener;
- 'hold': HoldListener;
- 'unhold': HoldListener;
- 'muted': MuteListener;
- 'unmuted': MuteListener;
- 'reinvite': ReInviteListener;
- 'update': UpdateListener;
- 'refer': ReferListener;
- 'replaces': ReferListener;
- 'sdp': SDPListener;
- 'icecandidate': IceCandidateListener;
- 'getusermediafailed': GenericErrorListener;
- 'peerconnection:createofferfailed': GenericErrorListener;
- 'peerconnection:createanswerfailed': GenericErrorListener;
- 'peerconnection:setlocaldescriptionfailed': GenericErrorListener;
- 'peerconnection:setremotedescriptionfailed': GenericErrorListener;
+ peerconnection: PeerConnectionListener;
+ connecting: ConnectingListener;
+ sending: SendingListener;
+ progress: CallListener;
+ accepted: CallListener;
+ confirmed: ConfirmedListener;
+ ended: EndListener;
+ failed: EndListener;
+ newDTMF: DTMFListener;
+ newInfo: InfoListener;
+ hold: HoldListener;
+ unhold: HoldListener;
+ muted: MuteListener;
+ unmuted: MuteListener;
+ reinvite: ReInviteListener;
+ update: UpdateListener;
+ refer: ReferListener;
+ replaces: ReferListener;
+ sdp: SDPListener;
+ icecandidate: IceCandidateListener;
+ getusermediafailed: GenericErrorListener;
+ 'peerconnection:createofferfailed': GenericErrorListener;
+ 'peerconnection:createanswerfailed': GenericErrorListener;
+ 'peerconnection:setlocaldescriptionfailed': GenericErrorListener;
+ 'peerconnection:setremotedescriptionfailed': GenericErrorListener;
}
declare enum SessionStatus {
- STATUS_NULL = 0,
- STATUS_INVITE_SENT = 1,
- STATUS_1XX_RECEIVED = 2,
- STATUS_INVITE_RECEIVED = 3,
- STATUS_WAITING_FOR_ANSWER = 4,
- STATUS_ANSWERED = 5,
- STATUS_WAITING_FOR_ACK = 6,
- STATUS_CANCELED = 7,
- STATUS_TERMINATED = 8,
- STATUS_CONFIRMED = 9
+ STATUS_NULL = 0,
+ STATUS_INVITE_SENT = 1,
+ STATUS_1XX_RECEIVED = 2,
+ STATUS_INVITE_RECEIVED = 3,
+ STATUS_WAITING_FOR_ANSWER = 4,
+ STATUS_ANSWERED = 5,
+ STATUS_WAITING_FOR_ACK = 6,
+ STATUS_CANCELED = 7,
+ STATUS_TERMINATED = 8,
+ STATUS_CONFIRMED = 9,
}
export class RTCSession extends EventEmitter {
- constructor (ua: UA);
+ constructor(ua: UA);
- static get C(): typeof SessionStatus;
+ static get C(): typeof SessionStatus;
- get C(): typeof SessionStatus;
+ get C(): typeof SessionStatus;
- get causes(): typeof causes;
+ get causes(): typeof causes;
- get id(): string;
+ get id(): string;
- set data(_data: any);
- get data(): any;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ set data(_data: any);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ get data(): any;
- get connection(): RTCPeerConnectionDeprecated;
+ get connection(): RTCPeerConnectionDeprecated;
- get contact(): string;
+ get contact(): string;
- get direction(): SessionDirection;
+ get direction(): SessionDirection;
- get local_identity(): NameAddrHeader;
+ get local_identity(): NameAddrHeader;
- get remote_identity(): NameAddrHeader;
+ get remote_identity(): NameAddrHeader;
- get start_time(): Date;
+ get start_time(): Date;
- get end_time(): Date;
+ get end_time(): Date;
- get status(): SessionStatus;
+ get status(): SessionStatus;
- isInProgress(): boolean;
+ isInProgress(): boolean;
- isEstablished(): boolean;
+ isEstablished(): boolean;
- isEnded(): boolean;
+ isEnded(): boolean;
- isReadyToReOffer(): boolean;
+ isReadyToReOffer(): boolean;
- answer(options?: AnswerOptions): void;
+ answer(options?: AnswerOptions): void;
- terminate(options?: TerminateOptions): void;
+ terminate(options?: TerminateOptions): void;
- sendDTMF(tones: string | number, options?: DTMFOptions): void;
+ sendDTMF(tones: string | number, options?: DTMFOptions): void;
- sendInfo(contentType: string, body?: string, options?: ExtraHeaders): void;
+ sendInfo(contentType: string, body?: string, options?: ExtraHeaders): void;
- hold(options?: HoldOptions, done?: VoidFunction): boolean;
+ hold(options?: HoldOptions, done?: VoidFunction): boolean;
- unhold(options?: HoldOptions, done?: VoidFunction): boolean;
+ unhold(options?: HoldOptions, done?: VoidFunction): boolean;
- renegotiate(options?: RenegotiateOptions, done?: VoidFunction): boolean;
+ renegotiate(options?: RenegotiateOptions, done?: VoidFunction): boolean;
- isOnHold(): OnHoldResult;
+ isOnHold(): OnHoldResult;
- mute(options?: MediaStreamTypes): void;
+ mute(options?: MediaStreamTypes): void;
- unmute(options?: MediaStreamTypes): void;
+ unmute(options?: MediaStreamTypes): void;
- isMuted(): MediaStreamTypes;
+ isMuted(): MediaStreamTypes;
- refer(target: string | URI, options?: ReferOptions): void;
+ refer(target: string | URI, options?: ReferOptions): void;
- on<T extends keyof RTCSessionEventMap>(type: T, listener: RTCSessionEventMap[T]): this;
+ on<T extends keyof RTCSessionEventMap>(
+ type: T,
+ listener: RTCSessionEventMap[T]
+ ): this;
}
diff --git a/src/RTCSession.js b/src/RTCSession.js
index 1b9e6eb..7ac7e76 100644
--- a/src/RTCSession.js
+++ b/src/RTCSession.js
@@ -1,5 +1,5 @@
-// eslint-disable-next-line no-redeclare
/* globals RTCPeerConnection: false, RTCSessionDescription: false */
+/* eslint-disable no-invalid-this */
const EventEmitter = require('events').EventEmitter;
const sdp_transform = require('sdp-transform');
@@ -21,3630 +21,3346 @@ const URI = require('./URI');
const logger = new Logger('RTCSession');
const C = {
- // RTCSession states.
- STATUS_NULL : 0,
- STATUS_INVITE_SENT : 1,
- STATUS_1XX_RECEIVED : 2,
- STATUS_INVITE_RECEIVED : 3,
- STATUS_WAITING_FOR_ANSWER : 4,
- STATUS_ANSWERED : 5,
- STATUS_WAITING_FOR_ACK : 6,
- STATUS_CANCELED : 7,
- STATUS_TERMINATED : 8,
- STATUS_CONFIRMED : 9
+ // RTCSession states.
+ STATUS_NULL: 0,
+ STATUS_INVITE_SENT: 1,
+ STATUS_1XX_RECEIVED: 2,
+ STATUS_INVITE_RECEIVED: 3,
+ STATUS_WAITING_FOR_ANSWER: 4,
+ STATUS_ANSWERED: 5,
+ STATUS_WAITING_FOR_ACK: 6,
+ STATUS_CANCELED: 7,
+ STATUS_TERMINATED: 8,
+ STATUS_CONFIRMED: 9,
};
/**
* Local variables.
*/
-const holdMediaTypes = [ 'audio', 'video' ];
-
-module.exports = class RTCSession extends EventEmitter
-{
- /**
- * Expose C object.
- */
- static get C()
- {
- return C;
- }
-
- constructor(ua)
- {
- logger.debug('new');
-
- super();
-
- this._id = null;
- this._ua = ua;
- this._status = C.STATUS_NULL;
- this._dialog = null;
- this._earlyDialogs = {};
- this._contact = null;
- this._from_tag = null;
- this._to_tag = null;
-
- // The RTCPeerConnection instance (public attribute).
- this._connection = null;
-
- // Prevent races on serial PeerConnction operations.
- this._connectionPromiseQueue = Promise.resolve();
-
- // Incoming/Outgoing request being currently processed.
- this._request = null;
-
- // Cancel state for initial outgoing request.
- this._is_canceled = false;
- this._cancel_reason = '';
-
- // RTCSession confirmation flag.
- this._is_confirmed = false;
-
- // Is late SDP being negotiated.
- this._late_sdp = false;
-
- // Default rtcOfferConstraints and rtcAnswerConstrainsts (passed in connect() or answer()).
- this._rtcOfferConstraints = null;
- this._rtcAnswerConstraints = null;
-
- // Local MediaStream.
- this._localMediaStream = null;
- this._localMediaStreamLocallyGenerated = false;
-
- // Flag to indicate PeerConnection ready for new actions.
- this._rtcReady = true;
-
- // Flag to indicate ICE candidate gathering is finished even if iceGatheringState is not yet 'complete'.
- this._iceReady = false;
-
- // SIP Timers.
- this._timers = {
- ackTimer : null,
- expiresTimer : null,
- invite2xxTimer : null,
- userNoAnswerTimer : null
- };
-
- // Session info.
- this._direction = null;
- this._local_identity = null;
- this._remote_identity = null;
- this._start_time = null;
- this._end_time = null;
- this._tones = null;
-
- // Mute/Hold state.
- this._audioMuted = false;
- this._videoMuted = false;
- this._localHold = false;
- this._remoteHold = false;
-
- // Session Timers (RFC 4028).
- this._sessionTimers = {
- enabled : this._ua.configuration.session_timers,
- refreshMethod : this._ua.configuration.session_timers_refresh_method,
- defaultExpires : JsSIP_C.SESSION_EXPIRES,
- currentExpires : null,
- running : false,
- refresher : false,
- timer : null // A setTimeout.
- };
-
- // Map of ReferSubscriber instances indexed by the REFER's CSeq number.
- this._referSubscribers = {};
-
- // Custom session empty object for high level use.
- this._data = {};
- }
-
- /**
- * User API
- */
-
- // Expose RTCSession constants as a property of the RTCSession instance.
- get C()
- {
- return C;
- }
-
- // Expose session failed/ended causes as a property of the RTCSession instance.
- get causes()
- {
- return JsSIP_C.causes;
- }
-
- get id()
- {
- return this._id;
- }
-
- get connection()
- {
- return this._connection;
- }
-
- get contact()
- {
- return this._contact;
- }
-
- get direction()
- {
- return this._direction;
- }
-
- get local_identity()
- {
- return this._local_identity;
- }
-
- get remote_identity()
- {
- return this._remote_identity;
- }
-
- get start_time()
- {
- return this._start_time;
- }
-
- get end_time()
- {
- return this._end_time;
- }
-
- get data()
- {
- return this._data;
- }
-
- set data(_data)
- {
- this._data = _data;
- }
-
- get status()
- {
- return this._status;
- }
-
- isInProgress()
- {
- switch (this._status)
- {
- case C.STATUS_NULL:
- case C.STATUS_INVITE_SENT:
- case C.STATUS_1XX_RECEIVED:
- case C.STATUS_INVITE_RECEIVED:
- case C.STATUS_WAITING_FOR_ANSWER:
- return true;
- default:
- return false;
- }
- }
-
- isEstablished()
- {
- switch (this._status)
- {
- case C.STATUS_ANSWERED:
- case C.STATUS_WAITING_FOR_ACK:
- case C.STATUS_CONFIRMED:
- return true;
- default:
- return false;
- }
- }
-
- isEnded()
- {
- switch (this._status)
- {
- case C.STATUS_CANCELED:
- case C.STATUS_TERMINATED:
- return true;
- default:
- return false;
- }
- }
-
- isMuted()
- {
- return {
- audio : this._audioMuted,
- video : this._videoMuted
- };
- }
-
- isOnHold()
- {
- return {
- local : this._localHold,
- remote : this._remoteHold
- };
- }
-
- connect(target, options = {}, initCallback)
- {
- logger.debug('connect()');
-
- const originalTarget = target;
- const eventHandlers = Utils.cloneObject(options.eventHandlers);
- const extraHeaders = Utils.cloneArray(options.extraHeaders);
- const mediaConstraints = Utils.cloneObject(options.mediaConstraints, {
- audio : true,
- video : true
- });
- const mediaStream = options.mediaStream || null;
- const pcConfig = Utils.cloneObject(options.pcConfig, { iceServers: [] });
- const rtcConstraints = options.rtcConstraints || null;
- const rtcOfferConstraints = options.rtcOfferConstraints || null;
-
- this._rtcOfferConstraints = rtcOfferConstraints;
- this._rtcAnswerConstraints = options.rtcAnswerConstraints || null;
-
- this._data = options.data || this._data;
-
- // Check target.
- if (target === undefined)
- {
- throw new TypeError('Not enough arguments');
- }
-
- // Check Session Status.
- if (this._status !== C.STATUS_NULL)
- {
- throw new Exceptions.InvalidStateError(this._status);
- }
-
- // Check WebRTC support.
- if (!window.RTCPeerConnection)
- {
- throw new Exceptions.NotSupportedError('WebRTC not supported');
- }
-
- // Check target validity.
- target = this._ua.normalizeTarget(target);
- if (!target)
- {
- throw new TypeError(`Invalid target: ${originalTarget}`);
- }
-
- // Session Timers.
- if (this._sessionTimers.enabled)
- {
- if (Utils.isDecimal(options.sessionTimersExpires))
- {
- if (options.sessionTimersExpires >= JsSIP_C.MIN_SESSION_EXPIRES)
- {
- this._sessionTimers.defaultExpires = options.sessionTimersExpires;
- }
- else
- {
- this._sessionTimers.defaultExpires = JsSIP_C.SESSION_EXPIRES;
- }
- }
- }
-
- // Set event handlers.
- for (const event in eventHandlers)
- {
- if (Object.prototype.hasOwnProperty.call(eventHandlers, event))
- {
- this.on(event, eventHandlers[event]);
- }
- }
-
- // Session parameter initialization.
- this._from_tag = Utils.newTag();
-
- // Set anonymous property.
- const anonymous = options.anonymous || false;
-
- const requestParams = { from_tag: this._from_tag };
-
- this._contact = this._ua.contact.toString({
- anonymous,
- outbound : true
- });
-
- if (anonymous)
- {
- requestParams.from_display_name = 'Anonymous';
- requestParams.from_uri = new URI('sip', 'anonymous', 'anonymous.invalid');
-
- extraHeaders.push(`P-Preferred-Identity: ${this._ua.configuration.uri.toString()}`);
- extraHeaders.push('Privacy: id');
- }
- else if (options.fromUserName)
- {
- requestParams.from_uri = new URI('sip', options.fromUserName, this._ua.configuration.uri.host);
-
- extraHeaders.push(`P-Preferred-Identity: ${this._ua.configuration.uri.toString()}`);
- }
-
- if (options.fromDisplayName)
- {
- requestParams.from_display_name = options.fromDisplayName;
- }
-
- extraHeaders.push(`Contact: ${this._contact}`);
- extraHeaders.push('Content-Type: application/sdp');
- if (this._sessionTimers.enabled)
- {
- extraHeaders.push(`Session-Expires: ${this._sessionTimers.defaultExpires}${this._ua.configuration.session_timers_force_refresher ? ';refresher=uac' : ''}`);
- }
-
- this._request = new SIPMessage.InitialOutgoingInviteRequest(
- target, this._ua, requestParams, extraHeaders);
-
- this._id = this._request.call_id + this._from_tag;
-
- // Create a new RTCPeerConnection instance.
- this._createRTCConnection(pcConfig, rtcConstraints);
-
- // Set internal properties.
- this._direction = 'outgoing';
- this._local_identity = this._request.from;
- this._remote_identity = this._request.to;
-
- // User explicitly provided a newRTCSession callback for this session.
- if (initCallback)
- {
- initCallback(this);
- }
-
- this._newRTCSession('local', this._request);
-
- this._sendInitialRequest(mediaConstraints, rtcOfferConstraints, mediaStream);
- }
-
- init_incoming(request, initCallback)
- {
- logger.debug('init_incoming()');
-
- let expires;
- const contentType = request.hasHeader('Content-Type') ?
- request.getHeader('Content-Type').toLowerCase() : undefined;
-
- // Check body and content type.
- if (request.body && (contentType !== 'application/sdp'))
- {
- request.reply(415);
-
- return;
- }
-
- // Session parameter initialization.
- this._status = C.STATUS_INVITE_RECEIVED;
- this._from_tag = request.from_tag;
- this._id = request.call_id + this._from_tag;
- this._request = request;
- this._contact = this._ua.contact.toString();
-
- // Get the Expires header value if exists.
- if (request.hasHeader('expires'))
- {
- expires = request.getHeader('expires') * 1000;
- }
-
- /* Set the to_tag before
- * replying a response code that will create a dialog.
- */
- request.to_tag = Utils.newTag();
-
- // An error on dialog creation will fire 'failed' event.
- if (!this._createDialog(request, 'UAS', true))
- {
- request.reply(500, 'Missing Contact header field');
-
- return;
- }
-
- if (request.body)
- {
- this._late_sdp = false;
- }
- else
- {
- this._late_sdp = true;
- }
-
- this._status = C.STATUS_WAITING_FOR_ANSWER;
-
- // Set userNoAnswerTimer.
- this._timers.userNoAnswerTimer = setTimeout(() =>
- {
- request.reply(408);
- this._failed('local', null, JsSIP_C.causes.NO_ANSWER);
- }, this._ua.configuration.no_answer_timeout
- );
-
- /* Set expiresTimer
- * RFC3261 13.3.1
- */
- if (expires)
- {
- this._timers.expiresTimer = setTimeout(() =>
- {
- if (this._status === C.STATUS_WAITING_FOR_ANSWER)
- {
- request.reply(487);
- this._failed('system', null, JsSIP_C.causes.EXPIRES);
- }
- }, expires
- );
- }
-
- // Set internal properties.
- this._direction = 'incoming';
- this._local_identity = request.to;
- this._remote_identity = request.from;
-
- // A init callback was specifically defined.
- if (initCallback)
- {
- initCallback(this);
- }
-
- // Fire 'newRTCSession' event.
- this._newRTCSession('remote', request);
-
- // The user may have rejected the call in the 'newRTCSession' event.
- if (this._status === C.STATUS_TERMINATED)
- {
- return;
- }
-
- // Reply 180.
- request.reply(180, null, [ `Contact: ${this._contact}` ]);
-
- // Fire 'progress' event.
- // TODO: Document that 'response' field in 'progress' event is null for incoming calls.
- this._progress('local', null);
- }
-
- /**
- * Answer the call.
- */
- answer(options = {})
- {
- logger.debug('answer()');
-
- const request = this._request;
- const extraHeaders = Utils.cloneArray(options.extraHeaders);
- const mediaConstraints = Utils.cloneObject(options.mediaConstraints);
- const mediaStream = options.mediaStream || null;
- const pcConfig = Utils.cloneObject(options.pcConfig, { iceServers: [] });
- const rtcConstraints = options.rtcConstraints || null;
- const rtcAnswerConstraints = options.rtcAnswerConstraints || null;
- const rtcOfferConstraints = Utils.cloneObject(options.rtcOfferConstraints);
-
- let tracks;
- let peerHasAudioLine = false;
- let peerHasVideoLine = false;
- let peerOffersFullAudio = false;
- let peerOffersFullVideo = false;
-
- this._rtcAnswerConstraints = rtcAnswerConstraints;
- this._rtcOfferConstraints = options.rtcOfferConstraints || null;
-
- this._data = options.data || this._data;
-
- // Check Session Direction and Status.
- if (this._direction !== 'incoming')
- {
- throw new Exceptions.NotSupportedError('"answer" not supported for outgoing RTCSession');
- }
-
- // Check Session status.
- if (this._status !== C.STATUS_WAITING_FOR_ANSWER)
- {
- throw new Exceptions.InvalidStateError(this._status);
- }
-
- // Session Timers.
- if (this._sessionTimers.enabled)
- {
- if (Utils.isDecimal(options.sessionTimersExpires))
- {
- if (options.sessionTimersExpires >= JsSIP_C.MIN_SESSION_EXPIRES)
- {
- this._sessionTimers.defaultExpires = options.sessionTimersExpires;
- }
- else
- {
- this._sessionTimers.defaultExpires = JsSIP_C.SESSION_EXPIRES;
- }
- }
- }
-
- this._status = C.STATUS_ANSWERED;
-
- // An error on dialog creation will fire 'failed' event.
- if (!this._createDialog(request, 'UAS'))
- {
- request.reply(500, 'Error creating dialog');
-
- return;
- }
-
- clearTimeout(this._timers.userNoAnswerTimer);
-
- extraHeaders.unshift(`Contact: ${this._contact}`);
-
- // Determine incoming media from incoming SDP offer (if any).
- const sdp = request.parseSDP();
-
- // Make sure sdp.media is an array, not the case if there is only one media.
- if (!Array.isArray(sdp.media))
- {
- sdp.media = [ sdp.media ];
- }
-
- // Go through all medias in SDP to find offered capabilities to answer with.
- for (const m of sdp.media)
- {
- if (m.type === 'audio')
- {
- peerHasAudioLine = true;
- if (!m.direction || m.direction === 'sendrecv')
- {
- peerOffersFullAudio = true;
- }
- }
- if (m.type === 'video')
- {
- peerHasVideoLine = true;
- if (!m.direction || m.direction === 'sendrecv')
- {
- peerOffersFullVideo = true;
- }
- }
- }
-
- // Remove audio from mediaStream if suggested by mediaConstraints.
- if (mediaStream && mediaConstraints.audio === false)
- {
- tracks = mediaStream.getAudioTracks();
- for (const track of tracks)
- {
- mediaStream.removeTrack(track);
- }
- }
-
- // Remove video from mediaStream if suggested by mediaConstraints.
- if (mediaStream && mediaConstraints.video === false)
- {
- tracks = mediaStream.getVideoTracks();
- for (const track of tracks)
- {
- mediaStream.removeTrack(track);
- }
- }
-
- // Set audio constraints based on incoming stream if not supplied.
- if (!mediaStream && mediaConstraints.audio === undefined)
- {
- mediaConstraints.audio = peerOffersFullAudio;
- }
-
- // Set video constraints based on incoming stream if not supplied.
- if (!mediaStream && mediaConstraints.video === undefined)
- {
- mediaConstraints.video = peerOffersFullVideo;
- }
-
- // Don't ask for audio if the incoming offer has no audio section.
- if (!mediaStream && !peerHasAudioLine && !rtcOfferConstraints.offerToReceiveAudio)
- {
- mediaConstraints.audio = false;
- }
-
- // Don't ask for video if the incoming offer has no video section.
- if (!mediaStream && !peerHasVideoLine && !rtcOfferConstraints.offerToReceiveVideo)
- {
- mediaConstraints.video = false;
- }
-
- // Create a new RTCPeerConnection instance.
- // TODO: This may throw an error, should react.
- this._createRTCConnection(pcConfig, rtcConstraints);
-
- Promise.resolve()
- // Handle local MediaStream.
- .then(() =>
- {
- // A local MediaStream is given, use it.
- if (mediaStream)
- {
- return mediaStream;
- }
-
- // Audio and/or video requested, prompt getUserMedia.
- else if (mediaConstraints.audio || mediaConstraints.video)
- {
- this._localMediaStreamLocallyGenerated = true;
-
- return navigator.mediaDevices.getUserMedia(mediaConstraints)
- .catch((error) =>
- {
- if (this._status === C.STATUS_TERMINATED)
- {
- throw new Error('terminated');
- }
-
- request.reply(480);
- this._failed('local', null, JsSIP_C.causes.USER_DENIED_MEDIA_ACCESS);
-
- logger.warn('emit "getusermediafailed" [error:%o]', error);
-
- this.emit('getusermediafailed', error);
-
- throw new Error('getUserMedia() failed');
- });
- }
- })
- // Attach MediaStream to RTCPeerconnection.
- .then((stream) =>
- {
- if (this._status === C.STATUS_TERMINATED)
- {
- throw new Error('terminated');
- }
-
- this._localMediaStream = stream;
- if (stream)
- {
- stream.getTracks().forEach((track) =>
- {
- this._connection.addTrack(track, stream);
- });
- }
- })
- // Set remote description.
- .then(() =>
- {
- if (this._late_sdp)
- {
- return;
- }
-
- const e = { originator: 'remote', type: 'offer', sdp: request.body };
-
- logger.debug('emit "sdp"');
- this.emit('sdp', e);
-
- const offer = new RTCSessionDescription({ type: 'offer', sdp: e.sdp });
-
- this._connectionPromiseQueue = this._connectionPromiseQueue
- .then(() => this._connection.setRemoteDescription(offer))
- .catch((error) =>
- {
- request.reply(488);
-
- this._failed('system', null, JsSIP_C.causes.WEBRTC_ERROR);
-
- logger.warn('emit "peerconnection:setremotedescriptionfailed" [error:%o]', error);
-
- this.emit('peerconnection:setremotedescriptionfailed', error);
-
- throw new Error('peerconnection.setRemoteDescription() failed');
- });
-
- return this._connectionPromiseQueue;
- })
- // Create local description.
- .then(() =>
- {
- if (this._status === C.STATUS_TERMINATED)
- {
- throw new Error('terminated');
- }
-
- // TODO: Is this event already useful?
- this._connecting(request);
-
- if (!this._late_sdp)
- {
- return this._createLocalDescription('answer', rtcAnswerConstraints)
- .catch(() =>
- {
- request.reply(500);
-
- throw new Error('_createLocalDescription() failed');
- });
- }
- else
- {
- return this._createLocalDescription('offer', this._rtcOfferConstraints)
- .catch(() =>
- {
- request.reply(500);
-
- throw new Error('_createLocalDescription() failed');
- });
- }
- })
- // Send reply.
- .then((desc) =>
- {
- if (this._status === C.STATUS_TERMINATED)
- {
- throw new Error('terminated');
- }
-
- this._handleSessionTimersInIncomingRequest(request, extraHeaders);
-
- request.reply(200, null, extraHeaders,
- desc,
- () =>
- {
- this._status = C.STATUS_WAITING_FOR_ACK;
-
- this._setInvite2xxTimer(request, desc);
- this._setACKTimer();
- this._accepted('local');
- },
- () =>
- {
- this._failed('system', null, JsSIP_C.causes.CONNECTION_ERROR);
- }
- );
- })
- .catch((error) =>
- {
- if (this._status === C.STATUS_TERMINATED)
- {
- return;
- }
-
- logger.warn(`answer() failed: ${error.message}`);
-
- this._failed('system', error.message, JsSIP_C.causes.INTERNAL_ERROR);
- });
- }
-
- /**
- * Terminate the call.
- */
- terminate(options = {})
- {
- logger.debug('terminate()');
-
- const cause = options.cause || JsSIP_C.causes.BYE;
- const extraHeaders = Utils.cloneArray(options.extraHeaders);
- const body = options.body;
-
- let cancel_reason;
- let status_code = options.status_code;
- let reason_phrase = options.reason_phrase;
-
- // Check Session Status.
- if (this._status === C.STATUS_TERMINATED)
- {
- throw new Exceptions.InvalidStateError(this._status);
- }
-
- switch (this._status)
- {
- // - UAC -
- case C.STATUS_NULL:
- case C.STATUS_INVITE_SENT:
- case C.STATUS_1XX_RECEIVED:
- logger.debug('canceling session');
-
- if (status_code && (status_code < 200 || status_code >= 700))
- {
- throw new TypeError(`Invalid status_code: ${status_code}`);
- }
- else if (status_code)
- {
- reason_phrase = reason_phrase || JsSIP_C.REASON_PHRASE[status_code] || '';
- cancel_reason = `SIP ;cause=${status_code} ;text="${reason_phrase}"`;
- }
-
- // Check Session Status.
- if (this._status === C.STATUS_NULL || this._status === C.STATUS_INVITE_SENT)
- {
- this._is_canceled = true;
- this._cancel_reason = cancel_reason;
- }
- else if (this._status === C.STATUS_1XX_RECEIVED)
- {
- this._request.cancel(cancel_reason);
- }
-
- this._status = C.STATUS_CANCELED;
-
- this._failed('local', null, JsSIP_C.causes.CANCELED);
- break;
-
- // - UAS -
- case C.STATUS_WAITING_FOR_ANSWER:
- case C.STATUS_ANSWERED:
- logger.debug('rejecting session');
-
- status_code = status_code || 480;
-
- if (status_code < 300 || status_code >= 700)
- {
- throw new TypeError(`Invalid status_code: ${status_code}`);
- }
-
- this._request.reply(status_code, reason_phrase, extraHeaders, body);
- this._failed('local', null, JsSIP_C.causes.REJECTED);
- break;
-
- case C.STATUS_WAITING_FOR_ACK:
- case C.STATUS_CONFIRMED:
- logger.debug('terminating session');
-
- reason_phrase = options.reason_phrase || JsSIP_C.REASON_PHRASE[status_code] || '';
-
- if (status_code && (status_code < 200 || status_code >= 700))
- {
- throw new TypeError(`Invalid status_code: ${status_code}`);
- }
- else if (status_code)
- {
- extraHeaders.push(`Reason: SIP ;cause=${status_code}; text="${reason_phrase}"`);
- }
-
- /* RFC 3261 section 15 (Terminating a session):
- *
- * "...the callee's UA MUST NOT send a BYE on a confirmed dialog
- * until it has received an ACK for its 2xx response or until the server
- * transaction times out."
- */
- if (this._status === C.STATUS_WAITING_FOR_ACK &&
- this._direction === 'incoming' &&
- this._request.server_transaction.state !== Transactions.C.STATUS_TERMINATED)
- {
-
- // Save the dialog for later restoration.
- const dialog = this._dialog;
-
- // Send the BYE as soon as the ACK is received...
- this.receiveRequest = ({ method }) =>
- {
- if (method === JsSIP_C.ACK)
- {
- this.sendRequest(JsSIP_C.BYE, {
- extraHeaders,
- body
- });
- dialog.terminate();
- }
- };
-
- // .., or when the INVITE transaction times out
- this._request.server_transaction.on('stateChanged', () =>
- {
- if (this._request.server_transaction.state ===
- Transactions.C.STATUS_TERMINATED)
- {
- this.sendRequest(JsSIP_C.BYE, {
- extraHeaders,
- body
- });
- dialog.terminate();
- }
- });
-
- this._ended('local', null, cause);
-
- // Restore the dialog into 'this' in order to be able to send the in-dialog BYE :-).
- this._dialog = dialog;
-
- // Restore the dialog into 'ua' so the ACK can reach 'this' session.
- this._ua.newDialog(dialog);
- }
- else
- {
- this.sendRequest(JsSIP_C.BYE, {
- extraHeaders,
- body
- });
-
- this._ended('local', null, cause);
- }
- }
- }
-
- sendDTMF(tones, options = {})
- {
- logger.debug('sendDTMF() | tones: %s', tones);
-
- let duration = options.duration || null;
- let interToneGap = options.interToneGap || null;
- const transportType = options.transportType || JsSIP_C.DTMF_TRANSPORT.INFO;
-
- if (tones === undefined)
- {
- throw new TypeError('Not enough arguments');
- }
-
- // Check Session Status.
- if (
- this._status !== C.STATUS_CONFIRMED &&
- this._status !== C.STATUS_WAITING_FOR_ACK &&
- this._status !== C.STATUS_1XX_RECEIVED
- )
- {
- throw new Exceptions.InvalidStateError(this._status);
- }
-
- // Check Transport type.
- if (
- transportType !== JsSIP_C.DTMF_TRANSPORT.INFO &&
- transportType !== JsSIP_C.DTMF_TRANSPORT.RFC2833
- )
- {
- throw new TypeError(`invalid transportType: ${transportType}`);
- }
-
- // Convert to string.
- if (typeof tones === 'number')
- {
- tones = tones.toString();
- }
-
- // Check tones.
- if (!tones || typeof tones !== 'string' || !tones.match(/^[0-9A-DR#*,]+$/i))
- {
- throw new TypeError(`Invalid tones: ${tones}`);
- }
-
- // Check duration.
- if (duration && !Utils.isDecimal(duration))
- {
- throw new TypeError(`Invalid tone duration: ${duration}`);
- }
- else if (!duration)
- {
- duration = RTCSession_DTMF.C.DEFAULT_DURATION;
- }
- else if (duration < RTCSession_DTMF.C.MIN_DURATION)
- {
- logger.debug(`"duration" value is lower than the minimum allowed, setting it to ${RTCSession_DTMF.C.MIN_DURATION} milliseconds`);
- duration = RTCSession_DTMF.C.MIN_DURATION;
- }
- else if (duration > RTCSession_DTMF.C.MAX_DURATION)
- {
- logger.debug(`"duration" value is greater than the maximum allowed, setting it to ${RTCSession_DTMF.C.MAX_DURATION} milliseconds`);
- duration = RTCSession_DTMF.C.MAX_DURATION;
- }
- else
- {
- duration = Math.abs(duration);
- }
- options.duration = duration;
-
- // Check interToneGap.
- if (interToneGap && !Utils.isDecimal(interToneGap))
- {
- throw new TypeError(`Invalid interToneGap: ${interToneGap}`);
- }
- else if (!interToneGap)
- {
- interToneGap = RTCSession_DTMF.C.DEFAULT_INTER_TONE_GAP;
- }
- else if (interToneGap < RTCSession_DTMF.C.MIN_INTER_TONE_GAP)
- {
- logger.debug(`"interToneGap" value is lower than the minimum allowed, setting it to ${RTCSession_DTMF.C.MIN_INTER_TONE_GAP} milliseconds`);
- interToneGap = RTCSession_DTMF.C.MIN_INTER_TONE_GAP;
- }
- else
- {
- interToneGap = Math.abs(interToneGap);
- }
-
- // RFC2833. Let RTCDTMFSender enqueue the DTMFs.
- if (transportType === JsSIP_C.DTMF_TRANSPORT.RFC2833)
- {
- // Send DTMF in current audio RTP stream.
- const sender = this._getDTMFRTPSender();
-
- if (sender)
- {
- // Add remaining buffered tones.
- tones = sender.toneBuffer + tones;
- // Insert tones.
- sender.insertDTMF(tones, duration, interToneGap);
- }
-
- return;
- }
-
- if (this._tones)
- {
- // Tones are already queued, just add to the queue.
- this._tones += tones;
-
- return;
- }
-
- this._tones = tones;
-
- // Send the first tone.
- _sendDTMF.call(this);
-
- function _sendDTMF()
- {
- let timeout;
-
- if (this._status === C.STATUS_TERMINATED || !this._tones)
- {
- // Stop sending DTMF.
- this._tones = null;
-
- return;
- }
-
- // Retrieve the next tone.
- const tone = this._tones[0];
-
- // Remove the tone from this._tones.
- this._tones = this._tones.substring(1);
-
- if (tone === ',')
- {
- timeout = 2000;
- }
- else
- {
- // Send DTMF via SIP INFO messages.
- const dtmf = new RTCSession_DTMF(this);
-
- options.eventHandlers = {
- onFailed : () => { this._tones = null; }
- };
- dtmf.send(tone, options);
- timeout = duration + interToneGap;
- }
-
- // Set timeout for the next tone.
- setTimeout(_sendDTMF.bind(this), timeout);
- }
- }
-
- sendInfo(contentType, body, options = {})
- {
- logger.debug('sendInfo()');
-
- // Check Session Status.
- if (
- this._status !== C.STATUS_CONFIRMED &&
- this._status !== C.STATUS_WAITING_FOR_ACK &&
- this._status !== C.STATUS_1XX_RECEIVED
- )
- {
- throw new Exceptions.InvalidStateError(this._status);
- }
-
- const info = new RTCSession_Info(this);
-
- info.send(contentType, body, options);
- }
-
- /**
- * Mute
- */
- mute(options = { audio: true, video: false })
- {
- logger.debug('mute()');
-
- let audioMuted = false, videoMuted = false;
-
- if (this._audioMuted === false && options.audio)
- {
- audioMuted = true;
- this._audioMuted = true;
- this._toggleMuteAudio(true);
- }
-
- if (this._videoMuted === false && options.video)
- {
- videoMuted = true;
- this._videoMuted = true;
- this._toggleMuteVideo(true);
- }
-
- if (audioMuted === true || videoMuted === true)
- {
- this._onmute({
- audio : audioMuted,
- video : videoMuted
- });
- }
- }
-
- /**
- * Unmute
- */
- unmute(options = { audio: true, video: true })
- {
- logger.debug('unmute()');
-
- let audioUnMuted = false, videoUnMuted = false;
-
- if (this._audioMuted === true && options.audio)
- {
- audioUnMuted = true;
- this._audioMuted = false;
-
- if (this._localHold === false)
- {
- this._toggleMuteAudio(false);
- }
- }
-
- if (this._videoMuted === true && options.video)
- {
- videoUnMuted = true;
- this._videoMuted = false;
-
- if (this._localHold === false)
- {
- this._toggleMuteVideo(false);
- }
- }
-
- if (audioUnMuted === true || videoUnMuted === true)
- {
- this._onunmute({
- audio : audioUnMuted,
- video : videoUnMuted
- });
- }
- }
-
- /**
- * Hold
- */
- hold(options = {}, done)
- {
- logger.debug('hold()');
-
- if (this._status !== C.STATUS_WAITING_FOR_ACK && this._status !== C.STATUS_CONFIRMED)
- {
- return false;
- }
-
- if (this._localHold === true)
- {
- return false;
- }
-
- if (!this.isReadyToReOffer())
- {
- return false;
- }
-
- this._localHold = true;
- this._onhold('local');
-
- const eventHandlers = {
- succeeded : () =>
- {
- if (done) { done(); }
- },
- failed : () =>
- {
- this.terminate({
- cause : JsSIP_C.causes.WEBRTC_ERROR,
- status_code : 500,
- reason_phrase : 'Hold Failed'
- });
- }
- };
-
- if (options.useUpdate)
- {
- this._sendUpdate({
- sdpOffer : true,
- eventHandlers,
- extraHeaders : options.extraHeaders
- });
- }
- else
- {
- this._sendReinvite({
- eventHandlers,
- extraHeaders : options.extraHeaders
- });
- }
-
- return true;
- }
-
- unhold(options = {}, done)
- {
- logger.debug('unhold()');
-
- if (this._status !== C.STATUS_WAITING_FOR_ACK && this._status !== C.STATUS_CONFIRMED)
- {
- return false;
- }
-
- if (this._localHold === false)
- {
- return false;
- }
-
- if (!this.isReadyToReOffer())
- {
- return false;
- }
-
- this._localHold = false;
- this._onunhold('local');
-
- const eventHandlers = {
- succeeded : () =>
- {
- if (done) { done(); }
- },
- failed : () =>
- {
- this.terminate({
- cause : JsSIP_C.causes.WEBRTC_ERROR,
- status_code : 500,
- reason_phrase : 'Unhold Failed'
- });
- }
- };
-
- if (options.useUpdate)
- {
- this._sendUpdate({
- sdpOffer : true,
- eventHandlers,
- extraHeaders : options.extraHeaders
- });
- }
- else
- {
- this._sendReinvite({
- eventHandlers,
- extraHeaders : options.extraHeaders
- });
- }
-
- return true;
- }
-
- renegotiate(options = {}, done)
- {
- logger.debug('renegotiate()');
-
- const rtcOfferConstraints = options.rtcOfferConstraints || null;
-
- if (this._status !== C.STATUS_WAITING_FOR_ACK && this._status !== C.STATUS_CONFIRMED)
- {
- return false;
- }
-
- if (!this.isReadyToReOffer())
- {
- return false;
- }
-
- const eventHandlers = {
- succeeded : () =>
- {
- if (done) { done(); }
- },
- failed : () =>
- {
- this.terminate({
- cause : JsSIP_C.causes.WEBRTC_ERROR,
- status_code : 500,
- reason_phrase : 'Media Renegotiation Failed'
- });
- }
- };
-
- this._setLocalMediaStatus();
-
- if (options.useUpdate)
- {
- this._sendUpdate({
- sdpOffer : true,
- eventHandlers,
- rtcOfferConstraints,
- extraHeaders : options.extraHeaders
- });
- }
- else
- {
- this._sendReinvite({
- eventHandlers,
- rtcOfferConstraints,
- extraHeaders : options.extraHeaders
- });
- }
-
- return true;
- }
-
- /**
- * Refer
- */
- refer(target, options)
- {
- logger.debug('refer()');
-
- const originalTarget = target;
-
- if (this._status !== C.STATUS_WAITING_FOR_ACK && this._status !== C.STATUS_CONFIRMED)
- {
- return false;
- }
-
- // Check target validity.
- target = this._ua.normalizeTarget(target);
- if (!target)
- {
- throw new TypeError(`Invalid target: ${originalTarget}`);
- }
-
- const referSubscriber = new RTCSession_ReferSubscriber(this);
-
- referSubscriber.sendRefer(target, options);
-
- // Store in the map.
- const id = referSubscriber.id;
-
- this._referSubscribers[id] = referSubscriber;
-
- // Listen for ending events so we can remove it from the map.
- referSubscriber.on('requestFailed', () =>
- {
- delete this._referSubscribers[id];
- });
- referSubscriber.on('accepted', () =>
- {
- delete this._referSubscribers[id];
- });
- referSubscriber.on('failed', () =>
- {
- delete this._referSubscribers[id];
- });
-
- return referSubscriber;
- }
-
- /**
- * Send a generic in-dialog Request
- */
- sendRequest(method, options)
- {
- logger.debug('sendRequest()');
-
- if (this._dialog)
- {
- return this._dialog.sendRequest(method, options);
- }
- else
- {
- const dialogsArray = Object.values(this._earlyDialogs);
-
- if (dialogsArray.length > 0)
- {
- return dialogsArray[0].sendRequest(method, options);
- }
-
- logger.warn('sendRequest() | no valid early dialog found');
-
- return;
- }
- }
-
- /**
- * In dialog Request Reception
- */
- receiveRequest(request)
- {
- logger.debug('receiveRequest()');
-
- if (request.method === JsSIP_C.CANCEL)
- {
- /* RFC3261 15 States that a UAS may have accepted an invitation while a CANCEL
- * was in progress and that the UAC MAY continue with the session established by
- * any 2xx response, or MAY terminate with BYE. JsSIP does continue with the
- * established session. So the CANCEL is processed only if the session is not yet
- * established.
- */
-
- /*
- * Terminate the whole session in case the user didn't accept (or yet send the answer)
- * nor reject the request opening the session.
- */
- if (this._status === C.STATUS_WAITING_FOR_ANSWER ||
- this._status === C.STATUS_ANSWERED)
- {
- this._status = C.STATUS_CANCELED;
- this._request.reply(487);
- this._failed('remote', request, JsSIP_C.causes.CANCELED);
- }
- }
- else
- {
- // Requests arriving here are in-dialog requests.
- switch (request.method)
- {
- case JsSIP_C.ACK:
- if (this._status !== C.STATUS_WAITING_FOR_ACK)
- {
- return;
- }
-
- // Update signaling status.
- this._status = C.STATUS_CONFIRMED;
-
- clearTimeout(this._timers.ackTimer);
- clearTimeout(this._timers.invite2xxTimer);
-
- if (this._late_sdp)
- {
- if (!request.body)
- {
- this.terminate({
- cause : JsSIP_C.causes.MISSING_SDP,
- status_code : 400
- });
- break;
- }
-
- const e = { originator: 'remote', type: 'answer', sdp: request.body };
-
- logger.debug('emit "sdp"');
- this.emit('sdp', e);
-
- const answer = new RTCSessionDescription({ type: 'answer', sdp: e.sdp });
-
- this._connectionPromiseQueue = this._connectionPromiseQueue
- .then(() => this._connection.setRemoteDescription(answer))
- .then(() =>
- {
- if (!this._is_confirmed)
- {
- this._confirmed('remote', request);
- }
- })
- .catch((error) =>
- {
- this.terminate({
- cause : JsSIP_C.causes.BAD_MEDIA_DESCRIPTION,
- status_code : 488
- });
-
- logger.warn('emit "peerconnection:setremotedescriptionfailed" [error:%o]', error);
- this.emit('peerconnection:setremotedescriptionfailed', error);
- });
- }
- else if (!this._is_confirmed)
- {
- this._confirmed('remote', request);
- }
-
- break;
- case JsSIP_C.BYE:
- if (this._status === C.STATUS_CONFIRMED ||
- this._status === C.STATUS_WAITING_FOR_ACK)
- {
- request.reply(200);
- this._ended('remote', request, JsSIP_C.causes.BYE);
- }
- else if (this._status === C.STATUS_INVITE_RECEIVED ||
- this._status === C.STATUS_WAITING_FOR_ANSWER)
- {
- request.reply(200);
- this._request.reply(487, 'BYE Received');
- this._ended('remote', request, JsSIP_C.causes.BYE);
- }
- else
- {
- request.reply(403, 'Wrong Status');
- }
- break;
- case JsSIP_C.INVITE:
- if (this._status === C.STATUS_CONFIRMED)
- {
- if (request.hasHeader('replaces'))
- {
- this._receiveReplaces(request);
- }
- else
- {
- this._receiveReinvite(request);
- }
- }
- else
- {
- request.reply(403, 'Wrong Status');
- }
- break;
- case JsSIP_C.INFO:
- if (this._status === C.STATUS_1XX_RECEIVED ||
- this._status === C.STATUS_WAITING_FOR_ANSWER ||
- this._status === C.STATUS_ANSWERED ||
- this._status === C.STATUS_WAITING_FOR_ACK ||
- this._status === C.STATUS_CONFIRMED)
- {
- const contentType = request.hasHeader('Content-Type') ?
- request.getHeader('Content-Type').toLowerCase() : undefined;
-
- if (contentType && (contentType.match(/^application\/dtmf-relay/i)))
- {
- new RTCSession_DTMF(this).init_incoming(request);
- }
- else if (contentType !== undefined)
- {
- new RTCSession_Info(this).init_incoming(request);
- }
- else
- {
- request.reply(415);
- }
- }
- else
- {
- request.reply(403, 'Wrong Status');
- }
- break;
- case JsSIP_C.UPDATE:
- if (this._status === C.STATUS_CONFIRMED)
- {
- this._receiveUpdate(request);
- }
- else
- {
- request.reply(403, 'Wrong Status');
- }
- break;
- case JsSIP_C.REFER:
- if (this._status === C.STATUS_CONFIRMED)
- {
- this._receiveRefer(request);
- }
- else
- {
- request.reply(403, 'Wrong Status');
- }
- break;
- case JsSIP_C.NOTIFY:
- if (this._status === C.STATUS_CONFIRMED)
- {
- this._receiveNotify(request);
- }
- else
- {
- request.reply(403, 'Wrong Status');
- }
- break;
- default:
- request.reply(501);
- }
- }
- }
-
- /**
- * Session Callbacks
- */
-
- onTransportError()
- {
- logger.warn('onTransportError()');
-
- if (this._status !== C.STATUS_TERMINATED)
- {
- this.terminate({
- status_code : 500,
- reason_phrase : JsSIP_C.causes.CONNECTION_ERROR,
- cause : JsSIP_C.causes.CONNECTION_ERROR
- });
- }
- }
-
- onRequestTimeout()
- {
- logger.warn('onRequestTimeout()');
-
- if (this._status !== C.STATUS_TERMINATED)
- {
- this.terminate({
- status_code : 408,
- reason_phrase : JsSIP_C.causes.REQUEST_TIMEOUT,
- cause : JsSIP_C.causes.REQUEST_TIMEOUT
- });
- }
- }
-
- onDialogError()
- {
- logger.warn('onDialogError()');
-
- if (this._status !== C.STATUS_TERMINATED)
- {
- this.terminate({
- status_code : 500,
- reason_phrase : JsSIP_C.causes.DIALOG_ERROR,
- cause : JsSIP_C.causes.DIALOG_ERROR
- });
- }
- }
-
- // Called from DTMF handler.
- newDTMF(data)
- {
- logger.debug('newDTMF()');
-
- this.emit('newDTMF', data);
- }
-
- // Called from Info handler.
- newInfo(data)
- {
- logger.debug('newInfo()');
-
- this.emit('newInfo', data);
- }
-
- /**
- * Check if RTCSession is ready for an outgoing re-INVITE or UPDATE with SDP.
- */
- isReadyToReOffer()
- {
- if (!this._rtcReady)
- {
- logger.debug('isReadyToReOffer() | internal WebRTC status not ready');
-
- return false;
- }
-
- // No established yet.
- if (!this._dialog)
- {
- logger.debug('isReadyToReOffer() | session not established yet');
-
- return false;
- }
-
- // Another INVITE transaction is in progress.
- if (this._dialog.uac_pending_reply === true ||
- this._dialog.uas_pending_reply === true)
- {
- logger.debug('isReadyToReOffer() | there is another INVITE/UPDATE transaction in progress');
-
- return false;
- }
-
- return true;
- }
-
- _close()
- {
- logger.debug('close()');
-
- // Close local MediaStream if it was not given by the user.
- if (this._localMediaStream && this._localMediaStreamLocallyGenerated)
- {
- logger.debug('close() | closing local MediaStream');
-
- Utils.closeMediaStream(this._localMediaStream);
- }
-
- if (this._status === C.STATUS_TERMINATED)
- {
- return;
- }
-
- this._status = C.STATUS_TERMINATED;
-
- // Terminate RTC.
- if (this._connection)
- {
- try
- {
- this._connection.close();
- }
- catch (error)
- {
- logger.warn('close() | error closing the RTCPeerConnection: %o', error);
- }
- }
-
- // Terminate signaling.
-
- // Clear SIP timers.
- for (const timer in this._timers)
- {
- if (Object.prototype.hasOwnProperty.call(this._timers, timer))
- {
- clearTimeout(this._timers[timer]);
- }
- }
-
- // Clear Session Timers.
- clearTimeout(this._sessionTimers.timer);
-
- // Terminate confirmed dialog.
- if (this._dialog)
- {
- this._dialog.terminate();
- delete this._dialog;
- }
-
- // Terminate early dialogs.
- for (const dialog in this._earlyDialogs)
- {
- if (Object.prototype.hasOwnProperty.call(this._earlyDialogs, dialog))
- {
- this._earlyDialogs[dialog].terminate();
- delete this._earlyDialogs[dialog];
- }
- }
-
- // Terminate REFER subscribers.
- for (const subscriber in this._referSubscribers)
- {
- if (Object.prototype.hasOwnProperty.call(this._referSubscribers, subscriber))
- {
- delete this._referSubscribers[subscriber];
- }
- }
-
- this._ua.destroyRTCSession(this);
- }
-
- /**
- * Private API.
- */
-
- /**
- * RFC3261 13.3.1.4
- * Response retransmissions cannot be accomplished by transaction layer
- * since it is destroyed when receiving the first 2xx answer
- */
- _setInvite2xxTimer(request, body)
- {
- let timeout = Timers.T1;
-
- function invite2xxRetransmission()
- {
- if (this._status !== C.STATUS_WAITING_FOR_ACK)
- {
- return;
- }
-
- request.reply(200, null, [ `Contact: ${this._contact}` ], body);
-
- if (timeout < Timers.T2)
- {
- timeout = timeout * 2;
- if (timeout > Timers.T2)
- {
- timeout = Timers.T2;
- }
- }
-
- this._timers.invite2xxTimer = setTimeout(
- invite2xxRetransmission.bind(this), timeout);
- }
-
- this._timers.invite2xxTimer = setTimeout(
- invite2xxRetransmission.bind(this), timeout);
- }
-
-
- /**
- * RFC3261 14.2
- * If a UAS generates a 2xx response and never receives an ACK,
- * it SHOULD generate a BYE to terminate the dialog.
- */
- _setACKTimer()
- {
- this._timers.ackTimer = setTimeout(() =>
- {
- if (this._status === C.STATUS_WAITING_FOR_ACK)
- {
- logger.debug('no ACK received, terminating the session');
-
- clearTimeout(this._timers.invite2xxTimer);
- this.sendRequest(JsSIP_C.BYE);
- this._ended('remote', null, JsSIP_C.causes.NO_ACK);
- }
- }, Timers.TIMER_H);
- }
-
-
- _createRTCConnection(pcConfig, rtcConstraints)
- {
- this._connection = new RTCPeerConnection(pcConfig, rtcConstraints);
-
- this._connection.addEventListener('iceconnectionstatechange', () =>
- {
- const state = this._connection.iceConnectionState;
-
- // TODO: Do more with different states.
- if (state === 'failed')
- {
- this.terminate({
- cause : JsSIP_C.causes.RTP_TIMEOUT,
- status_code : 408,
- reason_phrase : JsSIP_C.causes.RTP_TIMEOUT
- });
- }
- });
-
- logger.debug('emit "peerconnection"');
-
- this.emit('peerconnection', {
- peerconnection : this._connection
- });
- }
-
- _createLocalDescription(type, constraints)
- {
- logger.debug('createLocalDescription()');
-
- if (type !== 'offer' && type !== 'answer')
- throw new Error(`createLocalDescription() | invalid type "${type}"`);
-
- const connection = this._connection;
-
- this._rtcReady = false;
-
- return Promise.resolve()
- // Create Offer or Answer.
- .then(() =>
- {
- if (type === 'offer')
- {
- return connection.createOffer(constraints)
- .catch((error) =>
- {
- logger.warn('emit "peerconnection:createofferfailed" [error:%o]', error);
-
- this.emit('peerconnection:createofferfailed', error);
-
- return Promise.reject(error);
- });
- }
- else
- {
- return connection.createAnswer(constraints)
- .catch((error) =>
- {
- logger.warn('emit "peerconnection:createanswerfailed" [error:%o]', error);
-
- this.emit('peerconnection:createanswerfailed', error);
-
- return Promise.reject(error);
- });
- }
- })
- // Set local description.
- .then((desc) =>
- {
- return connection.setLocalDescription(desc)
- .catch((error) =>
- {
- this._rtcReady = true;
-
- logger.warn('emit "peerconnection:setlocaldescriptionfailed" [error:%o]', error);
-
- this.emit('peerconnection:setlocaldescriptionfailed', error);
-
- return Promise.reject(error);
- });
- })
- .then(() =>
- {
- // Resolve right away if 'pc.iceGatheringState' is 'complete'.
- /**
- * Resolve right away if:
- * - 'connection.iceGatheringState' is 'complete' and no 'iceRestart' constraint is set.
- * - 'connection.iceGatheringState' is 'gathering' and 'iceReady' is true.
- */
- const iceRestart = constraints && constraints.iceRestart;
-
- if ((connection.iceGatheringState === 'complete' && !iceRestart) ||
- (connection.iceGatheringState === 'gathering' && this._iceReady))
- {
- this._rtcReady = true;
-
- const e = { originator: 'local', type: type, sdp: connection.localDescription.sdp };
-
- logger.debug('emit "sdp"');
-
- this.emit('sdp', e);
-
- return Promise.resolve(e.sdp);
- }
-
- // Add 'pc.onicencandidate' event handler to resolve on last candidate.
- return new Promise((resolve) =>
- {
- let finished = false;
- let iceCandidateListener;
- let iceGatheringStateListener;
-
- this._iceReady = false;
-
- const ready = () =>
- {
- if (finished)
- {
- return;
- }
-
- connection.removeEventListener('icecandidate', iceCandidateListener);
- connection.removeEventListener('icegatheringstatechange', iceGatheringStateListener);
-
- finished = true;
- this._rtcReady = true;
-
- // connection.iceGatheringState will still indicate 'gathering' and thus be blocking.
- this._iceReady = true;
-
- const e = { originator: 'local', type: type, sdp: connection.localDescription.sdp };
-
- logger.debug('emit "sdp"');
-
- this.emit('sdp', e);
-
- resolve(e.sdp);
- };
-
- connection.addEventListener('icecandidate', iceCandidateListener = (event) =>
- {
- const candidate = event.candidate;
-
- if (candidate)
- {
- this.emit('icecandidate', {
- candidate,
- ready
- });
- }
- else
- {
- ready();
- }
- });
-
- connection.addEventListener('icegatheringstatechange', iceGatheringStateListener = () =>
- {
- if (connection.iceGatheringState === 'complete')
- {
- ready();
- }
- });
- });
- });
- }
-
- /**
- * Dialog Management
- */
- _createDialog(message, type, early)
- {
- const local_tag = (type === 'UAS') ? message.to_tag : message.from_tag;
- const remote_tag = (type === 'UAS') ? message.from_tag : message.to_tag;
- const id = message.call_id + local_tag + remote_tag;
-
- let early_dialog = this._earlyDialogs[id];
-
- // Early Dialog.
- if (early)
- {
- if (early_dialog)
- {
- return true;
- }
- else
- {
- early_dialog = new Dialog(this, message, type, Dialog.C.STATUS_EARLY);
-
- // Dialog has been successfully created.
- if (early_dialog.error)
- {
- logger.debug(early_dialog.error);
- this._failed('remote', message, JsSIP_C.causes.INTERNAL_ERROR);
-
- return false;
- }
- else
- {
- this._earlyDialogs[id] = early_dialog;
-
- return true;
- }
- }
- }
-
- // Confirmed Dialog.
- else
- {
- this._from_tag = message.from_tag;
- this._to_tag = message.to_tag;
-
- // In case the dialog is in _early_ state, update it.
- if (early_dialog)
- {
- early_dialog.update(message, type);
- this._dialog = early_dialog;
- delete this._earlyDialogs[id];
-
- return true;
- }
-
- // Otherwise, create a _confirmed_ dialog.
- const dialog = new Dialog(this, message, type);
-
- if (dialog.error)
- {
- logger.debug(dialog.error);
- this._failed('remote', message, JsSIP_C.causes.INTERNAL_ERROR);
-
- return false;
- }
- else
- {
- this._dialog = dialog;
-
- return true;
- }
- }
- }
-
- /**
- * In dialog INVITE Reception
- */
-
- _receiveReinvite(request)
- {
- logger.debug('receiveReinvite()');
-
- const contentType = request.hasHeader('Content-Type') ?
- request.getHeader('Content-Type').toLowerCase() : undefined;
- const data = {
- request,
- callback : undefined,
- reject : reject.bind(this)
- };
-
- let rejected = false;
-
- function reject(options = {})
- {
- rejected = true;
-
- const status_code = options.status_code || 403;
- const reason_phrase = options.reason_phrase || '';
- const extraHeaders = Utils.cloneArray(options.extraHeaders);
-
- if (this._status !== C.STATUS_CONFIRMED)
- {
- return false;
- }
-
- if (status_code < 300 || status_code >= 700)
- {
- throw new TypeError(`Invalid status_code: ${status_code}`);
- }
-
- request.reply(status_code, reason_phrase, extraHeaders);
- }
-
- // Emit 'reinvite'.
- this.emit('reinvite', data);
-
- if (rejected)
- {
- return;
- }
-
- this._late_sdp = false;
-
- // Request without SDP.
- if (!request.body)
- {
- this._late_sdp = true;
- if (this._remoteHold)
- {
- this._remoteHold = false;
- this._onunhold('remote');
- }
- this._connectionPromiseQueue = this._connectionPromiseQueue
- .then(() => this._createLocalDescription('offer', this._rtcOfferConstraints))
- .then((sdp) =>
- {
- sendAnswer.call(this, sdp);
- })
- .catch(() =>
- {
- request.reply(500);
- });
-
- return;
- }
-
- // Request with SDP.
- if (contentType !== 'application/sdp')
- {
- logger.debug('invalid Content-Type');
- request.reply(415);
-
- return;
- }
-
- this._processInDialogSdpOffer(request)
- // Send answer.
- .then((desc) =>
- {
- if (this._status === C.STATUS_TERMINATED)
- {
- return;
- }
-
- sendAnswer.call(this, desc);
- })
- .catch((error) =>
- {
- logger.warn(error);
- });
-
- function sendAnswer(desc)
- {
- const extraHeaders = [ `Contact: ${this._contact}` ];
-
- this._handleSessionTimersInIncomingRequest(request, extraHeaders);
-
- if (this._late_sdp)
- {
- desc = this._mangleOffer(desc);
- }
-
- request.reply(200, null, extraHeaders, desc,
- () =>
- {
- this._status = C.STATUS_WAITING_FOR_ACK;
- this._setInvite2xxTimer(request, desc);
- this._setACKTimer();
- }
- );
-
- // If callback is given execute it.
- if (typeof data.callback === 'function')
- {
- data.callback();
- }
- }
- }
-
- /**
- * In dialog UPDATE Reception
- */
- _receiveUpdate(request)
- {
- logger.debug('receiveUpdate()');
-
- const contentType = request.hasHeader('Content-Type') ?
- request.getHeader('Content-Type').toLowerCase() : undefined;
- const data = {
- request,
- callback : undefined,
- reject : reject.bind(this)
- };
-
- let rejected = false;
-
- function reject(options = {})
- {
- rejected = true;
-
- const status_code = options.status_code || 403;
- const reason_phrase = options.reason_phrase || '';
- const extraHeaders = Utils.cloneArray(options.extraHeaders);
-
- if (this._status !== C.STATUS_CONFIRMED)
- {
- return false;
- }
-
- if (status_code < 300 || status_code >= 700)
- {
- throw new TypeError(`Invalid status_code: ${status_code}`);
- }
-
- request.reply(status_code, reason_phrase, extraHeaders);
- }
-
- // Emit 'update'.
- this.emit('update', data);
-
- if (rejected)
- {
- return;
- }
-
- if (!request.body)
- {
- sendAnswer.call(this, null);
-
- return;
- }
-
- if (contentType !== 'application/sdp')
- {
- logger.debug('invalid Content-Type');
-
- request.reply(415);
-
- return;
- }
-
- this._processInDialogSdpOffer(request)
- // Send answer.
- .then((desc) =>
- {
- if (this._status === C.STATUS_TERMINATED)
- {
- return;
- }
-
- sendAnswer.call(this, desc);
- })
- .catch((error) =>
- {
- logger.warn(error);
- });
-
- function sendAnswer(desc)
- {
- const extraHeaders = [ `Contact: ${this._contact}` ];
-
- this._handleSessionTimersInIncomingRequest(request, extraHeaders);
-
- request.reply(200, null, extraHeaders, desc);
-
- // If callback is given execute it.
- if (typeof data.callback === 'function')
- {
- data.callback();
- }
- }
- }
-
- _processInDialogSdpOffer(request)
- {
- logger.debug('_processInDialogSdpOffer()');
-
- const sdp = request.parseSDP();
-
- let hold = false;
-
- for (const m of sdp.media)
- {
- if (holdMediaTypes.indexOf(m.type) === -1)
- {
- continue;
- }
-
- const direction = m.direction || sdp.direction || 'sendrecv';
-
- if (direction === 'sendonly' || direction === 'inactive')
- {
- hold = true;
- }
- // If at least one of the streams is active don't emit 'hold'.
- else
- {
- hold = false;
- break;
- }
- }
-
- const e = { originator: 'remote', type: 'offer', sdp: request.body };
-
- logger.debug('emit "sdp"');
- this.emit('sdp', e);
-
- const offer = new RTCSessionDescription({ type: 'offer', sdp: e.sdp });
-
- this._connectionPromiseQueue = this._connectionPromiseQueue
- // Set remote description.
- .then(() =>
- {
- if (this._status === C.STATUS_TERMINATED)
- {
- throw new Error('terminated');
- }
-
- return this._connection.setRemoteDescription(offer)
- .catch((error) =>
- {
- request.reply(488);
- logger.warn('emit "peerconnection:setremotedescriptionfailed" [error:%o]', error);
-
- this.emit('peerconnection:setremotedescriptionfailed', error);
-
- throw error;
- });
- })
- .then(() =>
- {
- if (this._status === C.STATUS_TERMINATED)
- {
- throw new Error('terminated');
- }
-
- if (this._remoteHold === true && hold === false)
- {
- this._remoteHold = false;
- this._onunhold('remote');
- }
- else if (this._remoteHold === false && hold === true)
- {
- this._remoteHold = true;
- this._onhold('remote');
- }
- })
- // Create local description.
- .then(() =>
- {
- if (this._status === C.STATUS_TERMINATED)
- {
- throw new Error('terminated');
- }
-
- return this._createLocalDescription('answer', this._rtcAnswerConstraints)
- .catch((error) =>
- {
- request.reply(500);
- logger.warn('emit "peerconnection:createtelocaldescriptionfailed" [error:%o]', error);
-
- throw error;
- });
- })
- .catch((error) =>
- {
- logger.warn('_processInDialogSdpOffer() failed [error: %o]', error);
- });
-
- return this._connectionPromiseQueue;
- }
-
- /**
- * In dialog Refer Reception
- */
- _receiveRefer(request)
- {
- logger.debug('receiveRefer()');
-
- if (!request.refer_to)
- {
- logger.debug('no Refer-To header field present in REFER');
- request.reply(400);
-
- return;
- }
-
- if (request.refer_to.uri.scheme !== JsSIP_C.SIP)
- {
- logger.debug('Refer-To header field points to a non-SIP URI scheme');
- request.reply(416);
-
- return;
- }
-
- // Reply before the transaction timer expires.
- request.reply(202);
-
- const notifier = new RTCSession_ReferNotifier(this, request.cseq);
-
- logger.debug('emit "refer"');
-
- // Emit 'refer'.
- this.emit('refer', {
- request,
- accept : (initCallback, options) =>
- {
- accept.call(this, initCallback, options);
- },
- reject : () =>
- {
- reject.call(this);
- }
- });
-
- function accept(initCallback, options = {})
- {
- initCallback = (typeof initCallback === 'function')? initCallback : null;
-
- if (this._status !== C.STATUS_WAITING_FOR_ACK &&
- this._status !== C.STATUS_CONFIRMED)
- {
- return false;
- }
-
- const session = new RTCSession(this._ua);
-
- session.on('progress', ({ response }) =>
- {
- notifier.notify(response.status_code, response.reason_phrase);
- });
-
- session.on('accepted', ({ response }) =>
- {
- notifier.notify(response.status_code, response.reason_phrase);
- });
-
- session.on('_failed', ({ message, cause }) =>
- {
- if (message)
- {
- notifier.notify(message.status_code, message.reason_phrase);
- }
- else
- {
- notifier.notify(487, cause);
- }
- });
-
- // Consider the Replaces header present in the Refer-To URI.
- if (request.refer_to.uri.hasHeader('replaces'))
- {
- const replaces = decodeURIComponent(request.refer_to.uri.getHeader('replaces'));
-
- options.extraHeaders = Utils.cloneArray(options.extraHeaders);
- options.extraHeaders.push(`Replaces: ${replaces}`);
- }
-
- session.connect(request.refer_to.uri.toAor(), options, initCallback);
- }
-
- function reject()
- {
- notifier.notify(603);
- }
- }
-
- /**
- * In dialog Notify Reception
- */
- _receiveNotify(request)
- {
- logger.debug('receiveNotify()');
-
- if (!request.event)
- {
- request.reply(400);
- }
-
- switch (request.event.event)
- {
- case 'refer': {
- let id;
- let referSubscriber;
-
- if (request.event.params && request.event.params.id)
- {
- id = request.event.params.id;
- referSubscriber = this._referSubscribers[id];
- }
- else if (Object.keys(this._referSubscribers).length === 1)
- {
- referSubscriber = this._referSubscribers[
- Object.keys(this._referSubscribers)[0]];
- }
- else
- {
- request.reply(400, 'Missing event id parameter');
-
- return;
- }
-
- if (!referSubscriber)
- {
- request.reply(481, 'Subscription does not exist');
-
- return;
- }
-
- referSubscriber.receiveNotify(request);
- request.reply(200);
-
- break;
- }
-
- default: {
- request.reply(489);
- }
- }
- }
-
- /**
- * INVITE with Replaces Reception
- */
- _receiveReplaces(request)
- {
- logger.debug('receiveReplaces()');
-
- function accept(initCallback)
- {
- if (this._status !== C.STATUS_WAITING_FOR_ACK &&
- this._status !== C.STATUS_CONFIRMED)
- {
- return false;
- }
-
- const session = new RTCSession(this._ua);
-
- // Terminate the current session when the new one is confirmed.
- session.on('confirmed', () =>
- {
- this.terminate();
- });
-
- session.init_incoming(request, initCallback);
- }
-
- function reject()
- {
- logger.debug('Replaced INVITE rejected by the user');
- request.reply(486);
- }
-
- // Emit 'replace'.
- this.emit('replaces', {
- request,
- accept : (initCallback) => { accept.call(this, initCallback); },
- reject : () => { reject.call(this); }
- });
- }
-
- /**
- * Initial Request Sender
- */
- _sendInitialRequest(mediaConstraints, rtcOfferConstraints, mediaStream)
- {
- const request_sender = new RequestSender(this._ua, this._request, {
- onRequestTimeout : () =>
- {
- this.onRequestTimeout();
- },
- onTransportError : () =>
- {
- this.onTransportError();
- },
- // Update the request on authentication.
- onAuthenticated : (request) =>
- {
- this._request = request;
- },
- onReceiveResponse : (response) =>
- {
- this._receiveInviteResponse(response);
- }
- });
-
- // This Promise is resolved within the next iteration, so the app has now
- // a chance to set events such as 'peerconnection' and 'connecting'.
- Promise.resolve()
- // Get a stream if required.
- .then(() =>
- {
- // A stream is given, let the app set events such as 'peerconnection' and 'connecting'.
- if (mediaStream)
- {
- return mediaStream;
- }
- // Request for user media access.
- else if (mediaConstraints.audio || mediaConstraints.video)
- {
- this._localMediaStreamLocallyGenerated = true;
-
- return navigator.mediaDevices.getUserMedia(mediaConstraints)
- .catch((error) =>
- {
- if (this._status === C.STATUS_TERMINATED)
- {
- throw new Error('terminated');
- }
-
- this._failed('local', null, JsSIP_C.causes.USER_DENIED_MEDIA_ACCESS);
-
- logger.warn('emit "getusermediafailed" [error:%o]', error);
-
- this.emit('getusermediafailed', error);
-
- throw error;
- });
- }
- })
- .then((stream) =>
- {
- if (this._status === C.STATUS_TERMINATED)
- {
- throw new Error('terminated');
- }
-
- this._localMediaStream = stream;
-
- if (stream)
- {
- stream.getTracks().forEach((track) =>
- {
- this._connection.addTrack(track, stream);
- });
- }
-
- // TODO: should this be triggered here?
- this._connecting(this._request);
-
- return this._createLocalDescription('offer', rtcOfferConstraints)
- .catch((error) =>
- {
- this._failed('local', null, JsSIP_C.causes.WEBRTC_ERROR);
-
- throw error;
- });
- })
- .then((desc) =>
- {
- if (this._is_canceled || this._status === C.STATUS_TERMINATED)
- {
- throw new Error('terminated');
- }
-
- this._request.body = desc;
- this._status = C.STATUS_INVITE_SENT;
-
- logger.debug('emit "sending" [request:%o]', this._request);
-
- // Emit 'sending' so the app can mangle the body before the request is sent.
- this.emit('sending', {
- request : this._request
- });
-
- request_sender.send();
- })
- .catch((error) =>
- {
- if (this._status === C.STATUS_TERMINATED)
- {
- return;
- }
-
- logger.warn(error);
- });
- }
-
- /**
- * Get DTMF RTCRtpSender.
- */
- _getDTMFRTPSender()
- {
- const sender = this._connection.getSenders().find((rtpSender) =>
- {
- return rtpSender.track && rtpSender.track.kind === 'audio';
- });
-
- if (!(sender && sender.dtmf))
- {
- logger.warn('sendDTMF() | no local audio track to send DTMF with');
-
- return;
- }
-
- return sender.dtmf;
- }
-
- /**
- * Reception of Response for Initial INVITE
- */
- _receiveInviteResponse(response)
- {
- logger.debug('receiveInviteResponse()');
-
- // Handle 2XX retransmissions and responses from forked requests.
- if (this._dialog && (response.status_code >=200 && response.status_code <=299))
- {
-
- /*
- * If it is a retransmission from the endpoint that established
- * the dialog, send an ACK
- */
- if (this._dialog.id.call_id === response.call_id &&
- this._dialog.id.local_tag === response.from_tag &&
- this._dialog.id.remote_tag === response.to_tag)
- {
- this.sendRequest(JsSIP_C.ACK);
-
- return;
- }
-
- // If not, send an ACK and terminate.
- else
- {
- const dialog = new Dialog(this, response, 'UAC');
-
- if (dialog.error !== undefined)
- {
- logger.debug(dialog.error);
-
- return;
- }
-
- this.sendRequest(JsSIP_C.ACK);
- this.sendRequest(JsSIP_C.BYE);
-
- return;
- }
-
- }
-
- // Proceed to cancellation if the user requested.
- if (this._is_canceled)
- {
- if (response.status_code >= 100 && response.status_code < 200)
- {
- this._request.cancel(this._cancel_reason);
- }
- else if (response.status_code >= 200 && response.status_code < 299)
- {
- this._acceptAndTerminate(response);
- }
-
- return;
- }
-
- if (this._status !== C.STATUS_INVITE_SENT && this._status !== C.STATUS_1XX_RECEIVED)
- {
- return;
- }
-
- switch (true)
- {
- case /^100$/.test(response.status_code):
- this._status = C.STATUS_1XX_RECEIVED;
- break;
-
- case /^1[0-9]{2}$/.test(response.status_code):
- {
- // Do nothing with 1xx responses without To tag.
- if (!response.to_tag)
- {
- logger.debug('1xx response received without to tag');
- break;
- }
-
- // Create Early Dialog if 1XX comes with contact.
- if (response.hasHeader('contact'))
- {
- // An error on dialog creation will fire 'failed' event.
- if (!this._createDialog(response, 'UAC', true))
- {
- break;
- }
- }
-
- this._status = C.STATUS_1XX_RECEIVED;
-
- if (!response.body)
- {
- this._progress('remote', response);
- break;
- }
-
- const e = { originator: 'remote', type: 'answer', sdp: response.body };
-
- logger.debug('emit "sdp"');
- this.emit('sdp', e);
-
- const answer = new RTCSessionDescription({ type: 'answer', sdp: e.sdp });
-
- this._connectionPromiseQueue = this._connectionPromiseQueue
- .then(() => this._connection.setRemoteDescription(answer))
- .then(() => this._progress('remote', response))
- .catch((error) =>
- {
- logger.warn('emit "peerconnection:setremotedescriptionfailed" [error:%o]', error);
-
- this.emit('peerconnection:setremotedescriptionfailed', error);
- });
- break;
- }
-
- case /^2[0-9]{2}$/.test(response.status_code):
- {
- this._status = C.STATUS_CONFIRMED;
-
- if (!response.body)
- {
- this._acceptAndTerminate(response, 400, JsSIP_C.causes.MISSING_SDP);
- this._failed('remote', response, JsSIP_C.causes.BAD_MEDIA_DESCRIPTION);
- break;
- }
-
- // An error on dialog creation will fire 'failed' event.
- if (!this._createDialog(response, 'UAC'))
- {
- break;
- }
-
- const e = { originator: 'remote', type: 'answer', sdp: response.body };
-
- logger.debug('emit "sdp"');
- this.emit('sdp', e);
-
- const answer = new RTCSessionDescription({ type: 'answer', sdp: e.sdp });
-
- this._connectionPromiseQueue = this._connectionPromiseQueue
- .then(() =>
- {
- // Be ready for 200 with SDP after a 180/183 with SDP.
- // We created a SDP 'answer' for it, so check the current signaling state.
- if (this._connection.signalingState === 'stable')
- {
- return this._connection.createOffer(this._rtcOfferConstraints)
- .then((offer) => this._connection.setLocalDescription(offer))
- .catch((error) =>
- {
- this._acceptAndTerminate(response, 500, error.toString());
- this._failed('local', response, JsSIP_C.causes.WEBRTC_ERROR);
- });
- }
- })
- .then(() =>
- {
- this._connection.setRemoteDescription(answer)
- .then(() =>
- {
- // Handle Session Timers.
- this._handleSessionTimersInIncomingResponse(response);
-
- this._accepted('remote', response);
- this.sendRequest(JsSIP_C.ACK);
- this._confirmed('local', null);
- })
- .catch((error) =>
- {
- this._acceptAndTerminate(response, 488, 'Not Acceptable Here');
- this._failed('remote', response, JsSIP_C.causes.BAD_MEDIA_DESCRIPTION);
-
- logger.warn('emit "peerconnection:setremotedescriptionfailed" [error:%o]', error);
-
- this.emit('peerconnection:setremotedescriptionfailed', error);
- });
- });
- break;
- }
-
- default:
- {
- const cause = Utils.sipErrorCause(response.status_code);
-
- this._failed('remote', response, cause);
- }
- }
- }
-
- /**
- * Send Re-INVITE
- */
- _sendReinvite(options = {})
- {
- logger.debug('sendReinvite()');
-
- const extraHeaders = Utils.cloneArray(options.extraHeaders);
- const eventHandlers = Utils.cloneObject(options.eventHandlers);
- const rtcOfferConstraints = options.rtcOfferConstraints ||
- this._rtcOfferConstraints || null;
-
- let succeeded = false;
-
- extraHeaders.push(`Contact: ${this._contact}`);
- extraHeaders.push('Content-Type: application/sdp');
-
- // Session Timers.
- if (this._sessionTimers.running)
- {
- extraHeaders.push(`Session-Expires: ${this._sessionTimers.currentExpires};refresher=${this._sessionTimers.refresher ? 'uac' : 'uas'}`);
- }
-
- this._connectionPromiseQueue = this._connectionPromiseQueue
- .then(() => this._createLocalDescription('offer', rtcOfferConstraints))
- .then((sdp) =>
- {
- sdp = this._mangleOffer(sdp);
-
- const e = { originator: 'local', type: 'offer', sdp };
-
- logger.debug('emit "sdp"');
- this.emit('sdp', e);
-
- this.sendRequest(JsSIP_C.INVITE, {
- extraHeaders,
- body : sdp,
- eventHandlers : {
- onSuccessResponse : (response) =>
- {
- onSucceeded.call(this, response);
- succeeded = true;
- },
- onErrorResponse : (response) =>
- {
- onFailed.call(this, response);
- },
- onTransportError : () =>
- {
- this.onTransportError(); // Do nothing because session ends.
- },
- onRequestTimeout : () =>
- {
- this.onRequestTimeout(); // Do nothing because session ends.
- },
- onDialogError : () =>
- {
- this.onDialogError(); // Do nothing because session ends.
- }
- }
- });
- })
- .catch(() =>
- {
- onFailed();
- });
-
- function onSucceeded(response)
- {
- if (this._status === C.STATUS_TERMINATED)
- {
- return;
- }
-
- this.sendRequest(JsSIP_C.ACK);
-
- // If it is a 2XX retransmission exit now.
- if (succeeded) { return; }
-
- // Handle Session Timers.
- this._handleSessionTimersInIncomingResponse(response);
-
- // Must have SDP answer.
- if (!response.body)
- {
- onFailed.call(this);
-
- return;
- }
- else if (!response.hasHeader('Content-Type') || response.getHeader('Content-Type').toLowerCase() !== 'application/sdp')
- {
- onFailed.call(this);
-
- return;
- }
-
- const e = { originator: 'remote', type: 'answer', sdp: response.body };
-
- logger.debug('emit "sdp"');
- this.emit('sdp', e);
-
- const answer = new RTCSessionDescription({ type: 'answer', sdp: e.sdp });
-
- this._connectionPromiseQueue = this._connectionPromiseQueue
- .then(() => this._connection.setRemoteDescription(answer))
- .then(() =>
- {
- if (eventHandlers.succeeded)
- {
- eventHandlers.succeeded(response);
- }
- })
- .catch((error) =>
- {
- onFailed.call(this);
-
- logger.warn('emit "peerconnection:setremotedescriptionfailed" [error:%o]', error);
-
- this.emit('peerconnection:setremotedescriptionfailed', error);
- });
- }
-
- function onFailed(response)
- {
- if (eventHandlers.failed)
- {
- eventHandlers.failed(response);
- }
- }
- }
-
- /**
- * Send UPDATE
- */
- _sendUpdate(options = {})
- {
- logger.debug('sendUpdate()');
-
- const extraHeaders = Utils.cloneArray(options.extraHeaders);
- const eventHandlers = Utils.cloneObject(options.eventHandlers);
- const rtcOfferConstraints = options.rtcOfferConstraints ||
- this._rtcOfferConstraints || null;
- const sdpOffer = options.sdpOffer || false;
-
- let succeeded = false;
-
- extraHeaders.push(`Contact: ${this._contact}`);
-
- // Session Timers.
- if (this._sessionTimers.running)
- {
- extraHeaders.push(`Session-Expires: ${this._sessionTimers.currentExpires};refresher=${this._sessionTimers.refresher ? 'uac' : 'uas'}`);
- }
-
- if (sdpOffer)
- {
- extraHeaders.push('Content-Type: application/sdp');
-
- this._connectionPromiseQueue = this._connectionPromiseQueue
- .then(() => this._createLocalDescription('offer', rtcOfferConstraints))
- .then((sdp) =>
- {
- sdp = this._mangleOffer(sdp);
-
- const e = { originator: 'local', type: 'offer', sdp };
-
- logger.debug('emit "sdp"');
- this.emit('sdp', e);
-
- this.sendRequest(JsSIP_C.UPDATE, {
- extraHeaders,
- body : sdp,
- eventHandlers : {
- onSuccessResponse : (response) =>
- {
- onSucceeded.call(this, response);
- succeeded = true;
- },
- onErrorResponse : (response) =>
- {
- onFailed.call(this, response);
- },
- onTransportError : () =>
- {
- this.onTransportError(); // Do nothing because session ends.
- },
- onRequestTimeout : () =>
- {
- this.onRequestTimeout(); // Do nothing because session ends.
- },
- onDialogError : () =>
- {
- this.onDialogError(); // Do nothing because session ends.
- }
- }
- });
- })
- .catch(() =>
- {
- onFailed.call(this);
- });
- }
-
- // No SDP.
- else
- {
- this.sendRequest(JsSIP_C.UPDATE, {
- extraHeaders,
- eventHandlers : {
- onSuccessResponse : (response) =>
- {
- onSucceeded.call(this, response);
- },
- onErrorResponse : (response) =>
- {
- onFailed.call(this, response);
- },
- onTransportError : () =>
- {
- this.onTransportError(); // Do nothing because session ends.
- },
- onRequestTimeout : () =>
- {
- this.onRequestTimeout(); // Do nothing because session ends.
- },
- onDialogError : () =>
- {
- this.onDialogError(); // Do nothing because session ends.
- }
- }
- });
- }
-
- function onSucceeded(response)
- {
- if (this._status === C.STATUS_TERMINATED)
- {
- return;
- }
-
- // If it is a 2XX retransmission exit now.
- if (succeeded) { return; }
-
- // Handle Session Timers.
- this._handleSessionTimersInIncomingResponse(response);
-
- // Must have SDP answer.
- if (sdpOffer)
- {
- if (!response.body)
- {
- onFailed.call(this);
-
- return;
- }
- else if (!response.hasHeader('Content-Type') || response.getHeader('Content-Type').toLowerCase() !== 'application/sdp')
- {
- onFailed.call(this);
-
- return;
- }
-
- const e = { originator: 'remote', type: 'answer', sdp: response.body };
-
- logger.debug('emit "sdp"');
- this.emit('sdp', e);
-
- const answer = new RTCSessionDescription({ type: 'answer', sdp: e.sdp });
-
- this._connectionPromiseQueue = this._connectionPromiseQueue
- .then(() => this._connection.setRemoteDescription(answer))
- .then(() =>
- {
- if (eventHandlers.succeeded)
- {
- eventHandlers.succeeded(response);
- }
- })
- .catch((error) =>
- {
- onFailed.call(this);
-
- logger.warn('emit "peerconnection:setremotedescriptionfailed" [error:%o]', error);
-
- this.emit('peerconnection:setremotedescriptionfailed', error);
- });
- }
- // No SDP answer.
- else if (eventHandlers.succeeded)
- {
- eventHandlers.succeeded(response);
- }
- }
-
- function onFailed(response)
- {
- if (eventHandlers.failed) { eventHandlers.failed(response); }
- }
- }
-
- _acceptAndTerminate(response, status_code, reason_phrase)
- {
- logger.debug('acceptAndTerminate()');
-
- const extraHeaders = [];
-
- if (status_code)
- {
- reason_phrase = reason_phrase || JsSIP_C.REASON_PHRASE[status_code] || '';
- extraHeaders.push(`Reason: SIP ;cause=${status_code}; text="${reason_phrase}"`);
- }
-
- // An error on dialog creation will fire 'failed' event.
- if (this._dialog || this._createDialog(response, 'UAC'))
- {
- this.sendRequest(JsSIP_C.ACK);
- this.sendRequest(JsSIP_C.BYE, {
- extraHeaders
- });
- }
-
- // Update session status.
- this._status = C.STATUS_TERMINATED;
- }
-
- /**
- * Correctly set the SDP direction attributes if the call is on local hold
- */
- _mangleOffer(sdp)
- {
-
- if (!this._localHold && !this._remoteHold)
- {
- return sdp;
- }
-
- sdp = sdp_transform.parse(sdp);
-
- // Local hold.
- if (this._localHold && !this._remoteHold)
- {
- logger.debug('mangleOffer() | me on hold, mangling offer');
- for (const m of sdp.media)
- {
- if (holdMediaTypes.indexOf(m.type) === -1)
- {
- continue;
- }
- if (!m.direction)
- {
- m.direction = 'sendonly';
- }
- else if (m.direction === 'sendrecv')
- {
- m.direction = 'sendonly';
- }
- else if (m.direction === 'recvonly')
- {
- m.direction = 'inactive';
- }
- }
- }
- // Local and remote hold.
- else if (this._localHold && this._remoteHold)
- {
- logger.debug('mangleOffer() | both on hold, mangling offer');
- for (const m of sdp.media)
- {
- if (holdMediaTypes.indexOf(m.type) === -1)
- {
- continue;
- }
- m.direction = 'inactive';
- }
- }
- // Remote hold.
- else if (this._remoteHold)
- {
- logger.debug('mangleOffer() | remote on hold, mangling offer');
- for (const m of sdp.media)
- {
- if (holdMediaTypes.indexOf(m.type) === -1)
- {
- continue;
- }
- if (!m.direction)
- {
- m.direction = 'recvonly';
- }
- else if (m.direction === 'sendrecv')
- {
- m.direction = 'recvonly';
- }
- else if (m.direction === 'recvonly')
- {
- m.direction = 'inactive';
- }
- }
- }
-
- return sdp_transform.write(sdp);
- }
-
- _setLocalMediaStatus()
- {
- let enableAudio = true, enableVideo = true;
-
- if (this._localHold || this._remoteHold)
- {
- enableAudio = false;
- enableVideo = false;
- }
-
- if (this._audioMuted)
- {
- enableAudio = false;
- }
-
- if (this._videoMuted)
- {
- enableVideo = false;
- }
-
- this._toggleMuteAudio(!enableAudio);
- this._toggleMuteVideo(!enableVideo);
- }
-
- /**
- * Handle SessionTimers for an incoming INVITE or UPDATE.
- * @param {IncomingRequest} request
- * @param {Array} responseExtraHeaders Extra headers for the 200 response.
- */
- _handleSessionTimersInIncomingRequest(request, responseExtraHeaders)
- {
- if (!this._sessionTimers.enabled) { return; }
-
- let session_expires_refresher;
-
- if (request.session_expires && request.session_expires >= JsSIP_C.MIN_SESSION_EXPIRES)
- {
- this._sessionTimers.currentExpires = request.session_expires;
- session_expires_refresher = request.session_expires_refresher || 'uas';
- }
- else
- {
- this._sessionTimers.currentExpires = this._sessionTimers.defaultExpires;
- session_expires_refresher = 'uas';
- }
-
- responseExtraHeaders.push(`Session-Expires: ${this._sessionTimers.currentExpires};refresher=${session_expires_refresher}`);
-
- this._sessionTimers.refresher = (session_expires_refresher === 'uas');
- this._runSessionTimer();
- }
-
- /**
- * Handle SessionTimers for an incoming response to INVITE or UPDATE.
- * @param {IncomingResponse} response
- */
- _handleSessionTimersInIncomingResponse(response)
- {
- if (!this._sessionTimers.enabled) { return; }
-
- let session_expires_refresher;
-
- if (response.session_expires &&
- response.session_expires >= JsSIP_C.MIN_SESSION_EXPIRES)
- {
- this._sessionTimers.currentExpires = response.session_expires;
- session_expires_refresher = response.session_expires_refresher || 'uac';
- }
- else
- {
- this._sessionTimers.currentExpires = this._sessionTimers.defaultExpires;
- session_expires_refresher = 'uac';
- }
-
- this._sessionTimers.refresher = (session_expires_refresher === 'uac');
- this._runSessionTimer();
- }
-
- _runSessionTimer()
- {
- const expires = this._sessionTimers.currentExpires;
-
- this._sessionTimers.running = true;
-
- clearTimeout(this._sessionTimers.timer);
-
- // I'm the refresher.
- if (this._sessionTimers.refresher)
- {
- this._sessionTimers.timer = setTimeout(() =>
- {
- if (this._status === C.STATUS_TERMINATED) { return; }
-
- if (!this.isReadyToReOffer()) { return; }
-
- logger.debug('runSessionTimer() | sending session refresh request');
-
- if (this._sessionTimers.refreshMethod === JsSIP_C.UPDATE)
- {
- this._sendUpdate();
- }
- else
- {
- this._sendReinvite();
- }
- }, expires * 500); // Half the given interval (as the RFC states).
- }
-
- // I'm not the refresher.
- else
- {
- this._sessionTimers.timer = setTimeout(() =>
- {
- if (this._status === C.STATUS_TERMINATED) { return; }
-
- logger.warn('runSessionTimer() | timer expired, terminating the session');
-
- this.terminate({
- cause : JsSIP_C.causes.REQUEST_TIMEOUT,
- status_code : 408,
- reason_phrase : 'Session Timer Expired'
- });
- }, expires * 1100);
- }
- }
-
- _toggleMuteAudio(mute)
- {
- const senders = this._connection.getSenders().filter((sender) =>
- {
- return sender.track && sender.track.kind === 'audio';
- });
-
- for (const sender of senders)
- {
- sender.track.enabled = !mute;
- }
- }
-
- _toggleMuteVideo(mute)
- {
- const senders = this._connection.getSenders().filter((sender) =>
- {
- return sender.track && sender.track.kind === 'video';
- });
-
- for (const sender of senders)
- {
- sender.track.enabled = !mute;
- }
- }
-
- _newRTCSession(originator, request)
- {
- logger.debug('newRTCSession()');
-
- this._ua.newRTCSession(this, {
- originator,
- session : this,
- request
- });
- }
-
- _connecting(request)
- {
- logger.debug('session connecting');
-
- logger.debug('emit "connecting"');
-
- this.emit('connecting', {
- request
- });
- }
-
- _progress(originator, response)
- {
- logger.debug('session progress');
-
- logger.debug('emit "progress"');
-
- this.emit('progress', {
- originator,
- response : response || null
- });
- }
-
- _accepted(originator, message)
- {
- logger.debug('session accepted');
-
- this._start_time = new Date();
-
- logger.debug('emit "accepted"');
-
- this.emit('accepted', {
- originator,
- response : message || null
- });
- }
-
- _confirmed(originator, ack)
- {
- logger.debug('session confirmed');
+const holdMediaTypes = ['audio', 'video'];
+
+module.exports = class RTCSession extends EventEmitter {
+ /**
+ * Expose C object.
+ */
+ static get C() {
+ return C;
+ }
+
+ constructor(ua) {
+ logger.debug('new');
+
+ super();
+
+ this._id = null;
+ this._ua = ua;
+ this._status = C.STATUS_NULL;
+ this._dialog = null;
+ this._earlyDialogs = {};
+ this._contact = null;
+ this._from_tag = null;
+ this._to_tag = null;
+
+ // The RTCPeerConnection instance (public attribute).
+ this._connection = null;
+
+ // Prevent races on serial PeerConnction operations.
+ this._connectionPromiseQueue = Promise.resolve();
+
+ // Incoming/Outgoing request being currently processed.
+ this._request = null;
+
+ // Cancel state for initial outgoing request.
+ this._is_canceled = false;
+ this._cancel_reason = '';
+
+ // RTCSession confirmation flag.
+ this._is_confirmed = false;
+
+ // Is late SDP being negotiated.
+ this._late_sdp = false;
+
+ // Default rtcOfferConstraints and rtcAnswerConstrainsts (passed in connect() or answer()).
+ this._rtcOfferConstraints = null;
+ this._rtcAnswerConstraints = null;
+
+ // Local MediaStream.
+ this._localMediaStream = null;
+ this._localMediaStreamLocallyGenerated = false;
+
+ // Flag to indicate PeerConnection ready for new actions.
+ this._rtcReady = true;
+
+ // Flag to indicate ICE candidate gathering is finished even if iceGatheringState is not yet 'complete'.
+ this._iceReady = false;
+
+ // SIP Timers.
+ this._timers = {
+ ackTimer: null,
+ expiresTimer: null,
+ invite2xxTimer: null,
+ userNoAnswerTimer: null,
+ };
+
+ // Session info.
+ this._direction = null;
+ this._local_identity = null;
+ this._remote_identity = null;
+ this._start_time = null;
+ this._end_time = null;
+ this._tones = null;
+
+ // Mute/Hold state.
+ this._audioMuted = false;
+ this._videoMuted = false;
+ this._localHold = false;
+ this._remoteHold = false;
+
+ // Session Timers (RFC 4028).
+ this._sessionTimers = {
+ enabled: this._ua.configuration.session_timers,
+ refreshMethod: this._ua.configuration.session_timers_refresh_method,
+ defaultExpires: JsSIP_C.SESSION_EXPIRES,
+ currentExpires: null,
+ running: false,
+ refresher: false,
+ timer: null, // A setTimeout.
+ };
+
+ // Map of ReferSubscriber instances indexed by the REFER's CSeq number.
+ this._referSubscribers = {};
+
+ // Custom session empty object for high level use.
+ this._data = {};
+ }
+
+ /**
+ * User API
+ */
+
+ // Expose RTCSession constants as a property of the RTCSession instance.
+ get C() {
+ return C;
+ }
+
+ // Expose session failed/ended causes as a property of the RTCSession instance.
+ get causes() {
+ return JsSIP_C.causes;
+ }
+
+ get id() {
+ return this._id;
+ }
+
+ get connection() {
+ return this._connection;
+ }
+
+ get contact() {
+ return this._contact;
+ }
+
+ get direction() {
+ return this._direction;
+ }
+
+ get local_identity() {
+ return this._local_identity;
+ }
+
+ get remote_identity() {
+ return this._remote_identity;
+ }
+
+ get start_time() {
+ return this._start_time;
+ }
+
+ get end_time() {
+ return this._end_time;
+ }
+
+ get data() {
+ return this._data;
+ }
+
+ set data(_data) {
+ this._data = _data;
+ }
+
+ get status() {
+ return this._status;
+ }
+
+ isInProgress() {
+ switch (this._status) {
+ case C.STATUS_NULL:
+ case C.STATUS_INVITE_SENT:
+ case C.STATUS_1XX_RECEIVED:
+ case C.STATUS_INVITE_RECEIVED:
+ case C.STATUS_WAITING_FOR_ANSWER: {
+ return true;
+ }
+ default: {
+ return false;
+ }
+ }
+ }
+
+ isEstablished() {
+ switch (this._status) {
+ case C.STATUS_ANSWERED:
+ case C.STATUS_WAITING_FOR_ACK:
+ case C.STATUS_CONFIRMED: {
+ return true;
+ }
+ default: {
+ return false;
+ }
+ }
+ }
+
+ isEnded() {
+ switch (this._status) {
+ case C.STATUS_CANCELED:
+ case C.STATUS_TERMINATED: {
+ return true;
+ }
+ default: {
+ return false;
+ }
+ }
+ }
+
+ isMuted() {
+ return {
+ audio: this._audioMuted,
+ video: this._videoMuted,
+ };
+ }
+
+ isOnHold() {
+ return {
+ local: this._localHold,
+ remote: this._remoteHold,
+ };
+ }
+
+ connect(target, options = {}, initCallback) {
+ logger.debug('connect()');
+
+ const originalTarget = target;
+ const eventHandlers = Utils.cloneObject(options.eventHandlers);
+ const extraHeaders = Utils.cloneArray(options.extraHeaders);
+ const mediaConstraints = Utils.cloneObject(options.mediaConstraints, {
+ audio: true,
+ video: true,
+ });
+ const mediaStream = options.mediaStream || null;
+ const pcConfig = Utils.cloneObject(options.pcConfig, { iceServers: [] });
+ const rtcConstraints = options.rtcConstraints || null;
+ const rtcOfferConstraints = options.rtcOfferConstraints || null;
+
+ this._rtcOfferConstraints = rtcOfferConstraints;
+ this._rtcAnswerConstraints = options.rtcAnswerConstraints || null;
+
+ this._data = options.data || this._data;
+
+ // Check target.
+ if (target === undefined) {
+ throw new TypeError('Not enough arguments');
+ }
+
+ // Check Session Status.
+ if (this._status !== C.STATUS_NULL) {
+ throw new Exceptions.InvalidStateError(this._status);
+ }
+
+ // Check WebRTC support.
+ // eslint-disable-next-line no-undef
+ if (!window.RTCPeerConnection) {
+ throw new Exceptions.NotSupportedError('WebRTC not supported');
+ }
+
+ // Check target validity.
+ target = this._ua.normalizeTarget(target);
+ if (!target) {
+ throw new TypeError(`Invalid target: ${originalTarget}`);
+ }
+
+ // Session Timers.
+ if (this._sessionTimers.enabled) {
+ if (Utils.isDecimal(options.sessionTimersExpires)) {
+ if (options.sessionTimersExpires >= JsSIP_C.MIN_SESSION_EXPIRES) {
+ this._sessionTimers.defaultExpires = options.sessionTimersExpires;
+ } else {
+ this._sessionTimers.defaultExpires = JsSIP_C.SESSION_EXPIRES;
+ }
+ }
+ }
+
+ // Set event handlers.
+ for (const event in eventHandlers) {
+ if (Object.prototype.hasOwnProperty.call(eventHandlers, event)) {
+ this.on(event, eventHandlers[event]);
+ }
+ }
+
+ // Session parameter initialization.
+ this._from_tag = Utils.newTag();
+
+ // Set anonymous property.
+ const anonymous = options.anonymous || false;
+
+ const requestParams = { from_tag: this._from_tag };
+
+ this._contact = this._ua.contact.toString({
+ anonymous,
+ outbound: true,
+ });
+
+ if (anonymous) {
+ requestParams.from_display_name = 'Anonymous';
+ requestParams.from_uri = new URI('sip', 'anonymous', 'anonymous.invalid');
+
+ extraHeaders.push(
+ `P-Preferred-Identity: ${this._ua.configuration.uri.toString()}`
+ );
+ extraHeaders.push('Privacy: id');
+ } else if (options.fromUserName) {
+ requestParams.from_uri = new URI(
+ 'sip',
+ options.fromUserName,
+ this._ua.configuration.uri.host
+ );
+
+ extraHeaders.push(
+ `P-Preferred-Identity: ${this._ua.configuration.uri.toString()}`
+ );
+ }
+
+ if (options.fromDisplayName) {
+ requestParams.from_display_name = options.fromDisplayName;
+ }
+
+ extraHeaders.push(`Contact: ${this._contact}`);
+ extraHeaders.push('Content-Type: application/sdp');
+ if (this._sessionTimers.enabled) {
+ extraHeaders.push(
+ `Session-Expires: ${this._sessionTimers.defaultExpires}${this._ua.configuration.session_timers_force_refresher ? ';refresher=uac' : ''}`
+ );
+ }
+
+ this._request = new SIPMessage.InitialOutgoingInviteRequest(
+ target,
+ this._ua,
+ requestParams,
+ extraHeaders
+ );
+
+ this._id = this._request.call_id + this._from_tag;
+
+ // Create a new RTCPeerConnection instance.
+ this._createRTCConnection(pcConfig, rtcConstraints);
+
+ // Set internal properties.
+ this._direction = 'outgoing';
+ this._local_identity = this._request.from;
+ this._remote_identity = this._request.to;
+
+ // User explicitly provided a newRTCSession callback for this session.
+ if (initCallback) {
+ initCallback(this);
+ }
+
+ this._newRTCSession('local', this._request);
+
+ this._sendInitialRequest(
+ mediaConstraints,
+ rtcOfferConstraints,
+ mediaStream
+ );
+ }
+
+ init_incoming(request, initCallback) {
+ logger.debug('init_incoming()');
+
+ let expires;
+ const contentType = request.hasHeader('Content-Type')
+ ? request.getHeader('Content-Type').toLowerCase()
+ : undefined;
+
+ // Check body and content type.
+ if (request.body && contentType !== 'application/sdp') {
+ request.reply(415);
+
+ return;
+ }
+
+ // Session parameter initialization.
+ this._status = C.STATUS_INVITE_RECEIVED;
+ this._from_tag = request.from_tag;
+ this._id = request.call_id + this._from_tag;
+ this._request = request;
+ this._contact = this._ua.contact.toString();
+
+ // Get the Expires header value if exists.
+ if (request.hasHeader('expires')) {
+ expires = request.getHeader('expires') * 1000;
+ }
+
+ /* Set the to_tag before
+ * replying a response code that will create a dialog.
+ */
+ request.to_tag = Utils.newTag();
+
+ // An error on dialog creation will fire 'failed' event.
+ if (!this._createDialog(request, 'UAS', true)) {
+ request.reply(500, 'Missing Contact header field');
+
+ return;
+ }
+
+ if (request.body) {
+ this._late_sdp = false;
+ } else {
+ this._late_sdp = true;
+ }
+
+ this._status = C.STATUS_WAITING_FOR_ANSWER;
+
+ // Set userNoAnswerTimer.
+ this._timers.userNoAnswerTimer = setTimeout(() => {
+ request.reply(408);
+ this._failed('local', null, JsSIP_C.causes.NO_ANSWER);
+ }, this._ua.configuration.no_answer_timeout);
+
+ /* Set expiresTimer
+ * RFC3261 13.3.1
+ */
+ if (expires) {
+ this._timers.expiresTimer = setTimeout(() => {
+ if (this._status === C.STATUS_WAITING_FOR_ANSWER) {
+ request.reply(487);
+ this._failed('system', null, JsSIP_C.causes.EXPIRES);
+ }
+ }, expires);
+ }
+
+ // Set internal properties.
+ this._direction = 'incoming';
+ this._local_identity = request.to;
+ this._remote_identity = request.from;
+
+ // A init callback was specifically defined.
+ if (initCallback) {
+ initCallback(this);
+ }
+
+ // Fire 'newRTCSession' event.
+ this._newRTCSession('remote', request);
+
+ // The user may have rejected the call in the 'newRTCSession' event.
+ if (this._status === C.STATUS_TERMINATED) {
+ return;
+ }
+
+ // Reply 180.
+ request.reply(180, null, [`Contact: ${this._contact}`]);
+
+ // Fire 'progress' event.
+ // TODO: Document that 'response' field in 'progress' event is null for incoming calls.
+ this._progress('local', null);
+ }
+
+ /**
+ * Answer the call.
+ */
+ answer(options = {}) {
+ logger.debug('answer()');
+
+ const request = this._request;
+ const extraHeaders = Utils.cloneArray(options.extraHeaders);
+ const mediaConstraints = Utils.cloneObject(options.mediaConstraints);
+ const mediaStream = options.mediaStream || null;
+ const pcConfig = Utils.cloneObject(options.pcConfig, { iceServers: [] });
+ const rtcConstraints = options.rtcConstraints || null;
+ const rtcAnswerConstraints = options.rtcAnswerConstraints || null;
+ const rtcOfferConstraints = Utils.cloneObject(options.rtcOfferConstraints);
+
+ let tracks;
+ let peerHasAudioLine = false;
+ let peerHasVideoLine = false;
+ let peerOffersFullAudio = false;
+ let peerOffersFullVideo = false;
+
+ this._rtcAnswerConstraints = rtcAnswerConstraints;
+ this._rtcOfferConstraints = options.rtcOfferConstraints || null;
+
+ this._data = options.data || this._data;
+
+ // Check Session Direction and Status.
+ if (this._direction !== 'incoming') {
+ throw new Exceptions.NotSupportedError(
+ '"answer" not supported for outgoing RTCSession'
+ );
+ }
+
+ // Check Session status.
+ if (this._status !== C.STATUS_WAITING_FOR_ANSWER) {
+ throw new Exceptions.InvalidStateError(this._status);
+ }
+
+ // Session Timers.
+ if (this._sessionTimers.enabled) {
+ if (Utils.isDecimal(options.sessionTimersExpires)) {
+ if (options.sessionTimersExpires >= JsSIP_C.MIN_SESSION_EXPIRES) {
+ this._sessionTimers.defaultExpires = options.sessionTimersExpires;
+ } else {
+ this._sessionTimers.defaultExpires = JsSIP_C.SESSION_EXPIRES;
+ }
+ }
+ }
+
+ this._status = C.STATUS_ANSWERED;
+
+ // An error on dialog creation will fire 'failed' event.
+ if (!this._createDialog(request, 'UAS')) {
+ request.reply(500, 'Error creating dialog');
+
+ return;
+ }
+
+ clearTimeout(this._timers.userNoAnswerTimer);
+
+ extraHeaders.unshift(`Contact: ${this._contact}`);
+
+ // Determine incoming media from incoming SDP offer (if any).
+ const sdp = request.parseSDP();
+
+ // Make sure sdp.media is an array, not the case if there is only one media.
+ if (!Array.isArray(sdp.media)) {
+ sdp.media = [sdp.media];
+ }
+
+ // Go through all medias in SDP to find offered capabilities to answer with.
+ for (const m of sdp.media) {
+ if (m.type === 'audio') {
+ peerHasAudioLine = true;
+ if (!m.direction || m.direction === 'sendrecv') {
+ peerOffersFullAudio = true;
+ }
+ }
+ if (m.type === 'video') {
+ peerHasVideoLine = true;
+ if (!m.direction || m.direction === 'sendrecv') {
+ peerOffersFullVideo = true;
+ }
+ }
+ }
+
+ // Remove audio from mediaStream if suggested by mediaConstraints.
+ if (mediaStream && mediaConstraints.audio === false) {
+ tracks = mediaStream.getAudioTracks();
+ for (const track of tracks) {
+ mediaStream.removeTrack(track);
+ }
+ }
+
+ // Remove video from mediaStream if suggested by mediaConstraints.
+ if (mediaStream && mediaConstraints.video === false) {
+ tracks = mediaStream.getVideoTracks();
+ for (const track of tracks) {
+ mediaStream.removeTrack(track);
+ }
+ }
+
+ // Set audio constraints based on incoming stream if not supplied.
+ if (!mediaStream && mediaConstraints.audio === undefined) {
+ mediaConstraints.audio = peerOffersFullAudio;
+ }
+
+ // Set video constraints based on incoming stream if not supplied.
+ if (!mediaStream && mediaConstraints.video === undefined) {
+ mediaConstraints.video = peerOffersFullVideo;
+ }
+
+ // Don't ask for audio if the incoming offer has no audio section.
+ if (
+ !mediaStream &&
+ !peerHasAudioLine &&
+ !rtcOfferConstraints.offerToReceiveAudio
+ ) {
+ mediaConstraints.audio = false;
+ }
+
+ // Don't ask for video if the incoming offer has no video section.
+ if (
+ !mediaStream &&
+ !peerHasVideoLine &&
+ !rtcOfferConstraints.offerToReceiveVideo
+ ) {
+ mediaConstraints.video = false;
+ }
+
+ // Create a new RTCPeerConnection instance.
+ // TODO: This may throw an error, should react.
+ this._createRTCConnection(pcConfig, rtcConstraints);
+
+ Promise.resolve()
+ // Handle local MediaStream.
+ .then(() => {
+ // A local MediaStream is given, use it.
+ if (mediaStream) {
+ return mediaStream;
+ }
+
+ // Audio and/or video requested, prompt getUserMedia.
+ else if (mediaConstraints.audio || mediaConstraints.video) {
+ this._localMediaStreamLocallyGenerated = true;
+
+ return navigator.mediaDevices
+ .getUserMedia(mediaConstraints)
+ .catch(error => {
+ if (this._status === C.STATUS_TERMINATED) {
+ throw new Error('terminated');
+ }
+
+ request.reply(480);
+ this._failed(
+ 'local',
+ null,
+ JsSIP_C.causes.USER_DENIED_MEDIA_ACCESS
+ );
+
+ logger.warn('emit "getusermediafailed" [error:%o]', error);
+
+ this.emit('getusermediafailed', error);
+
+ throw new Error('getUserMedia() failed');
+ });
+ }
+ })
+ // Attach MediaStream to RTCPeerconnection.
+ .then(stream => {
+ if (this._status === C.STATUS_TERMINATED) {
+ throw new Error('terminated');
+ }
+
+ this._localMediaStream = stream;
+ if (stream) {
+ stream.getTracks().forEach(track => {
+ this._connection.addTrack(track, stream);
+ });
+ }
+ })
+ // Set remote description.
+ .then(() => {
+ if (this._late_sdp) {
+ return;
+ }
+
+ const e = { originator: 'remote', type: 'offer', sdp: request.body };
+
+ logger.debug('emit "sdp"');
+ this.emit('sdp', e);
+
+ const offer = new RTCSessionDescription({ type: 'offer', sdp: e.sdp });
+
+ this._connectionPromiseQueue = this._connectionPromiseQueue
+ .then(() => this._connection.setRemoteDescription(offer))
+ .catch(error => {
+ request.reply(488);
+
+ this._failed('system', null, JsSIP_C.causes.WEBRTC_ERROR);
+
+ logger.warn(
+ 'emit "peerconnection:setremotedescriptionfailed" [error:%o]',
+ error
+ );
+
+ this.emit('peerconnection:setremotedescriptionfailed', error);
+
+ throw new Error('peerconnection.setRemoteDescription() failed');
+ });
+
+ return this._connectionPromiseQueue;
+ })
+ // Create local description.
+ .then(() => {
+ if (this._status === C.STATUS_TERMINATED) {
+ throw new Error('terminated');
+ }
+
+ // TODO: Is this event already useful?
+ this._connecting(request);
+
+ if (!this._late_sdp) {
+ return this._createLocalDescription(
+ 'answer',
+ rtcAnswerConstraints
+ ).catch(() => {
+ request.reply(500);
+
+ throw new Error('_createLocalDescription() failed');
+ });
+ } else {
+ return this._createLocalDescription(
+ 'offer',
+ this._rtcOfferConstraints
+ ).catch(() => {
+ request.reply(500);
+
+ throw new Error('_createLocalDescription() failed');
+ });
+ }
+ })
+ // Send reply.
+ .then(desc => {
+ if (this._status === C.STATUS_TERMINATED) {
+ throw new Error('terminated');
+ }
+
+ this._handleSessionTimersInIncomingRequest(request, extraHeaders);
+
+ request.reply(
+ 200,
+ null,
+ extraHeaders,
+ desc,
+ () => {
+ this._status = C.STATUS_WAITING_FOR_ACK;
+
+ this._setInvite2xxTimer(request, desc);
+ this._setACKTimer();
+ this._accepted('local');
+ },
+ () => {
+ this._failed('system', null, JsSIP_C.causes.CONNECTION_ERROR);
+ }
+ );
+ })
+ .catch(error => {
+ if (this._status === C.STATUS_TERMINATED) {
+ return;
+ }
+
+ logger.warn(`answer() failed: ${error.message}`);
+
+ this._failed('system', error.message, JsSIP_C.causes.INTERNAL_ERROR);
+ });
+ }
+
+ /**
+ * Terminate the call.
+ */
+ terminate(options = {}) {
+ logger.debug('terminate()');
+
+ const cause = options.cause || JsSIP_C.causes.BYE;
+ const extraHeaders = Utils.cloneArray(options.extraHeaders);
+ const body = options.body;
+
+ let cancel_reason;
+ let status_code = options.status_code;
+ let reason_phrase = options.reason_phrase;
+
+ // Check Session Status.
+ if (this._status === C.STATUS_TERMINATED) {
+ throw new Exceptions.InvalidStateError(this._status);
+ }
+
+ switch (this._status) {
+ // - UAC -
+ case C.STATUS_NULL:
+ case C.STATUS_INVITE_SENT:
+ case C.STATUS_1XX_RECEIVED: {
+ logger.debug('canceling session');
+
+ if (status_code && (status_code < 200 || status_code >= 700)) {
+ throw new TypeError(`Invalid status_code: ${status_code}`);
+ } else if (status_code) {
+ reason_phrase =
+ reason_phrase || JsSIP_C.REASON_PHRASE[status_code] || '';
+ cancel_reason = `SIP ;cause=${status_code} ;text="${reason_phrase}"`;
+ }
+
+ // Check Session Status.
+ if (
+ this._status === C.STATUS_NULL ||
+ this._status === C.STATUS_INVITE_SENT
+ ) {
+ this._is_canceled = true;
+ this._cancel_reason = cancel_reason;
+ } else if (this._status === C.STATUS_1XX_RECEIVED) {
+ this._request.cancel(cancel_reason);
+ }
+
+ this._status = C.STATUS_CANCELED;
+
+ this._failed('local', null, JsSIP_C.causes.CANCELED);
+ break;
+ }
+
+ // - UAS -
+ case C.STATUS_WAITING_FOR_ANSWER:
+ case C.STATUS_ANSWERED: {
+ logger.debug('rejecting session');
+
+ status_code = status_code || 480;
+
+ if (status_code < 300 || status_code >= 700) {
+ throw new TypeError(`Invalid status_code: ${status_code}`);
+ }
+
+ this._request.reply(status_code, reason_phrase, extraHeaders, body);
+ this._failed('local', null, JsSIP_C.causes.REJECTED);
+ break;
+ }
+
+ case C.STATUS_WAITING_FOR_ACK:
+ case C.STATUS_CONFIRMED: {
+ logger.debug('terminating session');
+
+ reason_phrase =
+ options.reason_phrase || JsSIP_C.REASON_PHRASE[status_code] || '';
+
+ if (status_code && (status_code < 200 || status_code >= 700)) {
+ throw new TypeError(`Invalid status_code: ${status_code}`);
+ } else if (status_code) {
+ extraHeaders.push(
+ `Reason: SIP ;cause=${status_code}; text="${reason_phrase}"`
+ );
+ }
+
+ /* RFC 3261 section 15 (Terminating a session):
+ *
+ * "...the callee's UA MUST NOT send a BYE on a confirmed dialog
+ * until it has received an ACK for its 2xx response or until the server
+ * transaction times out."
+ */
+ if (
+ this._status === C.STATUS_WAITING_FOR_ACK &&
+ this._direction === 'incoming' &&
+ this._request.server_transaction.state !==
+ Transactions.C.STATUS_TERMINATED
+ ) {
+ // Save the dialog for later restoration.
+ const dialog = this._dialog;
+
+ // Send the BYE as soon as the ACK is received...
+ this.receiveRequest = ({ method }) => {
+ if (method === JsSIP_C.ACK) {
+ this.sendRequest(JsSIP_C.BYE, {
+ extraHeaders,
+ body,
+ });
+ dialog.terminate();
+ }
+ };
+
+ // .., or when the INVITE transaction times out
+ this._request.server_transaction.on('stateChanged', () => {
+ if (
+ this._request.server_transaction.state ===
+ Transactions.C.STATUS_TERMINATED
+ ) {
+ this.sendRequest(JsSIP_C.BYE, {
+ extraHeaders,
+ body,
+ });
+ dialog.terminate();
+ }
+ });
+
+ this._ended('local', null, cause);
+
+ // Restore the dialog into 'this' in order to be able to send the in-dialog BYE :-).
+ this._dialog = dialog;
+
+ // Restore the dialog into 'ua' so the ACK can reach 'this' session.
+ this._ua.newDialog(dialog);
+ } else {
+ this.sendRequest(JsSIP_C.BYE, {
+ extraHeaders,
+ body,
+ });
+
+ this._ended('local', null, cause);
+ }
+ }
+ }
+ }
+
+ sendDTMF(tones, options = {}) {
+ logger.debug('sendDTMF() | tones: %s', tones);
+
+ let duration = options.duration || null;
+ let interToneGap = options.interToneGap || null;
+ const transportType = options.transportType || JsSIP_C.DTMF_TRANSPORT.INFO;
+
+ if (tones === undefined) {
+ throw new TypeError('Not enough arguments');
+ }
+
+ // Check Session Status.
+ if (
+ this._status !== C.STATUS_CONFIRMED &&
+ this._status !== C.STATUS_WAITING_FOR_ACK &&
+ this._status !== C.STATUS_1XX_RECEIVED
+ ) {
+ throw new Exceptions.InvalidStateError(this._status);
+ }
+
+ // Check Transport type.
+ if (
+ transportType !== JsSIP_C.DTMF_TRANSPORT.INFO &&
+ transportType !== JsSIP_C.DTMF_TRANSPORT.RFC2833
+ ) {
+ throw new TypeError(`invalid transportType: ${transportType}`);
+ }
+
+ // Convert to string.
+ if (typeof tones === 'number') {
+ tones = tones.toString();
+ }
+
+ // Check tones.
+ if (
+ !tones ||
+ typeof tones !== 'string' ||
+ !tones.match(/^[0-9A-DR#*,]+$/i)
+ ) {
+ throw new TypeError(`Invalid tones: ${tones}`);
+ }
+
+ // Check duration.
+ if (duration && !Utils.isDecimal(duration)) {
+ throw new TypeError(`Invalid tone duration: ${duration}`);
+ } else if (!duration) {
+ duration = RTCSession_DTMF.C.DEFAULT_DURATION;
+ } else if (duration < RTCSession_DTMF.C.MIN_DURATION) {
+ logger.debug(
+ `"duration" value is lower than the minimum allowed, setting it to ${RTCSession_DTMF.C.MIN_DURATION} milliseconds`
+ );
+ duration = RTCSession_DTMF.C.MIN_DURATION;
+ } else if (duration > RTCSession_DTMF.C.MAX_DURATION) {
+ logger.debug(
+ `"duration" value is greater than the maximum allowed, setting it to ${RTCSession_DTMF.C.MAX_DURATION} milliseconds`
+ );
+ duration = RTCSession_DTMF.C.MAX_DURATION;
+ } else {
+ duration = Math.abs(duration);
+ }
+ options.duration = duration;
+
+ // Check interToneGap.
+ if (interToneGap && !Utils.isDecimal(interToneGap)) {
+ throw new TypeError(`Invalid interToneGap: ${interToneGap}`);
+ } else if (!interToneGap) {
+ interToneGap = RTCSession_DTMF.C.DEFAULT_INTER_TONE_GAP;
+ } else if (interToneGap < RTCSession_DTMF.C.MIN_INTER_TONE_GAP) {
+ logger.debug(
+ `"interToneGap" value is lower than the minimum allowed, setting it to ${RTCSession_DTMF.C.MIN_INTER_TONE_GAP} milliseconds`
+ );
+ interToneGap = RTCSession_DTMF.C.MIN_INTER_TONE_GAP;
+ } else {
+ interToneGap = Math.abs(interToneGap);
+ }
+
+ // RFC2833. Let RTCDTMFSender enqueue the DTMFs.
+ if (transportType === JsSIP_C.DTMF_TRANSPORT.RFC2833) {
+ // Send DTMF in current audio RTP stream.
+ const sender = this._getDTMFRTPSender();
+
+ if (sender) {
+ // Add remaining buffered tones.
+ tones = sender.toneBuffer + tones;
+ // Insert tones.
+ sender.insertDTMF(tones, duration, interToneGap);
+ }
+
+ return;
+ }
+
+ if (this._tones) {
+ // Tones are already queued, just add to the queue.
+ this._tones += tones;
+
+ return;
+ }
+
+ this._tones = tones;
+
+ // Send the first tone.
+ _sendDTMF.call(this);
+
+ function _sendDTMF() {
+ let timeout;
+
+ if (this._status === C.STATUS_TERMINATED || !this._tones) {
+ // Stop sending DTMF.
+ this._tones = null;
+
+ return;
+ }
+
+ // Retrieve the next tone.
+ const tone = this._tones[0];
+
+ // Remove the tone from this._tones.
+ this._tones = this._tones.substring(1);
+
+ if (tone === ',') {
+ timeout = 2000;
+ } else {
+ // Send DTMF via SIP INFO messages.
+ const dtmf = new RTCSession_DTMF(this);
+
+ options.eventHandlers = {
+ onFailed: () => {
+ this._tones = null;
+ },
+ };
+ dtmf.send(tone, options);
+ timeout = duration + interToneGap;
+ }
+
+ // Set timeout for the next tone.
+ setTimeout(_sendDTMF.bind(this), timeout);
+ }
+ }
+
+ sendInfo(contentType, body, options = {}) {
+ logger.debug('sendInfo()');
+
+ // Check Session Status.
+ if (
+ this._status !== C.STATUS_CONFIRMED &&
+ this._status !== C.STATUS_WAITING_FOR_ACK &&
+ this._status !== C.STATUS_1XX_RECEIVED
+ ) {
+ throw new Exceptions.InvalidStateError(this._status);
+ }
+
+ const info = new RTCSession_Info(this);
+
+ info.send(contentType, body, options);
+ }
+
+ /**
+ * Mute
+ */
+ mute(options = { audio: true, video: false }) {
+ logger.debug('mute()');
+
+ let audioMuted = false,
+ videoMuted = false;
+
+ if (this._audioMuted === false && options.audio) {
+ audioMuted = true;
+ this._audioMuted = true;
+ this._toggleMuteAudio(true);
+ }
+
+ if (this._videoMuted === false && options.video) {
+ videoMuted = true;
+ this._videoMuted = true;
+ this._toggleMuteVideo(true);
+ }
+
+ if (audioMuted === true || videoMuted === true) {
+ this._onmute({
+ audio: audioMuted,
+ video: videoMuted,
+ });
+ }
+ }
+
+ /**
+ * Unmute
+ */
+ unmute(options = { audio: true, video: true }) {
+ logger.debug('unmute()');
+
+ let audioUnMuted = false,
+ videoUnMuted = false;
+
+ if (this._audioMuted === true && options.audio) {
+ audioUnMuted = true;
+ this._audioMuted = false;
+
+ if (this._localHold === false) {
+ this._toggleMuteAudio(false);
+ }
+ }
+
+ if (this._videoMuted === true && options.video) {
+ videoUnMuted = true;
+ this._videoMuted = false;
+
+ if (this._localHold === false) {
+ this._toggleMuteVideo(false);
+ }
+ }
+
+ if (audioUnMuted === true || videoUnMuted === true) {
+ this._onunmute({
+ audio: audioUnMuted,
+ video: videoUnMuted,
+ });
+ }
+ }
+
+ /**
+ * Hold
+ */
+ hold(options = {}, done) {
+ logger.debug('hold()');
+
+ if (
+ this._status !== C.STATUS_WAITING_FOR_ACK &&
+ this._status !== C.STATUS_CONFIRMED
+ ) {
+ return false;
+ }
+
+ if (this._localHold === true) {
+ return false;
+ }
+
+ if (!this.isReadyToReOffer()) {
+ return false;
+ }
+
+ this._localHold = true;
+ this._onhold('local');
+
+ const eventHandlers = {
+ succeeded: () => {
+ if (done) {
+ done();
+ }
+ },
+ failed: () => {
+ this.terminate({
+ cause: JsSIP_C.causes.WEBRTC_ERROR,
+ status_code: 500,
+ reason_phrase: 'Hold Failed',
+ });
+ },
+ };
+
+ if (options.useUpdate) {
+ this._sendUpdate({
+ sdpOffer: true,
+ eventHandlers,
+ extraHeaders: options.extraHeaders,
+ });
+ } else {
+ this._sendReinvite({
+ eventHandlers,
+ extraHeaders: options.extraHeaders,
+ });
+ }
+
+ return true;
+ }
+
+ unhold(options = {}, done) {
+ logger.debug('unhold()');
+
+ if (
+ this._status !== C.STATUS_WAITING_FOR_ACK &&
+ this._status !== C.STATUS_CONFIRMED
+ ) {
+ return false;
+ }
+
+ if (this._localHold === false) {
+ return false;
+ }
+
+ if (!this.isReadyToReOffer()) {
+ return false;
+ }
+
+ this._localHold = false;
+ this._onunhold('local');
+
+ const eventHandlers = {
+ succeeded: () => {
+ if (done) {
+ done();
+ }
+ },
+ failed: () => {
+ this.terminate({
+ cause: JsSIP_C.causes.WEBRTC_ERROR,
+ status_code: 500,
+ reason_phrase: 'Unhold Failed',
+ });
+ },
+ };
+
+ if (options.useUpdate) {
+ this._sendUpdate({
+ sdpOffer: true,
+ eventHandlers,
+ extraHeaders: options.extraHeaders,
+ });
+ } else {
+ this._sendReinvite({
+ eventHandlers,
+ extraHeaders: options.extraHeaders,
+ });
+ }
+
+ return true;
+ }
+
+ renegotiate(options = {}, done) {
+ logger.debug('renegotiate()');
+
+ const rtcOfferConstraints = options.rtcOfferConstraints || null;
+
+ if (
+ this._status !== C.STATUS_WAITING_FOR_ACK &&
+ this._status !== C.STATUS_CONFIRMED
+ ) {
+ return false;
+ }
+
+ if (!this.isReadyToReOffer()) {
+ return false;
+ }
+
+ const eventHandlers = {
+ succeeded: () => {
+ if (done) {
+ done();
+ }
+ },
+ failed: () => {
+ this.terminate({
+ cause: JsSIP_C.causes.WEBRTC_ERROR,
+ status_code: 500,
+ reason_phrase: 'Media Renegotiation Failed',
+ });
+ },
+ };
+
+ this._setLocalMediaStatus();
+
+ if (options.useUpdate) {
+ this._sendUpdate({
+ sdpOffer: true,
+ eventHandlers,
+ rtcOfferConstraints,
+ extraHeaders: options.extraHeaders,
+ });
+ } else {
+ this._sendReinvite({
+ eventHandlers,
+ rtcOfferConstraints,
+ extraHeaders: options.extraHeaders,
+ });
+ }
+
+ return true;
+ }
+
+ /**
+ * Refer
+ */
+ refer(target, options) {
+ logger.debug('refer()');
+
+ const originalTarget = target;
+
+ if (
+ this._status !== C.STATUS_WAITING_FOR_ACK &&
+ this._status !== C.STATUS_CONFIRMED
+ ) {
+ return false;
+ }
+
+ // Check target validity.
+ target = this._ua.normalizeTarget(target);
+ if (!target) {
+ throw new TypeError(`Invalid target: ${originalTarget}`);
+ }
+
+ const referSubscriber = new RTCSession_ReferSubscriber(this);
+
+ referSubscriber.sendRefer(target, options);
+
+ // Store in the map.
+ const id = referSubscriber.id;
+
+ this._referSubscribers[id] = referSubscriber;
+
+ // Listen for ending events so we can remove it from the map.
+ referSubscriber.on('requestFailed', () => {
+ delete this._referSubscribers[id];
+ });
+ referSubscriber.on('accepted', () => {
+ delete this._referSubscribers[id];
+ });
+ referSubscriber.on('failed', () => {
+ delete this._referSubscribers[id];
+ });
+
+ return referSubscriber;
+ }
+
+ /**
+ * Send a generic in-dialog Request
+ */
+ sendRequest(method, options) {
+ logger.debug('sendRequest()');
+
+ if (this._dialog) {
+ return this._dialog.sendRequest(method, options);
+ } else {
+ const dialogsArray = Object.values(this._earlyDialogs);
+
+ if (dialogsArray.length > 0) {
+ return dialogsArray[0].sendRequest(method, options);
+ }
+
+ logger.warn('sendRequest() | no valid early dialog found');
+
+ return;
+ }
+ }
+
+ /**
+ * In dialog Request Reception
+ */
+ receiveRequest(request) {
+ logger.debug('receiveRequest()');
+
+ if (request.method === JsSIP_C.CANCEL) {
+ /* RFC3261 15 States that a UAS may have accepted an invitation while a CANCEL
+ * was in progress and that the UAC MAY continue with the session established by
+ * any 2xx response, or MAY terminate with BYE. JsSIP does continue with the
+ * established session. So the CANCEL is processed only if the session is not yet
+ * established.
+ */
+
+ /*
+ * Terminate the whole session in case the user didn't accept (or yet send the answer)
+ * nor reject the request opening the session.
+ */
+ if (
+ this._status === C.STATUS_WAITING_FOR_ANSWER ||
+ this._status === C.STATUS_ANSWERED
+ ) {
+ this._status = C.STATUS_CANCELED;
+ this._request.reply(487);
+ this._failed('remote', request, JsSIP_C.causes.CANCELED);
+ }
+ } else {
+ // Requests arriving here are in-dialog requests.
+ switch (request.method) {
+ case JsSIP_C.ACK: {
+ if (this._status !== C.STATUS_WAITING_FOR_ACK) {
+ return;
+ }
+
+ // Update signaling status.
+ this._status = C.STATUS_CONFIRMED;
+
+ clearTimeout(this._timers.ackTimer);
+ clearTimeout(this._timers.invite2xxTimer);
+
+ if (this._late_sdp) {
+ if (!request.body) {
+ this.terminate({
+ cause: JsSIP_C.causes.MISSING_SDP,
+ status_code: 400,
+ });
+ break;
+ }
+
+ const e = {
+ originator: 'remote',
+ type: 'answer',
+ sdp: request.body,
+ };
+
+ logger.debug('emit "sdp"');
+ this.emit('sdp', e);
+
+ const answer = new RTCSessionDescription({
+ type: 'answer',
+ sdp: e.sdp,
+ });
+
+ this._connectionPromiseQueue = this._connectionPromiseQueue
+ .then(() => this._connection.setRemoteDescription(answer))
+ .then(() => {
+ if (!this._is_confirmed) {
+ this._confirmed('remote', request);
+ }
+ })
+ .catch(error => {
+ this.terminate({
+ cause: JsSIP_C.causes.BAD_MEDIA_DESCRIPTION,
+ status_code: 488,
+ });
+
+ logger.warn(
+ 'emit "peerconnection:setremotedescriptionfailed" [error:%o]',
+ error
+ );
+ this.emit('peerconnection:setremotedescriptionfailed', error);
+ });
+ } else if (!this._is_confirmed) {
+ this._confirmed('remote', request);
+ }
+
+ break;
+ }
+ case JsSIP_C.BYE: {
+ if (
+ this._status === C.STATUS_CONFIRMED ||
+ this._status === C.STATUS_WAITING_FOR_ACK
+ ) {
+ request.reply(200);
+ this._ended('remote', request, JsSIP_C.causes.BYE);
+ } else if (
+ this._status === C.STATUS_INVITE_RECEIVED ||
+ this._status === C.STATUS_WAITING_FOR_ANSWER
+ ) {
+ request.reply(200);
+ this._request.reply(487, 'BYE Received');
+ this._ended('remote', request, JsSIP_C.causes.BYE);
+ } else {
+ request.reply(403, 'Wrong Status');
+ }
+ break;
+ }
+ case JsSIP_C.INVITE: {
+ if (this._status === C.STATUS_CONFIRMED) {
+ if (request.hasHeader('replaces')) {
+ this._receiveReplaces(request);
+ } else {
+ this._receiveReinvite(request);
+ }
+ } else {
+ request.reply(403, 'Wrong Status');
+ }
+ break;
+ }
+ case JsSIP_C.INFO: {
+ if (
+ this._status === C.STATUS_1XX_RECEIVED ||
+ this._status === C.STATUS_WAITING_FOR_ANSWER ||
+ this._status === C.STATUS_ANSWERED ||
+ this._status === C.STATUS_WAITING_FOR_ACK ||
+ this._status === C.STATUS_CONFIRMED
+ ) {
+ const contentType = request.hasHeader('Content-Type')
+ ? request.getHeader('Content-Type').toLowerCase()
+ : undefined;
+
+ if (contentType && contentType.match(/^application\/dtmf-relay/i)) {
+ new RTCSession_DTMF(this).init_incoming(request);
+ } else if (contentType !== undefined) {
+ new RTCSession_Info(this).init_incoming(request);
+ } else {
+ request.reply(415);
+ }
+ } else {
+ request.reply(403, 'Wrong Status');
+ }
+ break;
+ }
+ case JsSIP_C.UPDATE: {
+ if (this._status === C.STATUS_CONFIRMED) {
+ this._receiveUpdate(request);
+ } else {
+ request.reply(403, 'Wrong Status');
+ }
+ break;
+ }
+ case JsSIP_C.REFER: {
+ if (this._status === C.STATUS_CONFIRMED) {
+ this._receiveRefer(request);
+ } else {
+ request.reply(403, 'Wrong Status');
+ }
+ break;
+ }
+ case JsSIP_C.NOTIFY: {
+ if (this._status === C.STATUS_CONFIRMED) {
+ this._receiveNotify(request);
+ } else {
+ request.reply(403, 'Wrong Status');
+ }
+ break;
+ }
+ default: {
+ request.reply(501);
+ }
+ }
+ }
+ }
+
+ /**
+ * Session Callbacks
+ */
+
+ onTransportError() {
+ logger.warn('onTransportError()');
+
+ if (this._status !== C.STATUS_TERMINATED) {
+ this.terminate({
+ status_code: 500,
+ reason_phrase: JsSIP_C.causes.CONNECTION_ERROR,
+ cause: JsSIP_C.causes.CONNECTION_ERROR,
+ });
+ }
+ }
+
+ onRequestTimeout() {
+ logger.warn('onRequestTimeout()');
+
+ if (this._status !== C.STATUS_TERMINATED) {
+ this.terminate({
+ status_code: 408,
+ reason_phrase: JsSIP_C.causes.REQUEST_TIMEOUT,
+ cause: JsSIP_C.causes.REQUEST_TIMEOUT,
+ });
+ }
+ }
+
+ onDialogError() {
+ logger.warn('onDialogError()');
+
+ if (this._status !== C.STATUS_TERMINATED) {
+ this.terminate({
+ status_code: 500,
+ reason_phrase: JsSIP_C.causes.DIALOG_ERROR,
+ cause: JsSIP_C.causes.DIALOG_ERROR,
+ });
+ }
+ }
+
+ // Called from DTMF handler.
+ newDTMF(data) {
+ logger.debug('newDTMF()');
+
+ this.emit('newDTMF', data);
+ }
+
+ // Called from Info handler.
+ newInfo(data) {
+ logger.debug('newInfo()');
+
+ this.emit('newInfo', data);
+ }
+
+ /**
+ * Check if RTCSession is ready for an outgoing re-INVITE or UPDATE with SDP.
+ */
+ isReadyToReOffer() {
+ if (!this._rtcReady) {
+ logger.debug('isReadyToReOffer() | internal WebRTC status not ready');
+
+ return false;
+ }
+
+ // No established yet.
+ if (!this._dialog) {
+ logger.debug('isReadyToReOffer() | session not established yet');
+
+ return false;
+ }
+
+ // Another INVITE transaction is in progress.
+ if (
+ this._dialog.uac_pending_reply === true ||
+ this._dialog.uas_pending_reply === true
+ ) {
+ logger.debug(
+ 'isReadyToReOffer() | there is another INVITE/UPDATE transaction in progress'
+ );
+
+ return false;
+ }
+
+ return true;
+ }
+
+ _close() {
+ logger.debug('close()');
+
+ // Close local MediaStream if it was not given by the user.
+ if (this._localMediaStream && this._localMediaStreamLocallyGenerated) {
+ logger.debug('close() | closing local MediaStream');
+
+ Utils.closeMediaStream(this._localMediaStream);
+ }
+
+ if (this._status === C.STATUS_TERMINATED) {
+ return;
+ }
+
+ this._status = C.STATUS_TERMINATED;
+
+ // Terminate RTC.
+ if (this._connection) {
+ try {
+ this._connection.close();
+ } catch (error) {
+ logger.warn('close() | error closing the RTCPeerConnection: %o', error);
+ }
+ }
+
+ // Terminate signaling.
+
+ // Clear SIP timers.
+ for (const timer in this._timers) {
+ if (Object.prototype.hasOwnProperty.call(this._timers, timer)) {
+ clearTimeout(this._timers[timer]);
+ }
+ }
+
+ // Clear Session Timers.
+ clearTimeout(this._sessionTimers.timer);
+
+ // Terminate confirmed dialog.
+ if (this._dialog) {
+ this._dialog.terminate();
+ delete this._dialog;
+ }
+
+ // Terminate early dialogs.
+ for (const dialog in this._earlyDialogs) {
+ if (Object.prototype.hasOwnProperty.call(this._earlyDialogs, dialog)) {
+ this._earlyDialogs[dialog].terminate();
+ delete this._earlyDialogs[dialog];
+ }
+ }
+
+ // Terminate REFER subscribers.
+ for (const subscriber in this._referSubscribers) {
+ if (
+ Object.prototype.hasOwnProperty.call(this._referSubscribers, subscriber)
+ ) {
+ delete this._referSubscribers[subscriber];
+ }
+ }
+
+ this._ua.destroyRTCSession(this);
+ }
+
+ /**
+ * Private API.
+ */
+
+ /**
+ * RFC3261 13.3.1.4
+ * Response retransmissions cannot be accomplished by transaction layer
+ * since it is destroyed when receiving the first 2xx answer
+ */
+ _setInvite2xxTimer(request, body) {
+ let timeout = Timers.T1;
+
+ function invite2xxRetransmission() {
+ if (this._status !== C.STATUS_WAITING_FOR_ACK) {
+ return;
+ }
+
+ request.reply(200, null, [`Contact: ${this._contact}`], body);
+
+ if (timeout < Timers.T2) {
+ timeout = timeout * 2;
+ if (timeout > Timers.T2) {
+ timeout = Timers.T2;
+ }
+ }
+
+ this._timers.invite2xxTimer = setTimeout(
+ invite2xxRetransmission.bind(this),
+ timeout
+ );
+ }
+
+ this._timers.invite2xxTimer = setTimeout(
+ invite2xxRetransmission.bind(this),
+ timeout
+ );
+ }
+
+ /**
+ * RFC3261 14.2
+ * If a UAS generates a 2xx response and never receives an ACK,
+ * it SHOULD generate a BYE to terminate the dialog.
+ */
+ _setACKTimer() {
+ this._timers.ackTimer = setTimeout(() => {
+ if (this._status === C.STATUS_WAITING_FOR_ACK) {
+ logger.debug('no ACK received, terminating the session');
+
+ clearTimeout(this._timers.invite2xxTimer);
+ this.sendRequest(JsSIP_C.BYE);
+ this._ended('remote', null, JsSIP_C.causes.NO_ACK);
+ }
+ }, Timers.TIMER_H);
+ }
+
+ _createRTCConnection(pcConfig, rtcConstraints) {
+ this._connection = new RTCPeerConnection(pcConfig, rtcConstraints);
+
+ this._connection.addEventListener('iceconnectionstatechange', () => {
+ const state = this._connection.iceConnectionState;
+
+ // TODO: Do more with different states.
+ if (state === 'failed') {
+ this.terminate({
+ cause: JsSIP_C.causes.RTP_TIMEOUT,
+ status_code: 408,
+ reason_phrase: JsSIP_C.causes.RTP_TIMEOUT,
+ });
+ }
+ });
+
+ logger.debug('emit "peerconnection"');
+
+ this.emit('peerconnection', {
+ peerconnection: this._connection,
+ });
+ }
+
+ _createLocalDescription(type, constraints) {
+ logger.debug('createLocalDescription()');
+
+ if (type !== 'offer' && type !== 'answer') {
+ throw new Error(`createLocalDescription() | invalid type "${type}"`);
+ }
+
+ const connection = this._connection;
+
+ this._rtcReady = false;
+
+ return (
+ Promise.resolve()
+ // Create Offer or Answer.
+ .then(() => {
+ if (type === 'offer') {
+ return connection.createOffer(constraints).catch(error => {
+ logger.warn(
+ 'emit "peerconnection:createofferfailed" [error:%o]',
+ error
+ );
+
+ this.emit('peerconnection:createofferfailed', error);
+
+ return Promise.reject(error);
+ });
+ } else {
+ return connection.createAnswer(constraints).catch(error => {
+ logger.warn(
+ 'emit "peerconnection:createanswerfailed" [error:%o]',
+ error
+ );
+
+ this.emit('peerconnection:createanswerfailed', error);
+
+ return Promise.reject(error);
+ });
+ }
+ })
+ // Set local description.
+ .then(desc => {
+ return connection.setLocalDescription(desc).catch(error => {
+ this._rtcReady = true;
+
+ logger.warn(
+ 'emit "peerconnection:setlocaldescriptionfailed" [error:%o]',
+ error
+ );
+
+ this.emit('peerconnection:setlocaldescriptionfailed', error);
+
+ return Promise.reject(error);
+ });
+ })
+ .then(() => {
+ // Resolve right away if 'pc.iceGatheringState' is 'complete'.
+ /**
+ * Resolve right away if:
+ * - 'connection.iceGatheringState' is 'complete' and no 'iceRestart' constraint is set.
+ * - 'connection.iceGatheringState' is 'gathering' and 'iceReady' is true.
+ */
+ const iceRestart = constraints && constraints.iceRestart;
+
+ if (
+ (connection.iceGatheringState === 'complete' && !iceRestart) ||
+ (connection.iceGatheringState === 'gathering' && this._iceReady)
+ ) {
+ this._rtcReady = true;
+
+ const e = {
+ originator: 'local',
+ type: type,
+ sdp: connection.localDescription.sdp,
+ };
+
+ logger.debug('emit "sdp"');
+
+ this.emit('sdp', e);
+
+ return Promise.resolve(e.sdp);
+ }
+
+ // Add 'pc.onicencandidate' event handler to resolve on last candidate.
+ return new Promise(resolve => {
+ let finished = false;
+ let iceCandidateListener;
+ let iceGatheringStateListener;
+
+ this._iceReady = false;
+
+ const ready = () => {
+ if (finished) {
+ return;
+ }
+
+ connection.removeEventListener(
+ 'icecandidate',
+ iceCandidateListener
+ );
+ connection.removeEventListener(
+ 'icegatheringstatechange',
+ iceGatheringStateListener
+ );
+
+ finished = true;
+ this._rtcReady = true;
+
+ // connection.iceGatheringState will still indicate 'gathering' and thus be blocking.
+ this._iceReady = true;
+
+ const e = {
+ originator: 'local',
+ type: type,
+ sdp: connection.localDescription.sdp,
+ };
+
+ logger.debug('emit "sdp"');
+
+ this.emit('sdp', e);
+
+ resolve(e.sdp);
+ };
+
+ connection.addEventListener(
+ 'icecandidate',
+ (iceCandidateListener = event => {
+ const candidate = event.candidate;
+
+ if (candidate) {
+ this.emit('icecandidate', {
+ candidate,
+ ready,
+ });
+ } else {
+ ready();
+ }
+ })
+ );
+
+ connection.addEventListener(
+ 'icegatheringstatechange',
+ (iceGatheringStateListener = () => {
+ if (connection.iceGatheringState === 'complete') {
+ ready();
+ }
+ })
+ );
+ });
+ })
+ );
+ }
+
+ /**
+ * Dialog Management
+ */
+ _createDialog(message, type, early) {
+ const local_tag = type === 'UAS' ? message.to_tag : message.from_tag;
+ const remote_tag = type === 'UAS' ? message.from_tag : message.to_tag;
+ const id = message.call_id + local_tag + remote_tag;
+
+ let early_dialog = this._earlyDialogs[id];
+
+ // Early Dialog.
+ if (early) {
+ if (early_dialog) {
+ return true;
+ } else {
+ early_dialog = new Dialog(this, message, type, Dialog.C.STATUS_EARLY);
+
+ // Dialog has been successfully created.
+ if (early_dialog.error) {
+ logger.debug(early_dialog.error);
+ this._failed('remote', message, JsSIP_C.causes.INTERNAL_ERROR);
+
+ return false;
+ } else {
+ this._earlyDialogs[id] = early_dialog;
+
+ return true;
+ }
+ }
+ }
+
+ // Confirmed Dialog.
+ else {
+ this._from_tag = message.from_tag;
+ this._to_tag = message.to_tag;
+
+ // In case the dialog is in _early_ state, update it.
+ if (early_dialog) {
+ early_dialog.update(message, type);
+ this._dialog = early_dialog;
+ delete this._earlyDialogs[id];
+
+ return true;
+ }
+
+ // Otherwise, create a _confirmed_ dialog.
+ const dialog = new Dialog(this, message, type);
+
+ if (dialog.error) {
+ logger.debug(dialog.error);
+ this._failed('remote', message, JsSIP_C.causes.INTERNAL_ERROR);
+
+ return false;
+ } else {
+ this._dialog = dialog;
+
+ return true;
+ }
+ }
+ }
+
+ /**
+ * In dialog INVITE Reception
+ */
+
+ _receiveReinvite(request) {
+ logger.debug('receiveReinvite()');
+
+ const contentType = request.hasHeader('Content-Type')
+ ? request.getHeader('Content-Type').toLowerCase()
+ : undefined;
+ const data = {
+ request,
+ callback: undefined,
+ reject: reject.bind(this),
+ };
+
+ let rejected = false;
+
+ function reject(options = {}) {
+ rejected = true;
+
+ const status_code = options.status_code || 403;
+ const reason_phrase = options.reason_phrase || '';
+ const extraHeaders = Utils.cloneArray(options.extraHeaders);
+
+ if (this._status !== C.STATUS_CONFIRMED) {
+ return false;
+ }
+
+ if (status_code < 300 || status_code >= 700) {
+ throw new TypeError(`Invalid status_code: ${status_code}`);
+ }
+
+ request.reply(status_code, reason_phrase, extraHeaders);
+ }
+
+ // Emit 'reinvite'.
+ this.emit('reinvite', data);
+
+ if (rejected) {
+ return;
+ }
+
+ this._late_sdp = false;
+
+ // Request without SDP.
+ if (!request.body) {
+ this._late_sdp = true;
+ if (this._remoteHold) {
+ this._remoteHold = false;
+ this._onunhold('remote');
+ }
+ this._connectionPromiseQueue = this._connectionPromiseQueue
+ .then(() =>
+ this._createLocalDescription('offer', this._rtcOfferConstraints)
+ )
+ .then(sdp => {
+ sendAnswer.call(this, sdp);
+ })
+ .catch(() => {
+ request.reply(500);
+ });
+
+ return;
+ }
+
+ // Request with SDP.
+ if (contentType !== 'application/sdp') {
+ logger.debug('invalid Content-Type');
+ request.reply(415);
+
+ return;
+ }
+
+ this._processInDialogSdpOffer(request)
+ // Send answer.
+ .then(desc => {
+ if (this._status === C.STATUS_TERMINATED) {
+ return;
+ }
+
+ sendAnswer.call(this, desc);
+ })
+ .catch(error => {
+ logger.warn(error);
+ });
+
+ function sendAnswer(desc) {
+ const extraHeaders = [`Contact: ${this._contact}`];
+
+ this._handleSessionTimersInIncomingRequest(request, extraHeaders);
+
+ if (this._late_sdp) {
+ desc = this._mangleOffer(desc);
+ }
+
+ request.reply(200, null, extraHeaders, desc, () => {
+ this._status = C.STATUS_WAITING_FOR_ACK;
+ this._setInvite2xxTimer(request, desc);
+ this._setACKTimer();
+ });
+
+ // If callback is given execute it.
+ if (typeof data.callback === 'function') {
+ data.callback();
+ }
+ }
+ }
+
+ /**
+ * In dialog UPDATE Reception
+ */
+ _receiveUpdate(request) {
+ logger.debug('receiveUpdate()');
+
+ const contentType = request.hasHeader('Content-Type')
+ ? request.getHeader('Content-Type').toLowerCase()
+ : undefined;
+ const data = {
+ request,
+ callback: undefined,
+ reject: reject.bind(this),
+ };
+
+ let rejected = false;
+
+ function reject(options = {}) {
+ rejected = true;
+
+ const status_code = options.status_code || 403;
+ const reason_phrase = options.reason_phrase || '';
+ const extraHeaders = Utils.cloneArray(options.extraHeaders);
+
+ if (this._status !== C.STATUS_CONFIRMED) {
+ return false;
+ }
+
+ if (status_code < 300 || status_code >= 700) {
+ throw new TypeError(`Invalid status_code: ${status_code}`);
+ }
+
+ request.reply(status_code, reason_phrase, extraHeaders);
+ }
+
+ // Emit 'update'.
+ this.emit('update', data);
+
+ if (rejected) {
+ return;
+ }
+
+ if (!request.body) {
+ sendAnswer.call(this, null);
+
+ return;
+ }
+
+ if (contentType !== 'application/sdp') {
+ logger.debug('invalid Content-Type');
+
+ request.reply(415);
+
+ return;
+ }
+
+ this._processInDialogSdpOffer(request)
+ // Send answer.
+ .then(desc => {
+ if (this._status === C.STATUS_TERMINATED) {
+ return;
+ }
+
+ sendAnswer.call(this, desc);
+ })
+ .catch(error => {
+ logger.warn(error);
+ });
+
+ function sendAnswer(desc) {
+ const extraHeaders = [`Contact: ${this._contact}`];
+
+ this._handleSessionTimersInIncomingRequest(request, extraHeaders);
+
+ request.reply(200, null, extraHeaders, desc);
+
+ // If callback is given execute it.
+ if (typeof data.callback === 'function') {
+ data.callback();
+ }
+ }
+ }
+
+ _processInDialogSdpOffer(request) {
+ logger.debug('_processInDialogSdpOffer()');
+
+ const sdp = request.parseSDP();
+
+ let hold = false;
+
+ for (const m of sdp.media) {
+ if (holdMediaTypes.indexOf(m.type) === -1) {
+ continue;
+ }
+
+ const direction = m.direction || sdp.direction || 'sendrecv';
+
+ if (direction === 'sendonly' || direction === 'inactive') {
+ hold = true;
+ }
+ // If at least one of the streams is active don't emit 'hold'.
+ else {
+ hold = false;
+ break;
+ }
+ }
+
+ const e = { originator: 'remote', type: 'offer', sdp: request.body };
+
+ logger.debug('emit "sdp"');
+ this.emit('sdp', e);
+
+ const offer = new RTCSessionDescription({ type: 'offer', sdp: e.sdp });
+
+ this._connectionPromiseQueue = this._connectionPromiseQueue
+ // Set remote description.
+ .then(() => {
+ if (this._status === C.STATUS_TERMINATED) {
+ throw new Error('terminated');
+ }
+
+ return this._connection.setRemoteDescription(offer).catch(error => {
+ request.reply(488);
+ logger.warn(
+ 'emit "peerconnection:setremotedescriptionfailed" [error:%o]',
+ error
+ );
+
+ this.emit('peerconnection:setremotedescriptionfailed', error);
+
+ throw error;
+ });
+ })
+ .then(() => {
+ if (this._status === C.STATUS_TERMINATED) {
+ throw new Error('terminated');
+ }
+
+ if (this._remoteHold === true && hold === false) {
+ this._remoteHold = false;
+ this._onunhold('remote');
+ } else if (this._remoteHold === false && hold === true) {
+ this._remoteHold = true;
+ this._onhold('remote');
+ }
+ })
+ // Create local description.
+ .then(() => {
+ if (this._status === C.STATUS_TERMINATED) {
+ throw new Error('terminated');
+ }
+
+ return this._createLocalDescription(
+ 'answer',
+ this._rtcAnswerConstraints
+ ).catch(error => {
+ request.reply(500);
+ logger.warn(
+ 'emit "peerconnection:createtelocaldescriptionfailed" [error:%o]',
+ error
+ );
+
+ throw error;
+ });
+ })
+ .catch(error => {
+ logger.warn('_processInDialogSdpOffer() failed [error: %o]', error);
+ });
+
+ return this._connectionPromiseQueue;
+ }
+
+ /**
+ * In dialog Refer Reception
+ */
+ _receiveRefer(request) {
+ logger.debug('receiveRefer()');
+
+ if (!request.refer_to) {
+ logger.debug('no Refer-To header field present in REFER');
+ request.reply(400);
+
+ return;
+ }
+
+ if (request.refer_to.uri.scheme !== JsSIP_C.SIP) {
+ logger.debug('Refer-To header field points to a non-SIP URI scheme');
+ request.reply(416);
+
+ return;
+ }
+
+ // Reply before the transaction timer expires.
+ request.reply(202);
+
+ const notifier = new RTCSession_ReferNotifier(this, request.cseq);
+
+ logger.debug('emit "refer"');
+
+ // Emit 'refer'.
+ this.emit('refer', {
+ request,
+ accept: (initCallback, options) => {
+ accept.call(this, initCallback, options);
+ },
+ reject: () => {
+ reject.call(this);
+ },
+ });
+
+ function accept(initCallback, options = {}) {
+ initCallback = typeof initCallback === 'function' ? initCallback : null;
+
+ if (
+ this._status !== C.STATUS_WAITING_FOR_ACK &&
+ this._status !== C.STATUS_CONFIRMED
+ ) {
+ return false;
+ }
+
+ const session = new RTCSession(this._ua);
+
+ session.on('progress', ({ response }) => {
+ notifier.notify(response.status_code, response.reason_phrase);
+ });
+
+ session.on('accepted', ({ response }) => {
+ notifier.notify(response.status_code, response.reason_phrase);
+ });
+
+ session.on('_failed', ({ message, cause }) => {
+ if (message) {
+ notifier.notify(message.status_code, message.reason_phrase);
+ } else {
+ notifier.notify(487, cause);
+ }
+ });
+
+ // Consider the Replaces header present in the Refer-To URI.
+ if (request.refer_to.uri.hasHeader('replaces')) {
+ const replaces = decodeURIComponent(
+ request.refer_to.uri.getHeader('replaces')
+ );
+
+ options.extraHeaders = Utils.cloneArray(options.extraHeaders);
+ options.extraHeaders.push(`Replaces: ${replaces}`);
+ }
+
+ session.connect(request.refer_to.uri.toAor(), options, initCallback);
+ }
+
+ function reject() {
+ notifier.notify(603);
+ }
+ }
+
+ /**
+ * In dialog Notify Reception
+ */
+ _receiveNotify(request) {
+ logger.debug('receiveNotify()');
+
+ if (!request.event) {
+ request.reply(400);
+ }
+
+ switch (request.event.event) {
+ case 'refer': {
+ let id;
+ let referSubscriber;
+
+ if (request.event.params && request.event.params.id) {
+ id = request.event.params.id;
+ referSubscriber = this._referSubscribers[id];
+ } else if (Object.keys(this._referSubscribers).length === 1) {
+ referSubscriber =
+ this._referSubscribers[Object.keys(this._referSubscribers)[0]];
+ } else {
+ request.reply(400, 'Missing event id parameter');
+
+ return;
+ }
+
+ if (!referSubscriber) {
+ request.reply(481, 'Subscription does not exist');
+
+ return;
+ }
+
+ referSubscriber.receiveNotify(request);
+ request.reply(200);
+
+ break;
+ }
+
+ default: {
+ request.reply(489);
+ }
+ }
+ }
+
+ /**
+ * INVITE with Replaces Reception
+ */
+ _receiveReplaces(request) {
+ logger.debug('receiveReplaces()');
+
+ function accept(initCallback) {
+ if (
+ this._status !== C.STATUS_WAITING_FOR_ACK &&
+ this._status !== C.STATUS_CONFIRMED
+ ) {
+ return false;
+ }
+
+ const session = new RTCSession(this._ua);
+
+ // Terminate the current session when the new one is confirmed.
+ session.on('confirmed', () => {
+ this.terminate();
+ });
+
+ session.init_incoming(request, initCallback);
+ }
+
+ function reject() {
+ logger.debug('Replaced INVITE rejected by the user');
+ request.reply(486);
+ }
+
+ // Emit 'replace'.
+ this.emit('replaces', {
+ request,
+ accept: initCallback => {
+ accept.call(this, initCallback);
+ },
+ reject: () => {
+ reject.call(this);
+ },
+ });
+ }
+
+ /**
+ * Initial Request Sender
+ */
+ _sendInitialRequest(mediaConstraints, rtcOfferConstraints, mediaStream) {
+ const request_sender = new RequestSender(this._ua, this._request, {
+ onRequestTimeout: () => {
+ this.onRequestTimeout();
+ },
+ onTransportError: () => {
+ this.onTransportError();
+ },
+ // Update the request on authentication.
+ onAuthenticated: request => {
+ this._request = request;
+ },
+ onReceiveResponse: response => {
+ this._receiveInviteResponse(response);
+ },
+ });
+
+ // This Promise is resolved within the next iteration, so the app has now
+ // a chance to set events such as 'peerconnection' and 'connecting'.
+ Promise.resolve()
+ // Get a stream if required.
+ .then(() => {
+ // A stream is given, let the app set events such as 'peerconnection' and 'connecting'.
+ if (mediaStream) {
+ return mediaStream;
+ }
+ // Request for user media access.
+ else if (mediaConstraints.audio || mediaConstraints.video) {
+ this._localMediaStreamLocallyGenerated = true;
+
+ return navigator.mediaDevices
+ .getUserMedia(mediaConstraints)
+ .catch(error => {
+ if (this._status === C.STATUS_TERMINATED) {
+ throw new Error('terminated');
+ }
+
+ this._failed(
+ 'local',
+ null,
+ JsSIP_C.causes.USER_DENIED_MEDIA_ACCESS
+ );
+
+ logger.warn('emit "getusermediafailed" [error:%o]', error);
+
+ this.emit('getusermediafailed', error);
+
+ throw error;
+ });
+ }
+ })
+ .then(stream => {
+ if (this._status === C.STATUS_TERMINATED) {
+ throw new Error('terminated');
+ }
+
+ this._localMediaStream = stream;
+
+ if (stream) {
+ stream.getTracks().forEach(track => {
+ this._connection.addTrack(track, stream);
+ });
+ }
+
+ // TODO: should this be triggered here?
+ this._connecting(this._request);
+
+ return this._createLocalDescription('offer', rtcOfferConstraints).catch(
+ error => {
+ this._failed('local', null, JsSIP_C.causes.WEBRTC_ERROR);
+
+ throw error;
+ }
+ );
+ })
+ .then(desc => {
+ if (this._is_canceled || this._status === C.STATUS_TERMINATED) {
+ throw new Error('terminated');
+ }
+
+ this._request.body = desc;
+ this._status = C.STATUS_INVITE_SENT;
+
+ logger.debug('emit "sending" [request:%o]', this._request);
+
+ // Emit 'sending' so the app can mangle the body before the request is sent.
+ this.emit('sending', {
+ request: this._request,
+ });
+
+ request_sender.send();
+ })
+ .catch(error => {
+ if (this._status === C.STATUS_TERMINATED) {
+ return;
+ }
+
+ logger.warn(error);
+ });
+ }
+
+ /**
+ * Get DTMF RTCRtpSender.
+ */
+ _getDTMFRTPSender() {
+ const sender = this._connection.getSenders().find(rtpSender => {
+ return rtpSender.track && rtpSender.track.kind === 'audio';
+ });
+
+ if (!(sender && sender.dtmf)) {
+ logger.warn('sendDTMF() | no local audio track to send DTMF with');
+
+ return;
+ }
+
+ return sender.dtmf;
+ }
+
+ /**
+ * Reception of Response for Initial INVITE
+ */
+ _receiveInviteResponse(response) {
+ logger.debug('receiveInviteResponse()');
+
+ // Handle 2XX retransmissions and responses from forked requests.
+ if (
+ this._dialog &&
+ response.status_code >= 200 &&
+ response.status_code <= 299
+ ) {
+ /*
+ * If it is a retransmission from the endpoint that established
+ * the dialog, send an ACK
+ */
+ if (
+ this._dialog.id.call_id === response.call_id &&
+ this._dialog.id.local_tag === response.from_tag &&
+ this._dialog.id.remote_tag === response.to_tag
+ ) {
+ this.sendRequest(JsSIP_C.ACK);
+
+ return;
+ }
+
+ // If not, send an ACK and terminate.
+ else {
+ const dialog = new Dialog(this, response, 'UAC');
+
+ if (dialog.error !== undefined) {
+ logger.debug(dialog.error);
+
+ return;
+ }
+
+ this.sendRequest(JsSIP_C.ACK);
+ this.sendRequest(JsSIP_C.BYE);
+
+ return;
+ }
+ }
+
+ // Proceed to cancellation if the user requested.
+ if (this._is_canceled) {
+ if (response.status_code >= 100 && response.status_code < 200) {
+ this._request.cancel(this._cancel_reason);
+ } else if (response.status_code >= 200 && response.status_code < 299) {
+ this._acceptAndTerminate(response);
+ }
+
+ return;
+ }
+
+ if (
+ this._status !== C.STATUS_INVITE_SENT &&
+ this._status !== C.STATUS_1XX_RECEIVED
+ ) {
+ return;
+ }
+
+ switch (true) {
+ case /^100$/.test(response.status_code): {
+ this._status = C.STATUS_1XX_RECEIVED;
+ break;
+ }
+ case /^1[0-9]{2}$/.test(response.status_code): {
+ // Do nothing with 1xx responses without To tag.
+ if (!response.to_tag) {
+ logger.debug('1xx response received without to tag');
+ break;
+ }
+
+ // Create Early Dialog if 1XX comes with contact.
+ if (response.hasHeader('contact')) {
+ // An error on dialog creation will fire 'failed' event.
+ if (!this._createDialog(response, 'UAC', true)) {
+ break;
+ }
+ }
+
+ this._status = C.STATUS_1XX_RECEIVED;
+
+ if (!response.body) {
+ this._progress('remote', response);
+ break;
+ }
+
+ const e = { originator: 'remote', type: 'answer', sdp: response.body };
+
+ logger.debug('emit "sdp"');
+ this.emit('sdp', e);
+
+ const answer = new RTCSessionDescription({
+ type: 'answer',
+ sdp: e.sdp,
+ });
+
+ this._connectionPromiseQueue = this._connectionPromiseQueue
+ .then(() => this._connection.setRemoteDescription(answer))
+ .then(() => this._progress('remote', response))
+ .catch(error => {
+ logger.warn(
+ 'emit "peerconnection:setremotedescriptionfailed" [error:%o]',
+ error
+ );
+
+ this.emit('peerconnection:setremotedescriptionfailed', error);
+ });
+ break;
+ }
+
+ case /^2[0-9]{2}$/.test(response.status_code): {
+ this._status = C.STATUS_CONFIRMED;
+
+ if (!response.body) {
+ this._acceptAndTerminate(response, 400, JsSIP_C.causes.MISSING_SDP);
+ this._failed(
+ 'remote',
+ response,
+ JsSIP_C.causes.BAD_MEDIA_DESCRIPTION
+ );
+ break;
+ }
+
+ // An error on dialog creation will fire 'failed' event.
+ if (!this._createDialog(response, 'UAC')) {
+ break;
+ }
+
+ const e = { originator: 'remote', type: 'answer', sdp: response.body };
+
+ logger.debug('emit "sdp"');
+ this.emit('sdp', e);
+
+ const answer = new RTCSessionDescription({
+ type: 'answer',
+ sdp: e.sdp,
+ });
+
+ this._connectionPromiseQueue = this._connectionPromiseQueue
+ .then(() => {
+ // Be ready for 200 with SDP after a 180/183 with SDP.
+ // We created a SDP 'answer' for it, so check the current signaling state.
+ if (this._connection.signalingState === 'stable') {
+ return this._connection
+ .createOffer(this._rtcOfferConstraints)
+ .then(offer => this._connection.setLocalDescription(offer))
+ .catch(error => {
+ this._acceptAndTerminate(response, 500, error.toString());
+ this._failed('local', response, JsSIP_C.causes.WEBRTC_ERROR);
+ });
+ }
+ })
+ .then(() => {
+ this._connection
+ .setRemoteDescription(answer)
+ .then(() => {
+ // Handle Session Timers.
+ this._handleSessionTimersInIncomingResponse(response);
+
+ this._accepted('remote', response);
+ this.sendRequest(JsSIP_C.ACK);
+ this._confirmed('local', null);
+ })
+ .catch(error => {
+ this._acceptAndTerminate(response, 488, 'Not Acceptable Here');
+ this._failed(
+ 'remote',
+ response,
+ JsSIP_C.causes.BAD_MEDIA_DESCRIPTION
+ );
+
+ logger.warn(
+ 'emit "peerconnection:setremotedescriptionfailed" [error:%o]',
+ error
+ );
+
+ this.emit('peerconnection:setremotedescriptionfailed', error);
+ });
+ });
+ break;
+ }
+
+ default: {
+ const cause = Utils.sipErrorCause(response.status_code);
+
+ this._failed('remote', response, cause);
+ }
+ }
+ }
+
+ /**
+ * Send Re-INVITE
+ */
+ _sendReinvite(options = {}) {
+ logger.debug('sendReinvite()');
+
+ const extraHeaders = Utils.cloneArray(options.extraHeaders);
+ const eventHandlers = Utils.cloneObject(options.eventHandlers);
+ const rtcOfferConstraints =
+ options.rtcOfferConstraints || this._rtcOfferConstraints || null;
+
+ let succeeded = false;
+
+ extraHeaders.push(`Contact: ${this._contact}`);
+ extraHeaders.push('Content-Type: application/sdp');
+
+ // Session Timers.
+ if (this._sessionTimers.running) {
+ extraHeaders.push(
+ `Session-Expires: ${this._sessionTimers.currentExpires};refresher=${this._sessionTimers.refresher ? 'uac' : 'uas'}`
+ );
+ }
+
+ this._connectionPromiseQueue = this._connectionPromiseQueue
+ .then(() => this._createLocalDescription('offer', rtcOfferConstraints))
+ .then(sdp => {
+ sdp = this._mangleOffer(sdp);
+
+ const e = { originator: 'local', type: 'offer', sdp };
+
+ logger.debug('emit "sdp"');
+ this.emit('sdp', e);
+
+ this.sendRequest(JsSIP_C.INVITE, {
+ extraHeaders,
+ body: sdp,
+ eventHandlers: {
+ onSuccessResponse: response => {
+ onSucceeded.call(this, response);
+ succeeded = true;
+ },
+ onErrorResponse: response => {
+ onFailed.call(this, response);
+ },
+ onTransportError: () => {
+ this.onTransportError(); // Do nothing because session ends.
+ },
+ onRequestTimeout: () => {
+ this.onRequestTimeout(); // Do nothing because session ends.
+ },
+ onDialogError: () => {
+ this.onDialogError(); // Do nothing because session ends.
+ },
+ },
+ });
+ })
+ .catch(() => {
+ onFailed();
+ });
+
+ function onSucceeded(response) {
+ if (this._status === C.STATUS_TERMINATED) {
+ return;
+ }
+
+ this.sendRequest(JsSIP_C.ACK);
+
+ // If it is a 2XX retransmission exit now.
+ if (succeeded) {
+ return;
+ }
+
+ // Handle Session Timers.
+ this._handleSessionTimersInIncomingResponse(response);
+
+ // Must have SDP answer.
+ if (!response.body) {
+ onFailed.call(this);
+
+ return;
+ } else if (
+ !response.hasHeader('Content-Type') ||
+ response.getHeader('Content-Type').toLowerCase() !== 'application/sdp'
+ ) {
+ onFailed.call(this);
+
+ return;
+ }
+
+ const e = { originator: 'remote', type: 'answer', sdp: response.body };
+
+ logger.debug('emit "sdp"');
+ this.emit('sdp', e);
+
+ const answer = new RTCSessionDescription({ type: 'answer', sdp: e.sdp });
+
+ this._connectionPromiseQueue = this._connectionPromiseQueue
+ .then(() => this._connection.setRemoteDescription(answer))
+ .then(() => {
+ if (eventHandlers.succeeded) {
+ eventHandlers.succeeded(response);
+ }
+ })
+ .catch(error => {
+ onFailed.call(this);
+
+ logger.warn(
+ 'emit "peerconnection:setremotedescriptionfailed" [error:%o]',
+ error
+ );
+
+ this.emit('peerconnection:setremotedescriptionfailed', error);
+ });
+ }
+
+ function onFailed(response) {
+ if (eventHandlers.failed) {
+ eventHandlers.failed(response);
+ }
+ }
+ }
+
+ /**
+ * Send UPDATE
+ */
+ _sendUpdate(options = {}) {
+ logger.debug('sendUpdate()');
+
+ const extraHeaders = Utils.cloneArray(options.extraHeaders);
+ const eventHandlers = Utils.cloneObject(options.eventHandlers);
+ const rtcOfferConstraints =
+ options.rtcOfferConstraints || this._rtcOfferConstraints || null;
+ const sdpOffer = options.sdpOffer || false;
+
+ let succeeded = false;
+
+ extraHeaders.push(`Contact: ${this._contact}`);
+
+ // Session Timers.
+ if (this._sessionTimers.running) {
+ extraHeaders.push(
+ `Session-Expires: ${this._sessionTimers.currentExpires};refresher=${this._sessionTimers.refresher ? 'uac' : 'uas'}`
+ );
+ }
+
+ if (sdpOffer) {
+ extraHeaders.push('Content-Type: application/sdp');
+
+ this._connectionPromiseQueue = this._connectionPromiseQueue
+ .then(() => this._createLocalDescription('offer', rtcOfferConstraints))
+ .then(sdp => {
+ sdp = this._mangleOffer(sdp);
+
+ const e = { originator: 'local', type: 'offer', sdp };
+
+ logger.debug('emit "sdp"');
+ this.emit('sdp', e);
+
+ this.sendRequest(JsSIP_C.UPDATE, {
+ extraHeaders,
+ body: sdp,
+ eventHandlers: {
+ onSuccessResponse: response => {
+ onSucceeded.call(this, response);
+ succeeded = true;
+ },
+ onErrorResponse: response => {
+ onFailed.call(this, response);
+ },
+ onTransportError: () => {
+ this.onTransportError(); // Do nothing because session ends.
+ },
+ onRequestTimeout: () => {
+ this.onRequestTimeout(); // Do nothing because session ends.
+ },
+ onDialogError: () => {
+ this.onDialogError(); // Do nothing because session ends.
+ },
+ },
+ });
+ })
+ .catch(() => {
+ onFailed.call(this);
+ });
+ }
+
+ // No SDP.
+ else {
+ this.sendRequest(JsSIP_C.UPDATE, {
+ extraHeaders,
+ eventHandlers: {
+ onSuccessResponse: response => {
+ onSucceeded.call(this, response);
+ },
+ onErrorResponse: response => {
+ onFailed.call(this, response);
+ },
+ onTransportError: () => {
+ this.onTransportError(); // Do nothing because session ends.
+ },
+ onRequestTimeout: () => {
+ this.onRequestTimeout(); // Do nothing because session ends.
+ },
+ onDialogError: () => {
+ this.onDialogError(); // Do nothing because session ends.
+ },
+ },
+ });
+ }
+
+ function onSucceeded(response) {
+ if (this._status === C.STATUS_TERMINATED) {
+ return;
+ }
+
+ // If it is a 2XX retransmission exit now.
+ if (succeeded) {
+ return;
+ }
+
+ // Handle Session Timers.
+ this._handleSessionTimersInIncomingResponse(response);
+
+ // Must have SDP answer.
+ if (sdpOffer) {
+ if (!response.body) {
+ onFailed.call(this);
+
+ return;
+ } else if (
+ !response.hasHeader('Content-Type') ||
+ response.getHeader('Content-Type').toLowerCase() !== 'application/sdp'
+ ) {
+ onFailed.call(this);
+
+ return;
+ }
+
+ const e = { originator: 'remote', type: 'answer', sdp: response.body };
+
+ logger.debug('emit "sdp"');
+ this.emit('sdp', e);
+
+ const answer = new RTCSessionDescription({
+ type: 'answer',
+ sdp: e.sdp,
+ });
+
+ this._connectionPromiseQueue = this._connectionPromiseQueue
+ .then(() => this._connection.setRemoteDescription(answer))
+ .then(() => {
+ if (eventHandlers.succeeded) {
+ eventHandlers.succeeded(response);
+ }
+ })
+ .catch(error => {
+ onFailed.call(this);
+
+ logger.warn(
+ 'emit "peerconnection:setremotedescriptionfailed" [error:%o]',
+ error
+ );
+
+ this.emit('peerconnection:setremotedescriptionfailed', error);
+ });
+ }
+ // No SDP answer.
+ else if (eventHandlers.succeeded) {
+ eventHandlers.succeeded(response);
+ }
+ }
+
+ function onFailed(response) {
+ if (eventHandlers.failed) {
+ eventHandlers.failed(response);
+ }
+ }
+ }
+
+ _acceptAndTerminate(response, status_code, reason_phrase) {
+ logger.debug('acceptAndTerminate()');
+
+ const extraHeaders = [];
+
+ if (status_code) {
+ reason_phrase = reason_phrase || JsSIP_C.REASON_PHRASE[status_code] || '';
+ extraHeaders.push(
+ `Reason: SIP ;cause=${status_code}; text="${reason_phrase}"`
+ );
+ }
+
+ // An error on dialog creation will fire 'failed' event.
+ if (this._dialog || this._createDialog(response, 'UAC')) {
+ this.sendRequest(JsSIP_C.ACK);
+ this.sendRequest(JsSIP_C.BYE, {
+ extraHeaders,
+ });
+ }
+
+ // Update session status.
+ this._status = C.STATUS_TERMINATED;
+ }
+
+ /**
+ * Correctly set the SDP direction attributes if the call is on local hold
+ */
+ _mangleOffer(sdp) {
+ if (!this._localHold && !this._remoteHold) {
+ return sdp;
+ }
+
+ sdp = sdp_transform.parse(sdp);
+
+ // Local hold.
+ if (this._localHold && !this._remoteHold) {
+ logger.debug('mangleOffer() | me on hold, mangling offer');
+ for (const m of sdp.media) {
+ if (holdMediaTypes.indexOf(m.type) === -1) {
+ continue;
+ }
+ if (!m.direction) {
+ m.direction = 'sendonly';
+ } else if (m.direction === 'sendrecv') {
+ m.direction = 'sendonly';
+ } else if (m.direction === 'recvonly') {
+ m.direction = 'inactive';
+ }
+ }
+ }
+ // Local and remote hold.
+ else if (this._localHold && this._remoteHold) {
+ logger.debug('mangleOffer() | both on hold, mangling offer');
+ for (const m of sdp.media) {
+ if (holdMediaTypes.indexOf(m.type) === -1) {
+ continue;
+ }
+ m.direction = 'inactive';
+ }
+ }
+ // Remote hold.
+ else if (this._remoteHold) {
+ logger.debug('mangleOffer() | remote on hold, mangling offer');
+ for (const m of sdp.media) {
+ if (holdMediaTypes.indexOf(m.type) === -1) {
+ continue;
+ }
+ if (!m.direction) {
+ m.direction = 'recvonly';
+ } else if (m.direction === 'sendrecv') {
+ m.direction = 'recvonly';
+ } else if (m.direction === 'recvonly') {
+ m.direction = 'inactive';
+ }
+ }
+ }
+
+ return sdp_transform.write(sdp);
+ }
+
+ _setLocalMediaStatus() {
+ let enableAudio = true,
+ enableVideo = true;
+
+ if (this._localHold || this._remoteHold) {
+ enableAudio = false;
+ enableVideo = false;
+ }
+
+ if (this._audioMuted) {
+ enableAudio = false;
+ }
+
+ if (this._videoMuted) {
+ enableVideo = false;
+ }
+
+ this._toggleMuteAudio(!enableAudio);
+ this._toggleMuteVideo(!enableVideo);
+ }
+
+ /**
+ * Handle SessionTimers for an incoming INVITE or UPDATE.
+ * @param {IncomingRequest} request
+ * @param {Array} responseExtraHeaders Extra headers for the 200 response.
+ */
+ _handleSessionTimersInIncomingRequest(request, responseExtraHeaders) {
+ if (!this._sessionTimers.enabled) {
+ return;
+ }
+
+ let session_expires_refresher;
+
+ if (
+ request.session_expires &&
+ request.session_expires >= JsSIP_C.MIN_SESSION_EXPIRES
+ ) {
+ this._sessionTimers.currentExpires = request.session_expires;
+ session_expires_refresher = request.session_expires_refresher || 'uas';
+ } else {
+ this._sessionTimers.currentExpires = this._sessionTimers.defaultExpires;
+ session_expires_refresher = 'uas';
+ }
+
+ responseExtraHeaders.push(
+ `Session-Expires: ${this._sessionTimers.currentExpires};refresher=${session_expires_refresher}`
+ );
+
+ this._sessionTimers.refresher = session_expires_refresher === 'uas';
+ this._runSessionTimer();
+ }
+
+ /**
+ * Handle SessionTimers for an incoming response to INVITE or UPDATE.
+ * @param {IncomingResponse} response
+ */
+ _handleSessionTimersInIncomingResponse(response) {
+ if (!this._sessionTimers.enabled) {
+ return;
+ }
+
+ let session_expires_refresher;
+
+ if (
+ response.session_expires &&
+ response.session_expires >= JsSIP_C.MIN_SESSION_EXPIRES
+ ) {
+ this._sessionTimers.currentExpires = response.session_expires;
+ session_expires_refresher = response.session_expires_refresher || 'uac';
+ } else {
+ this._sessionTimers.currentExpires = this._sessionTimers.defaultExpires;
+ session_expires_refresher = 'uac';
+ }
+
+ this._sessionTimers.refresher = session_expires_refresher === 'uac';
+ this._runSessionTimer();
+ }
+
+ _runSessionTimer() {
+ const expires = this._sessionTimers.currentExpires;
+
+ this._sessionTimers.running = true;
+
+ clearTimeout(this._sessionTimers.timer);
+
+ // I'm the refresher.
+ if (this._sessionTimers.refresher) {
+ this._sessionTimers.timer = setTimeout(() => {
+ if (this._status === C.STATUS_TERMINATED) {
+ return;
+ }
+
+ if (!this.isReadyToReOffer()) {
+ return;
+ }
+
+ logger.debug('runSessionTimer() | sending session refresh request');
+
+ if (this._sessionTimers.refreshMethod === JsSIP_C.UPDATE) {
+ this._sendUpdate();
+ } else {
+ this._sendReinvite();
+ }
+ }, expires * 500); // Half the given interval (as the RFC states).
+ }
+
+ // I'm not the refresher.
+ else {
+ this._sessionTimers.timer = setTimeout(() => {
+ if (this._status === C.STATUS_TERMINATED) {
+ return;
+ }
+
+ logger.warn(
+ 'runSessionTimer() | timer expired, terminating the session'
+ );
+
+ this.terminate({
+ cause: JsSIP_C.causes.REQUEST_TIMEOUT,
+ status_code: 408,
+ reason_phrase: 'Session Timer Expired',
+ });
+ }, expires * 1100);
+ }
+ }
+
+ _toggleMuteAudio(mute) {
+ const senders = this._connection.getSenders().filter(sender => {
+ return sender.track && sender.track.kind === 'audio';
+ });
+
+ for (const sender of senders) {
+ sender.track.enabled = !mute;
+ }
+ }
+
+ _toggleMuteVideo(mute) {
+ const senders = this._connection.getSenders().filter(sender => {
+ return sender.track && sender.track.kind === 'video';
+ });
+
+ for (const sender of senders) {
+ sender.track.enabled = !mute;
+ }
+ }
+
+ _newRTCSession(originator, request) {
+ logger.debug('newRTCSession()');
+
+ this._ua.newRTCSession(this, {
+ originator,
+ session: this,
+ request,
+ });
+ }
+
+ _connecting(request) {
+ logger.debug('session connecting');
- this._is_confirmed = true;
+ logger.debug('emit "connecting"');
- logger.debug('emit "confirmed"');
+ this.emit('connecting', {
+ request,
+ });
+ }
- this.emit('confirmed', {
- originator,
- ack : ack || null
- });
- }
+ _progress(originator, response) {
+ logger.debug('session progress');
- _ended(originator, message, cause)
- {
- logger.debug('session ended');
+ logger.debug('emit "progress"');
- this._end_time = new Date();
+ this.emit('progress', {
+ originator,
+ response: response || null,
+ });
+ }
- this._close();
+ _accepted(originator, message) {
+ logger.debug('session accepted');
- logger.debug('emit "ended"');
+ this._start_time = new Date();
- this.emit('ended', {
- originator,
- message : message || null,
- cause
- });
- }
+ logger.debug('emit "accepted"');
- _failed(originator, message, cause)
- {
- logger.debug('session failed');
+ this.emit('accepted', {
+ originator,
+ response: message || null,
+ });
+ }
- // Emit private '_failed' event first.
- logger.debug('emit "_failed"');
+ _confirmed(originator, ack) {
+ logger.debug('session confirmed');
- this.emit('_failed', {
- originator,
- message : message || null,
- cause
- });
+ this._is_confirmed = true;
- this._close();
+ logger.debug('emit "confirmed"');
- logger.debug('emit "failed"');
+ this.emit('confirmed', {
+ originator,
+ ack: ack || null,
+ });
+ }
- this.emit('failed', {
- originator,
- message : message || null,
- cause
- });
- }
+ _ended(originator, message, cause) {
+ logger.debug('session ended');
- _onhold(originator)
- {
- logger.debug('session onhold');
+ this._end_time = new Date();
- this._setLocalMediaStatus();
+ this._close();
- logger.debug('emit "hold"');
+ logger.debug('emit "ended"');
- this.emit('hold', {
- originator
- });
- }
+ this.emit('ended', {
+ originator,
+ message: message || null,
+ cause,
+ });
+ }
- _onunhold(originator)
- {
- logger.debug('session onunhold');
+ _failed(originator, message, cause) {
+ logger.debug('session failed');
- this._setLocalMediaStatus();
+ // Emit private '_failed' event first.
+ logger.debug('emit "_failed"');
- logger.debug('emit "unhold"');
+ this.emit('_failed', {
+ originator,
+ message: message || null,
+ cause,
+ });
- this.emit('unhold', {
- originator
- });
- }
+ this._close();
- _onmute({ audio, video })
- {
- logger.debug('session onmute');
+ logger.debug('emit "failed"');
- this._setLocalMediaStatus();
+ this.emit('failed', {
+ originator,
+ message: message || null,
+ cause,
+ });
+ }
- logger.debug('emit "muted"');
+ _onhold(originator) {
+ logger.debug('session onhold');
- this.emit('muted', {
- audio,
- video
- });
- }
+ this._setLocalMediaStatus();
- _onunmute({ audio, video })
- {
- logger.debug('session onunmute');
+ logger.debug('emit "hold"');
- this._setLocalMediaStatus();
+ this.emit('hold', {
+ originator,
+ });
+ }
- logger.debug('emit "unmuted"');
+ _onunhold(originator) {
+ logger.debug('session onunhold');
- this.emit('unmuted', {
- audio,
- video
- });
- }
+ this._setLocalMediaStatus();
+
+ logger.debug('emit "unhold"');
+
+ this.emit('unhold', {
+ originator,
+ });
+ }
+
+ _onmute({ audio, video }) {
+ logger.debug('session onmute');
+
+ this._setLocalMediaStatus();
+
+ logger.debug('emit "muted"');
+
+ this.emit('muted', {
+ audio,
+ video,
+ });
+ }
+
+ _onunmute({ audio, video }) {
+ logger.debug('session onunmute');
+
+ this._setLocalMediaStatus();
+
+ logger.debug('emit "unmuted"');
+
+ this.emit('unmuted', {
+ audio,
+ video,
+ });
+ }
};
diff --git a/src/RTCSession/DTMF.js b/src/RTCSession/DTMF.js
index b296bae..abacc0f 100644
--- a/src/RTCSession/DTMF.js
+++ b/src/RTCSession/DTMF.js
@@ -7,185 +7,154 @@ const Utils = require('../Utils');
const logger = new Logger('RTCSession:DTMF');
const C = {
- MIN_DURATION : 70,
- MAX_DURATION : 6000,
- DEFAULT_DURATION : 100,
- MIN_INTER_TONE_GAP : 50,
- DEFAULT_INTER_TONE_GAP : 500
+ MIN_DURATION: 70,
+ MAX_DURATION: 6000,
+ DEFAULT_DURATION: 100,
+ MIN_INTER_TONE_GAP: 50,
+ DEFAULT_INTER_TONE_GAP: 500,
};
-module.exports = class DTMF extends EventEmitter
-{
- constructor(session)
- {
- super();
-
- this._session = session;
- this._direction = null;
- this._tone = null;
- this._duration = null;
- this._request = null;
- }
-
- get tone()
- {
- return this._tone;
- }
-
- get duration()
- {
- return this._duration;
- }
-
- send(tone, options = {})
- {
- if (tone === undefined)
- {
- throw new TypeError('Not enough arguments');
- }
-
- this._direction = 'outgoing';
-
- // Check RTCSession Status.
- if (
- this._session.status !== this._session.C.STATUS_CONFIRMED &&
- this._session.status !== this._session.C.STATUS_WAITING_FOR_ACK &&
- this._session.status !== this._session.C.STATUS_1XX_RECEIVED
- )
- {
- throw new Exceptions.InvalidStateError(this._session.status);
- }
-
- const extraHeaders = Utils.cloneArray(options.extraHeaders);
-
- this.eventHandlers = Utils.cloneObject(options.eventHandlers);
-
- // Check tone type.
- if (typeof tone === 'string')
- {
- tone = tone.toUpperCase();
- }
- else if (typeof tone === 'number')
- {
- tone = tone.toString();
- }
- else
- {
- throw new TypeError(`Invalid tone: ${tone}`);
- }
-
- // Check tone value.
- if (!tone.match(/^[0-9A-DR#*]$/))
- {
- throw new TypeError(`Invalid tone: ${tone}`);
- }
- else
- {
- this._tone = tone;
- }
-
- // Duration is checked/corrected in RTCSession.
- this._duration = options.duration;
-
- extraHeaders.push('Content-Type: application/dtmf-relay');
-
- let body = `Signal=${this._tone}\r\n`;
-
- body += `Duration=${this._duration}`;
-
- this._session.newDTMF({
- originator : 'local',
- dtmf : this,
- request : this._request
- });
-
- this._session.sendRequest(JsSIP_C.INFO, {
- extraHeaders,
- eventHandlers : {
- onSuccessResponse : (response) =>
- {
- this.emit('succeeded', {
- originator : 'remote',
- response
- });
- },
- onErrorResponse : (response) =>
- {
- if (this.eventHandlers.onFailed)
- {
- this.eventHandlers.onFailed();
- }
-
- this.emit('failed', {
- originator : 'remote',
- response
- });
- },
- onRequestTimeout : () =>
- {
- this._session.onRequestTimeout();
- },
- onTransportError : () =>
- {
- this._session.onTransportError();
- },
- onDialogError : () =>
- {
- this._session.onDialogError();
- }
- },
- body
- });
- }
-
- init_incoming(request)
- {
- const reg_tone = /^(Signal\s*?=\s*?)([0-9A-D#*]{1})(\s)?.*/;
- const reg_duration = /^(Duration\s?=\s?)([0-9]{1,4})(\s)?.*/;
-
- this._direction = 'incoming';
- this._request = request;
-
- request.reply(200);
-
- if (request.body)
- {
- const body = request.body.split('\n');
-
- if (body.length >= 1)
- {
- if (reg_tone.test(body[0]))
- {
- this._tone = body[0].replace(reg_tone, '$2');
- }
- }
- if (body.length >=2)
- {
- if (reg_duration.test(body[1]))
- {
- this._duration = parseInt(body[1].replace(reg_duration, '$2'), 10);
- }
- }
- }
-
- if (!this._duration)
- {
- this._duration = C.DEFAULT_DURATION;
- }
-
- if (!this._tone)
- {
- logger.debug('invalid INFO DTMF received, discarded');
- }
- else
- {
- this._session.newDTMF({
- originator : 'remote',
- dtmf : this,
- request
- });
- }
- }
+module.exports = class DTMF extends EventEmitter {
+ constructor(session) {
+ super();
+
+ this._session = session;
+ this._direction = null;
+ this._tone = null;
+ this._duration = null;
+ this._request = null;
+ }
+
+ get tone() {
+ return this._tone;
+ }
+
+ get duration() {
+ return this._duration;
+ }
+
+ send(tone, options = {}) {
+ if (tone === undefined) {
+ throw new TypeError('Not enough arguments');
+ }
+
+ this._direction = 'outgoing';
+
+ // Check RTCSession Status.
+ if (
+ this._session.status !== this._session.C.STATUS_CONFIRMED &&
+ this._session.status !== this._session.C.STATUS_WAITING_FOR_ACK &&
+ this._session.status !== this._session.C.STATUS_1XX_RECEIVED
+ ) {
+ throw new Exceptions.InvalidStateError(this._session.status);
+ }
+
+ const extraHeaders = Utils.cloneArray(options.extraHeaders);
+
+ this.eventHandlers = Utils.cloneObject(options.eventHandlers);
+
+ // Check tone type.
+ if (typeof tone === 'string') {
+ tone = tone.toUpperCase();
+ } else if (typeof tone === 'number') {
+ tone = tone.toString();
+ } else {
+ throw new TypeError(`Invalid tone: ${tone}`);
+ }
+
+ // Check tone value.
+ if (!tone.match(/^[0-9A-DR#*]$/)) {
+ throw new TypeError(`Invalid tone: ${tone}`);
+ } else {
+ this._tone = tone;
+ }
+
+ // Duration is checked/corrected in RTCSession.
+ this._duration = options.duration;
+
+ extraHeaders.push('Content-Type: application/dtmf-relay');
+
+ let body = `Signal=${this._tone}\r\n`;
+
+ body += `Duration=${this._duration}`;
+
+ this._session.newDTMF({
+ originator: 'local',
+ dtmf: this,
+ request: this._request,
+ });
+
+ this._session.sendRequest(JsSIP_C.INFO, {
+ extraHeaders,
+ eventHandlers: {
+ onSuccessResponse: response => {
+ this.emit('succeeded', {
+ originator: 'remote',
+ response,
+ });
+ },
+ onErrorResponse: response => {
+ if (this.eventHandlers.onFailed) {
+ this.eventHandlers.onFailed();
+ }
+
+ this.emit('failed', {
+ originator: 'remote',
+ response,
+ });
+ },
+ onRequestTimeout: () => {
+ this._session.onRequestTimeout();
+ },
+ onTransportError: () => {
+ this._session.onTransportError();
+ },
+ onDialogError: () => {
+ this._session.onDialogError();
+ },
+ },
+ body,
+ });
+ }
+
+ init_incoming(request) {
+ const reg_tone = /^(Signal\s*?=\s*?)([0-9A-D#*]{1})(\s)?.*/;
+ const reg_duration = /^(Duration\s?=\s?)([0-9]{1,4})(\s)?.*/;
+
+ this._direction = 'incoming';
+ this._request = request;
+
+ request.reply(200);
+
+ if (request.body) {
+ const body = request.body.split('\n');
+
+ if (body.length >= 1) {
+ if (reg_tone.test(body[0])) {
+ this._tone = body[0].replace(reg_tone, '$2');
+ }
+ }
+ if (body.length >= 2) {
+ if (reg_duration.test(body[1])) {
+ this._duration = parseInt(body[1].replace(reg_duration, '$2'), 10);
+ }
+ }
+ }
+
+ if (!this._duration) {
+ this._duration = C.DEFAULT_DURATION;
+ }
+
+ if (!this._tone) {
+ logger.debug('invalid INFO DTMF received, discarded');
+ } else {
+ this._session.newDTMF({
+ originator: 'remote',
+ dtmf: this,
+ request,
+ });
+ }
+ }
};
/**
diff --git a/src/RTCSession/Info.js b/src/RTCSession/Info.js
index c331c35..8c57bb9 100644
--- a/src/RTCSession/Info.js
+++ b/src/RTCSession/Info.js
@@ -3,106 +3,96 @@ const JsSIP_C = require('../Constants');
const Exceptions = require('../Exceptions');
const Utils = require('../Utils');
-module.exports = class Info extends EventEmitter
-{
- constructor(session)
- {
- super();
-
- this._session = session;
- this._direction = null;
- this._contentType = null;
- this._body = null;
- }
-
- get contentType()
- {
- return this._contentType;
- }
-
- get body()
- {
- return this._body;
- }
-
- send(contentType, body, options = {})
- {
- this._direction = 'outgoing';
-
- if (contentType === undefined)
- {
- throw new TypeError('Not enough arguments');
- }
-
- // Check RTCSession Status.
- if (this._session.status !== this._session.C.STATUS_CONFIRMED &&
- this._session.status !== this._session.C.STATUS_WAITING_FOR_ACK)
- {
- throw new Exceptions.InvalidStateError(this._session.status);
- }
-
- this._contentType = contentType;
- this._body = body;
-
- const extraHeaders = Utils.cloneArray(options.extraHeaders);
-
- extraHeaders.push(`Content-Type: ${contentType}`);
-
- this._session.newInfo({
- originator : 'local',
- info : this,
- request : this.request
- });
-
- this._session.sendRequest(JsSIP_C.INFO, {
- extraHeaders,
- eventHandlers : {
- onSuccessResponse : (response) =>
- {
- this.emit('succeeded', {
- originator : 'remote',
- response
- });
- },
- onErrorResponse : (response) =>
- {
- this.emit('failed', {
- originator : 'remote',
- response
- });
- },
- onTransportError : () =>
- {
- this._session.onTransportError();
- },
- onRequestTimeout : () =>
- {
- this._session.onRequestTimeout();
- },
- onDialogError : () =>
- {
- this._session.onDialogError();
- }
- },
- body
- });
- }
-
- init_incoming(request)
- {
- this._direction = 'incoming';
- this.request = request;
-
- request.reply(200);
-
- this._contentType = request.hasHeader('Content-Type') ?
- request.getHeader('Content-Type').toLowerCase() : undefined;
- this._body = request.body;
-
- this._session.newInfo({
- originator : 'remote',
- info : this,
- request
- });
- }
+module.exports = class Info extends EventEmitter {
+ constructor(session) {
+ super();
+
+ this._session = session;
+ this._direction = null;
+ this._contentType = null;
+ this._body = null;
+ }
+
+ get contentType() {
+ return this._contentType;
+ }
+
+ get body() {
+ return this._body;
+ }
+
+ send(contentType, body, options = {}) {
+ this._direction = 'outgoing';
+
+ if (contentType === undefined) {
+ throw new TypeError('Not enough arguments');
+ }
+
+ // Check RTCSession Status.
+ if (
+ this._session.status !== this._session.C.STATUS_CONFIRMED &&
+ this._session.status !== this._session.C.STATUS_WAITING_FOR_ACK
+ ) {
+ throw new Exceptions.InvalidStateError(this._session.status);
+ }
+
+ this._contentType = contentType;
+ this._body = body;
+
+ const extraHeaders = Utils.cloneArray(options.extraHeaders);
+
+ extraHeaders.push(`Content-Type: ${contentType}`);
+
+ this._session.newInfo({
+ originator: 'local',
+ info: this,
+ request: this.request,
+ });
+
+ this._session.sendRequest(JsSIP_C.INFO, {
+ extraHeaders,
+ eventHandlers: {
+ onSuccessResponse: response => {
+ this.emit('succeeded', {
+ originator: 'remote',
+ response,
+ });
+ },
+ onErrorResponse: response => {
+ this.emit('failed', {
+ originator: 'remote',
+ response,
+ });
+ },
+ onTransportError: () => {
+ this._session.onTransportError();
+ },
+ onRequestTimeout: () => {
+ this._session.onRequestTimeout();
+ },
+ onDialogError: () => {
+ this._session.onDialogError();
+ },
+ },
+ body,
+ });
+ }
+
+ init_incoming(request) {
+ this._direction = 'incoming';
+ this.request = request;
+
+ request.reply(200);
+
+ this._contentType = request.hasHeader('Content-Type')
+ ? request.getHeader('Content-Type').toLowerCase()
+ : undefined;
+ this._body = request.body;
+
+ this._session.newInfo({
+ originator: 'remote',
+ info: this,
+ request,
+ });
+ }
};
diff --git a/src/RTCSession/ReferNotifier.js b/src/RTCSession/ReferNotifier.js
index dcf5071..4af0ea0 100644
--- a/src/RTCSession/ReferNotifier.js
+++ b/src/RTCSession/ReferNotifier.js
@@ -4,58 +4,53 @@ const JsSIP_C = require('../Constants');
const logger = new Logger('RTCSession:ReferNotifier');
const C = {
- event_type : 'refer',
- body_type : 'message/sipfrag;version=2.0',
- expires : 300
+ event_type: 'refer',
+ body_type: 'message/sipfrag;version=2.0',
+ expires: 300,
};
-module.exports = class ReferNotifier
-{
- constructor(session, id, expires)
- {
- this._session = session;
- this._id = id;
- this._expires = expires || C.expires;
- this._active = true;
-
- // The creation of a Notifier results in an immediate NOTIFY.
- this.notify(100);
- }
-
- notify(code, reason)
- {
- logger.debug('notify()');
-
- if (this._active === false)
- {
- return;
- }
-
- reason = reason || JsSIP_C.REASON_PHRASE[code] || '';
-
- let state;
-
- if (code >= 200)
- {
- state = 'terminated;reason=noresource';
- }
- else
- {
- state = `active;expires=${this._expires}`;
- }
-
- // Put this in a try/catch block.
- this._session.sendRequest(JsSIP_C.NOTIFY, {
- extraHeaders : [
- `Event: ${C.event_type};id=${this._id}`,
- `Subscription-State: ${state}`,
- `Content-Type: ${C.body_type}`
- ],
- body : `SIP/2.0 ${code} ${reason}`,
- eventHandlers : {
- // If a negative response is received, subscription is canceled.
- onErrorResponse() { this._active = false; }
- }
- });
- }
+module.exports = class ReferNotifier {
+ constructor(session, id, expires) {
+ this._session = session;
+ this._id = id;
+ this._expires = expires || C.expires;
+ this._active = true;
+
+ // The creation of a Notifier results in an immediate NOTIFY.
+ this.notify(100);
+ }
+
+ notify(code, reason) {
+ logger.debug('notify()');
+
+ if (this._active === false) {
+ return;
+ }
+
+ reason = reason || JsSIP_C.REASON_PHRASE[code] || '';
+
+ let state;
+
+ if (code >= 200) {
+ state = 'terminated;reason=noresource';
+ } else {
+ state = `active;expires=${this._expires}`;
+ }
+
+ // Put this in a try/catch block.
+ this._session.sendRequest(JsSIP_C.NOTIFY, {
+ extraHeaders: [
+ `Event: ${C.event_type};id=${this._id}`,
+ `Subscription-State: ${state}`,
+ `Content-Type: ${C.body_type}`,
+ ],
+ body: `SIP/2.0 ${code} ${reason}`,
+ eventHandlers: {
+ // If a negative response is received, subscription is canceled.
+ onErrorResponse() {
+ this._active = false;
+ },
+ },
+ });
+ }
};
diff --git a/src/RTCSession/ReferSubscriber.js b/src/RTCSession/ReferSubscriber.js
index 0d2dbaa..26231ef 100644
--- a/src/RTCSession/ReferSubscriber.js
+++ b/src/RTCSession/ReferSubscriber.js
@@ -6,163 +6,157 @@ const Utils = require('../Utils');
const logger = new Logger('RTCSession:ReferSubscriber');
-module.exports = class ReferSubscriber extends EventEmitter
-{
- constructor(session)
- {
- super();
-
- this._id = null;
- this._session = session;
- }
-
- get id()
- {
- return this._id;
- }
-
- sendRefer(target, options = {})
- {
- logger.debug('sendRefer()');
-
- const extraHeaders = Utils.cloneArray(options.extraHeaders);
- const eventHandlers = Utils.cloneObject(options.eventHandlers);
-
- // Set event handlers.
- for (const event in eventHandlers)
- {
- if (Object.prototype.hasOwnProperty.call(eventHandlers, event))
- {
- this.on(event, eventHandlers[event]);
- }
- }
-
- // Replaces URI header field.
- let replaces = null;
-
- if (options.replaces)
- {
- replaces = options.replaces._request.call_id;
- replaces += `;to-tag=${options.replaces._to_tag}`;
- replaces += `;from-tag=${options.replaces._from_tag}`;
-
- replaces = encodeURIComponent(replaces);
- }
-
- // Refer-To header field.
- const referTo = `Refer-To: <${target}${replaces?`?Replaces=${replaces}`:''}>`;
-
- extraHeaders.push(referTo);
-
- // Referred-By header field (if not already present).
- if (!extraHeaders.some((header) => header.toLowerCase().startsWith('referred-by:')))
- {
- const referredBy = `Referred-By: <${this._session._ua._configuration.uri._scheme}:${this._session._ua._configuration.uri._user}@${this._session._ua._configuration.uri._host}>`;
-
- extraHeaders.push(referredBy);
- }
-
- extraHeaders.push(`Contact: ${this._session.contact}`);
-
- const request = this._session.sendRequest(JsSIP_C.REFER, {
- extraHeaders,
- eventHandlers : {
- onSuccessResponse : (response) =>
- {
- this._requestSucceeded(response);
- },
- onErrorResponse : (response) =>
- {
- this._requestFailed(response, JsSIP_C.causes.REJECTED);
- },
- onTransportError : () =>
- {
- this._requestFailed(null, JsSIP_C.causes.CONNECTION_ERROR);
- },
- onRequestTimeout : () =>
- {
- this._requestFailed(null, JsSIP_C.causes.REQUEST_TIMEOUT);
- },
- onDialogError : () =>
- {
- this._requestFailed(null, JsSIP_C.causes.DIALOG_ERROR);
- }
- }
- });
-
- this._id = request.cseq;
- }
-
- receiveNotify(request)
- {
- logger.debug('receiveNotify()');
-
- if (!request.body)
- {
- return;
- }
-
- const status_line = Grammar.parse(request.body.trim().split('\r\n', 1)[0], 'Status_Line');
-
- if (status_line === -1)
- {
- logger.debug(`receiveNotify() | error parsing NOTIFY body: "${request.body}"`);
-
- return;
- }
-
- switch (true)
- {
- case /^100$/.test(status_line.status_code):
- this.emit('trying', {
- request,
- status_line
- });
- break;
-
- case /^1[0-9]{2}$/.test(status_line.status_code):
- this.emit('progress', {
- request,
- status_line
- });
- break;
-
- case /^2[0-9]{2}$/.test(status_line.status_code):
- this.emit('accepted', {
- request,
- status_line
- });
- break;
-
- default:
- this.emit('failed', {
- request,
- status_line
- });
- break;
- }
- }
-
- _requestSucceeded(response)
- {
- logger.debug('REFER succeeded');
-
- logger.debug('emit "requestSucceeded"');
-
- this.emit('requestSucceeded', {
- response
- });
- }
-
- _requestFailed(response, cause)
- {
- logger.debug('REFER failed');
-
- logger.debug('emit "requestFailed"');
-
- this.emit('requestFailed', {
- response : response || null,
- cause
- });
- }
+module.exports = class ReferSubscriber extends EventEmitter {
+ constructor(session) {
+ super();
+
+ this._id = null;
+ this._session = session;
+ }
+
+ get id() {
+ return this._id;
+ }
+
+ sendRefer(target, options = {}) {
+ logger.debug('sendRefer()');
+
+ const extraHeaders = Utils.cloneArray(options.extraHeaders);
+ const eventHandlers = Utils.cloneObject(options.eventHandlers);
+
+ // Set event handlers.
+ for (const event in eventHandlers) {
+ if (Object.prototype.hasOwnProperty.call(eventHandlers, event)) {
+ this.on(event, eventHandlers[event]);
+ }
+ }
+
+ // Replaces URI header field.
+ let replaces = null;
+
+ if (options.replaces) {
+ replaces = options.replaces._request.call_id;
+ replaces += `;to-tag=${options.replaces._to_tag}`;
+ replaces += `;from-tag=${options.replaces._from_tag}`;
+
+ replaces = encodeURIComponent(replaces);
+ }
+
+ // Refer-To header field.
+ const referTo = `Refer-To: <${target}${replaces ? `?Replaces=${replaces}` : ''}>`;
+
+ extraHeaders.push(referTo);
+
+ // Referred-By header field (if not already present).
+ if (
+ !extraHeaders.some(header =>
+ header.toLowerCase().startsWith('referred-by:')
+ )
+ ) {
+ const referredBy = `Referred-By: <${this._session._ua._configuration.uri._scheme}:${this._session._ua._configuration.uri._user}@${this._session._ua._configuration.uri._host}>`;
+
+ extraHeaders.push(referredBy);
+ }
+
+ extraHeaders.push(`Contact: ${this._session.contact}`);
+
+ const request = this._session.sendRequest(JsSIP_C.REFER, {
+ extraHeaders,
+ eventHandlers: {
+ onSuccessResponse: response => {
+ this._requestSucceeded(response);
+ },
+ onErrorResponse: response => {
+ this._requestFailed(response, JsSIP_C.causes.REJECTED);
+ },
+ onTransportError: () => {
+ this._requestFailed(null, JsSIP_C.causes.CONNECTION_ERROR);
+ },
+ onRequestTimeout: () => {
+ this._requestFailed(null, JsSIP_C.causes.REQUEST_TIMEOUT);
+ },
+ onDialogError: () => {
+ this._requestFailed(null, JsSIP_C.causes.DIALOG_ERROR);
+ },
+ },
+ });
+
+ this._id = request.cseq;
+ }
+
+ receiveNotify(request) {
+ logger.debug('receiveNotify()');
+
+ if (!request.body) {
+ return;
+ }
+
+ const status_line = Grammar.parse(
+ request.body.trim().split('\r\n', 1)[0],
+ 'Status_Line'
+ );
+
+ if (status_line === -1) {
+ logger.debug(
+ `receiveNotify() | error parsing NOTIFY body: "${request.body}"`
+ );
+
+ return;
+ }
+
+ switch (true) {
+ case /^100$/.test(status_line.status_code): {
+ this.emit('trying', {
+ request,
+ status_line,
+ });
+ break;
+ }
+
+ case /^1[0-9]{2}$/.test(status_line.status_code): {
+ this.emit('progress', {
+ request,
+ status_line,
+ });
+ break;
+ }
+
+ case /^2[0-9]{2}$/.test(status_line.status_code): {
+ this.emit('accepted', {
+ request,
+ status_line,
+ });
+ break;
+ }
+
+ default: {
+ this.emit('failed', {
+ request,
+ status_line,
+ });
+ break;
+ }
+ }
+ }
+
+ _requestSucceeded(response) {
+ logger.debug('REFER succeeded');
+
+ logger.debug('emit "requestSucceeded"');
+
+ this.emit('requestSucceeded', {
+ response,
+ });
+ }
+
+ _requestFailed(response, cause) {
+ logger.debug('REFER failed');
+
+ logger.debug('emit "requestFailed"');
+
+ this.emit('requestFailed', {
+ response: response || null,
+ cause,
+ });
+ }
};
diff --git a/src/Registrator.d.ts b/src/Registrator.d.ts
index 7d5d6a1..544ddd6 100644
--- a/src/Registrator.d.ts
+++ b/src/Registrator.d.ts
@@ -1,12 +1,12 @@
-import {UA} from './UA'
-import {Transport} from './Transport'
+import { UA } from './UA';
+import { Transport } from './Transport';
export type ExtraContactParams = Record<string, string | number | boolean>;
export class Registrator {
- constructor(ua: UA, transport: Transport);
+ constructor(ua: UA, transport: Transport);
- setExtraHeaders(extraHeaders: string[]): void;
+ setExtraHeaders(extraHeaders: string[]): void;
- setExtraContactParams(extraContactParams: ExtraContactParams): void;
+ setExtraContactParams(extraContactParams: ExtraContactParams): void;
}
diff --git a/src/Registrator.js b/src/Registrator.js
index 0a70465..7ce227b 100644
--- a/src/Registrator.js
+++ b/src/Registrator.js
@@ -8,436 +8,406 @@ const logger = new Logger('Registrator');
const MIN_REGISTER_EXPIRES = 10; // In seconds.
-module.exports = class Registrator
-{
- constructor(ua, transport)
- {
- // Force reg_id to 1.
- this._reg_id = 1;
-
- this._ua = ua;
- this._transport = transport;
-
- this._registrar = ua.configuration.registrar_server;
- this._expires = ua.configuration.register_expires;
+module.exports = class Registrator {
+ constructor(ua, transport) {
+ // Force reg_id to 1.
+ this._reg_id = 1;
- // Call-ID and CSeq values RFC3261 10.2.
- this._call_id = Utils.createRandomToken(22);
- this._cseq = 0;
-
- this._to_uri = ua.configuration.uri;
-
- this._registrationTimer = null;
-
- // Ongoing Register request.
- this._registering = false;
-
- // Set status.
- this._registered = false;
-
- // Contact header.
- this._contact = this._ua.contact.toString();
-
- // Sip.ice media feature tag (RFC 5768).
- this._contact += ';+sip.ice';
-
- // Custom headers for REGISTER and un-REGISTER.
- this._extraHeaders = [];
-
- // Custom Contact header params for REGISTER and un-REGISTER.
- this._extraContactParams = '';
-
- // Contents of the sip.instance Contact header parameter.
- this._sipInstance = `"<urn:uuid:${this._ua.configuration.instance_id}>"`;
-
- this._contact += `;reg-id=${this._reg_id}`;
- this._contact += `;+sip.instance=${this._sipInstance}`;
- }
-
- get registered()
- {
- return this._registered;
- }
-
- setExtraHeaders(extraHeaders)
- {
- if (!Array.isArray(extraHeaders))
- {
- extraHeaders = [];
- }
-
- this._extraHeaders = extraHeaders.slice();
- }
-
- setExtraContactParams(extraContactParams)
- {
- if (!(extraContactParams instanceof Object))
- {
- extraContactParams = {};
- }
-
- // Reset it.
- this._extraContactParams = '';
-
- for (const param_key in extraContactParams)
- {
- if (Object.prototype.hasOwnProperty.call(extraContactParams, param_key))
- {
- const param_value = extraContactParams[param_key];
-
- this._extraContactParams += (`;${param_key}`);
- if (param_value)
- {
- this._extraContactParams += (`=${param_value}`);
- }
- }
- }
- }
-
- register()
- {
- if (this._registering)
- {
- logger.debug('Register request in progress...');
-
- return;
- }
-
- const extraHeaders = Utils.cloneArray(this._extraHeaders);
-
- let contactValue;
-
- if (this._expires)
- {
- contactValue = `${this._contact};expires=${this._expires}${this._extraContactParams}`;
- extraHeaders.push(`Expires: ${this._expires}`);
- }
- else
- {
- contactValue = `${this._contact}${this._extraContactParams}`;
- }
-
- extraHeaders.push(`Contact: ${contactValue}`);
-
- let fromTag = Utils.newTag();
-
- if (this._ua.configuration.register_from_tag_trail)
- {
- if (typeof this._ua.configuration.register_from_tag_trail === 'function')
- {
- fromTag += this._ua.configuration.register_from_tag_trail();
- }
- else
- {
- fromTag += this._ua.configuration.register_from_tag_trail;
- }
- }
-
- const request = new SIPMessage.OutgoingRequest(
- JsSIP_C.REGISTER, this._registrar, this._ua, {
- 'to_uri' : this._to_uri,
- 'call_id' : this._call_id,
- 'cseq' : (this._cseq += 1),
- 'from_tag' : fromTag
- }, extraHeaders);
-
- const request_sender = new RequestSender(this._ua, request, {
- onRequestTimeout : () =>
- {
- this._registrationFailure(null, JsSIP_C.causes.REQUEST_TIMEOUT);
- },
- onTransportError : () =>
- {
- this._registrationFailure(null, JsSIP_C.causes.CONNECTION_ERROR);
- },
- // Increase the CSeq on authentication.
- onAuthenticated : () =>
- {
- this._cseq += 1;
- },
- onReceiveResponse : (response) =>
- {
- // Discard responses to older REGISTER/un-REGISTER requests.
- if (response.cseq !== this._cseq)
- {
- return;
- }
-
- // Clear registration timer.
- if (this._registrationTimer !== null)
- {
- clearTimeout(this._registrationTimer);
- this._registrationTimer = null;
- }
-
- switch (true)
- {
- case /^1[0-9]{2}$/.test(response.status_code):
- {
- // Ignore provisional responses.
- break;
- }
-
- case /^2[0-9]{2}$/.test(response.status_code):
- {
- this._registering = false;
-
- if (!response.hasHeader('Contact'))
- {
- logger.debug('no Contact header in response to REGISTER, response ignored');
-
- break;
- }
-
- const contacts = response.headers['Contact']
- .reduce((a, b) => a.concat(b.parsed), []);
-
- // Get the Contact pointing to us and update the expires value accordingly.
- // Try to find a matching Contact using sip.instance and reg-id.
- let contact = contacts.find((element) => (
- (this._sipInstance === element.getParam('+sip.instance')) &&
- (this._reg_id === parseInt(element.getParam('reg-id')))
- ));
-
- // If no match was found using the sip.instance try comparing the URIs.
- if (!contact)
- {
- contact = contacts.find((element) => (
- (element.uri.user === this._ua.contact.uri.user)
- ));
- }
-
- if (!contact)
- {
- logger.debug('no Contact header pointing to us, response ignored');
-
- break;
- }
-
- let expires = contact.getParam('expires');
-
- if (!expires && response.hasHeader('expires'))
- {
- expires = response.getHeader('expires');
- }
-
- if (!expires)
- {
- expires = this._expires;
- }
-
- expires = Number(expires);
-
- if (expires < MIN_REGISTER_EXPIRES)
- expires = MIN_REGISTER_EXPIRES;
-
- const timeout = expires > 64
- ? (expires * 1000 / 2) +
- Math.floor(((expires / 2) - 32) * 1000 * Math.random())
- : (expires * 1000) - 5000;
-
- // Re-Register or emit an event before the expiration interval has elapsed.
- // For that, decrease the expires value. ie: 3 seconds.
- this._registrationTimer = setTimeout(() =>
- {
- this._registrationTimer = null;
- // If there are no listeners for registrationExpiring, renew registration.
- // If there are listeners, let the function listening do the register call.
- if (this._ua.listeners('registrationExpiring').length === 0)
- {
- this.register();
- }
- else
- {
- this._ua.emit('registrationExpiring');
- }
- }, timeout);
-
- // Save gruu values.
- if (contact.hasParam('temp-gruu'))
- {
- this._ua.contact.temp_gruu = contact.getParam('temp-gruu').replace(/"/g, '');
- }
- if (contact.hasParam('pub-gruu'))
- {
- this._ua.contact.pub_gruu = contact.getParam('pub-gruu').replace(/"/g, '');
- }
-
- if (!this._registered)
- {
- this._registered = true;
- this._ua.registered({ response });
- }
-
- break;
- }
-
- // Interval too brief RFC3261 10.2.8.
- case /^423$/.test(response.status_code):
- {
- if (response.hasHeader('min-expires'))
- {
- // Increase our registration interval to the suggested minimum.
- this._expires = Number(response.getHeader('min-expires'));
-
- if (this._expires < MIN_REGISTER_EXPIRES)
- this._expires = MIN_REGISTER_EXPIRES;
-
- // Assure register re-try with new expire.
- this._registering = false;
-
- // Attempt the registration again immediately.
- this.register();
- }
- else
- { // This response MUST contain a Min-Expires header field.
- logger.debug('423 response received for REGISTER without Min-Expires');
-
- this._registrationFailure(response, JsSIP_C.causes.SIP_FAILURE_CODE);
- }
-
- break;
- }
-
- default:
- {
- const cause = Utils.sipErrorCause(response.status_code);
-
- this._registrationFailure(response, cause);
- }
- }
- }
- });
-
- this._registering = true;
- request_sender.send();
- }
-
- unregister(options = {})
- {
- if (!this._registered)
- {
- logger.debug('already unregistered');
-
- return;
- }
-
- this._registered = false;
-
- // Clear the registration timer.
- if (this._registrationTimer !== null)
- {
- clearTimeout(this._registrationTimer);
- this._registrationTimer = null;
- }
-
- const extraHeaders = Utils.cloneArray(this._extraHeaders);
-
- if (options.all)
- {
- extraHeaders.push(`Contact: *${this._extraContactParams}`);
- }
- else
- {
- extraHeaders.push(`Contact: ${this._contact};expires=0${this._extraContactParams}`);
- }
-
- extraHeaders.push('Expires: 0');
-
- const request = new SIPMessage.OutgoingRequest(
- JsSIP_C.REGISTER, this._registrar, this._ua, {
- 'to_uri' : this._to_uri,
- 'call_id' : this._call_id,
- 'cseq' : (this._cseq += 1)
- }, extraHeaders);
-
- const request_sender = new RequestSender(this._ua, request, {
- onRequestTimeout : () =>
- {
- this._unregistered(null, JsSIP_C.causes.REQUEST_TIMEOUT);
- },
- onTransportError : () =>
- {
- this._unregistered(null, JsSIP_C.causes.CONNECTION_ERROR);
- },
- // Increase the CSeq on authentication.
- onAuthenticated : () =>
- {
- this._cseq += 1;
- },
- onReceiveResponse : (response) =>
- {
- switch (true)
- {
- case /^1[0-9]{2}$/.test(response.status_code):
- // Ignore provisional responses.
- break;
- case /^2[0-9]{2}$/.test(response.status_code):
- this._unregistered(response);
- break;
- default:
- {
- const cause = Utils.sipErrorCause(response.status_code);
-
- this._unregistered(response, cause);
- }
- }
- }
- });
-
- request_sender.send();
- }
-
- close()
- {
- if (this._registered)
- {
- this.unregister();
- }
- }
-
-
- onTransportClosed()
- {
- this._registering = false;
- if (this._registrationTimer !== null)
- {
- clearTimeout(this._registrationTimer);
- this._registrationTimer = null;
- }
-
- if (this._registered)
- {
- this._registered = false;
- this._ua.unregistered({});
- }
- }
-
- _registrationFailure(response, cause)
- {
- this._registering = false;
- this._ua.registrationFailed({
- response : response || null,
- cause
- });
-
- if (this._registered)
- {
- this._registered = false;
- this._ua.unregistered({
- response : response || null,
- cause
- });
- }
- }
-
- _unregistered(response, cause)
- {
- this._registering = false;
- this._registered = false;
- this._ua.unregistered({
- response : response || null,
- cause : cause || null
- });
- }
+ this._ua = ua;
+ this._transport = transport;
+
+ this._registrar = ua.configuration.registrar_server;
+ this._expires = ua.configuration.register_expires;
+
+ // Call-ID and CSeq values RFC3261 10.2.
+ this._call_id = Utils.createRandomToken(22);
+ this._cseq = 0;
+
+ this._to_uri = ua.configuration.uri;
+
+ this._registrationTimer = null;
+
+ // Ongoing Register request.
+ this._registering = false;
+
+ // Set status.
+ this._registered = false;
+
+ // Contact header.
+ this._contact = this._ua.contact.toString();
+
+ // Sip.ice media feature tag (RFC 5768).
+ this._contact += ';+sip.ice';
+
+ // Custom headers for REGISTER and un-REGISTER.
+ this._extraHeaders = [];
+
+ // Custom Contact header params for REGISTER and un-REGISTER.
+ this._extraContactParams = '';
+
+ // Contents of the sip.instance Contact header parameter.
+ this._sipInstance = `"<urn:uuid:${this._ua.configuration.instance_id}>"`;
+
+ this._contact += `;reg-id=${this._reg_id}`;
+ this._contact += `;+sip.instance=${this._sipInstance}`;
+ }
+
+ get registered() {
+ return this._registered;
+ }
+
+ setExtraHeaders(extraHeaders) {
+ if (!Array.isArray(extraHeaders)) {
+ extraHeaders = [];
+ }
+
+ this._extraHeaders = extraHeaders.slice();
+ }
+
+ setExtraContactParams(extraContactParams) {
+ if (!(extraContactParams instanceof Object)) {
+ extraContactParams = {};
+ }
+
+ // Reset it.
+ this._extraContactParams = '';
+
+ for (const param_key in extraContactParams) {
+ if (Object.prototype.hasOwnProperty.call(extraContactParams, param_key)) {
+ const param_value = extraContactParams[param_key];
+
+ this._extraContactParams += `;${param_key}`;
+ if (param_value) {
+ this._extraContactParams += `=${param_value}`;
+ }
+ }
+ }
+ }
+
+ register() {
+ if (this._registering) {
+ logger.debug('Register request in progress...');
+
+ return;
+ }
+
+ const extraHeaders = Utils.cloneArray(this._extraHeaders);
+
+ let contactValue;
+
+ if (this._expires) {
+ contactValue = `${this._contact};expires=${this._expires}${this._extraContactParams}`;
+ extraHeaders.push(`Expires: ${this._expires}`);
+ } else {
+ contactValue = `${this._contact}${this._extraContactParams}`;
+ }
+
+ extraHeaders.push(`Contact: ${contactValue}`);
+
+ let fromTag = Utils.newTag();
+
+ if (this._ua.configuration.register_from_tag_trail) {
+ if (
+ typeof this._ua.configuration.register_from_tag_trail === 'function'
+ ) {
+ fromTag += this._ua.configuration.register_from_tag_trail();
+ } else {
+ fromTag += this._ua.configuration.register_from_tag_trail;
+ }
+ }
+
+ const request = new SIPMessage.OutgoingRequest(
+ JsSIP_C.REGISTER,
+ this._registrar,
+ this._ua,
+ {
+ to_uri: this._to_uri,
+ call_id: this._call_id,
+ cseq: (this._cseq += 1),
+ from_tag: fromTag,
+ },
+ extraHeaders
+ );
+
+ const request_sender = new RequestSender(this._ua, request, {
+ onRequestTimeout: () => {
+ this._registrationFailure(null, JsSIP_C.causes.REQUEST_TIMEOUT);
+ },
+ onTransportError: () => {
+ this._registrationFailure(null, JsSIP_C.causes.CONNECTION_ERROR);
+ },
+ // Increase the CSeq on authentication.
+ onAuthenticated: () => {
+ this._cseq += 1;
+ },
+ onReceiveResponse: response => {
+ // Discard responses to older REGISTER/un-REGISTER requests.
+ if (response.cseq !== this._cseq) {
+ return;
+ }
+
+ // Clear registration timer.
+ if (this._registrationTimer !== null) {
+ clearTimeout(this._registrationTimer);
+ this._registrationTimer = null;
+ }
+
+ switch (true) {
+ case /^1[0-9]{2}$/.test(response.status_code): {
+ // Ignore provisional responses.
+ break;
+ }
+
+ case /^2[0-9]{2}$/.test(response.status_code): {
+ this._registering = false;
+
+ if (!response.hasHeader('Contact')) {
+ logger.debug(
+ 'no Contact header in response to REGISTER, response ignored'
+ );
+
+ break;
+ }
+
+ const contacts = response.headers['Contact'].reduce(
+ (a, b) => a.concat(b.parsed),
+ []
+ );
+
+ // Get the Contact pointing to us and update the expires value accordingly.
+ // Try to find a matching Contact using sip.instance and reg-id.
+ let contact = contacts.find(
+ element =>
+ this._sipInstance === element.getParam('+sip.instance') &&
+ this._reg_id === parseInt(element.getParam('reg-id'))
+ );
+
+ // If no match was found using the sip.instance try comparing the URIs.
+ if (!contact) {
+ contact = contacts.find(
+ element => element.uri.user === this._ua.contact.uri.user
+ );
+ }
+
+ if (!contact) {
+ logger.debug(
+ 'no Contact header pointing to us, response ignored'
+ );
+
+ break;
+ }
+
+ let expires = contact.getParam('expires');
+
+ if (!expires && response.hasHeader('expires')) {
+ expires = response.getHeader('expires');
+ }
+
+ if (!expires) {
+ expires = this._expires;
+ }
+
+ expires = Number(expires);
+
+ if (expires < MIN_REGISTER_EXPIRES) {
+ expires = MIN_REGISTER_EXPIRES;
+ }
+
+ const timeout =
+ expires > 64
+ ? (expires * 1000) / 2 +
+ Math.floor((expires / 2 - 32) * 1000 * Math.random())
+ : expires * 1000 - 5000;
+
+ // Re-Register or emit an event before the expiration interval has elapsed.
+ // For that, decrease the expires value. ie: 3 seconds.
+ this._registrationTimer = setTimeout(() => {
+ this._registrationTimer = null;
+ // If there are no listeners for registrationExpiring, renew registration.
+ // If there are listeners, let the function listening do the register call.
+ if (this._ua.listeners('registrationExpiring').length === 0) {
+ this.register();
+ } else {
+ this._ua.emit('registrationExpiring');
+ }
+ }, timeout);
+
+ // Save gruu values.
+ if (contact.hasParam('temp-gruu')) {
+ this._ua.contact.temp_gruu = contact
+ .getParam('temp-gruu')
+ .replace(/"/g, '');
+ }
+ if (contact.hasParam('pub-gruu')) {
+ this._ua.contact.pub_gruu = contact
+ .getParam('pub-gruu')
+ .replace(/"/g, '');
+ }
+
+ if (!this._registered) {
+ this._registered = true;
+ this._ua.registered({ response });
+ }
+
+ break;
+ }
+
+ // Interval too brief RFC3261 10.2.8.
+ case /^423$/.test(response.status_code): {
+ if (response.hasHeader('min-expires')) {
+ // Increase our registration interval to the suggested minimum.
+ this._expires = Number(response.getHeader('min-expires'));
+
+ if (this._expires < MIN_REGISTER_EXPIRES) {
+ this._expires = MIN_REGISTER_EXPIRES;
+ }
+
+ // Assure register re-try with new expire.
+ this._registering = false;
+
+ // Attempt the registration again immediately.
+ this.register();
+ } else {
+ // This response MUST contain a Min-Expires header field.
+ logger.debug(
+ '423 response received for REGISTER without Min-Expires'
+ );
+
+ this._registrationFailure(
+ response,
+ JsSIP_C.causes.SIP_FAILURE_CODE
+ );
+ }
+
+ break;
+ }
+
+ default: {
+ const cause = Utils.sipErrorCause(response.status_code);
+
+ this._registrationFailure(response, cause);
+ }
+ }
+ },
+ });
+
+ this._registering = true;
+ request_sender.send();
+ }
+
+ unregister(options = {}) {
+ if (!this._registered) {
+ logger.debug('already unregistered');
+
+ return;
+ }
+
+ this._registered = false;
+
+ // Clear the registration timer.
+ if (this._registrationTimer !== null) {
+ clearTimeout(this._registrationTimer);
+ this._registrationTimer = null;
+ }
+
+ const extraHeaders = Utils.cloneArray(this._extraHeaders);
+
+ if (options.all) {
+ extraHeaders.push(`Contact: *${this._extraContactParams}`);
+ } else {
+ extraHeaders.push(
+ `Contact: ${this._contact};expires=0${this._extraContactParams}`
+ );
+ }
+
+ extraHeaders.push('Expires: 0');
+
+ const request = new SIPMessage.OutgoingRequest(
+ JsSIP_C.REGISTER,
+ this._registrar,
+ this._ua,
+ {
+ to_uri: this._to_uri,
+ call_id: this._call_id,
+ cseq: (this._cseq += 1),
+ },
+ extraHeaders
+ );
+
+ const request_sender = new RequestSender(this._ua, request, {
+ onRequestTimeout: () => {
+ this._unregistered(null, JsSIP_C.causes.REQUEST_TIMEOUT);
+ },
+ onTransportError: () => {
+ this._unregistered(null, JsSIP_C.causes.CONNECTION_ERROR);
+ },
+ // Increase the CSeq on authentication.
+ onAuthenticated: () => {
+ this._cseq += 1;
+ },
+ onReceiveResponse: response => {
+ switch (true) {
+ case /^1[0-9]{2}$/.test(response.status_code): {
+ // Ignore provisional responses.
+ break;
+ }
+ case /^2[0-9]{2}$/.test(response.status_code): {
+ this._unregistered(response);
+ break;
+ }
+ default: {
+ const cause = Utils.sipErrorCause(response.status_code);
+
+ this._unregistered(response, cause);
+ }
+ }
+ },
+ });
+
+ request_sender.send();
+ }
+
+ close() {
+ if (this._registered) {
+ this.unregister();
+ }
+ }
+
+ onTransportClosed() {
+ this._registering = false;
+ if (this._registrationTimer !== null) {
+ clearTimeout(this._registrationTimer);
+ this._registrationTimer = null;
+ }
+
+ if (this._registered) {
+ this._registered = false;
+ this._ua.unregistered({});
+ }
+ }
+
+ _registrationFailure(response, cause) {
+ this._registering = false;
+ this._ua.registrationFailed({
+ response: response || null,
+ cause,
+ });
+
+ if (this._registered) {
+ this._registered = false;
+ this._ua.unregistered({
+ response: response || null,
+ cause,
+ });
+ }
+ }
+
+ _unregistered(response, cause) {
+ this._registering = false;
+ this._registered = false;
+ this._ua.unregistered({
+ response: response || null,
+ cause: cause || null,
+ });
+ }
};
diff --git a/src/RequestSender.js b/src/RequestSender.js
index f4e55ba..c7ddc35 100644
--- a/src/RequestSender.js
+++ b/src/RequestSender.js
@@ -7,163 +7,176 @@ const logger = new Logger('RequestSender');
// Default event handlers.
const EventHandlers = {
- onRequestTimeout : () => {},
- onTransportError : () => {},
- onReceiveResponse : () => {},
- onAuthenticated : () => {}
+ onRequestTimeout: () => {},
+ onTransportError: () => {},
+ onReceiveResponse: () => {},
+ onAuthenticated: () => {},
};
-module.exports = class RequestSender
-{
- constructor(ua, request, eventHandlers)
- {
- this._ua = ua;
- this._eventHandlers = eventHandlers;
- this._method = request.method;
- this._request = request;
- this._auth = null;
- this._challenged = false;
- this._staled = false;
-
- // Define the undefined handlers.
- for (const handler in EventHandlers)
- {
- if (Object.prototype.hasOwnProperty.call(EventHandlers, handler))
- {
- if (!this._eventHandlers[handler])
- {
- this._eventHandlers[handler] = EventHandlers[handler];
- }
- }
- }
-
- // If ua is in closing process or even closed just allow sending Bye and ACK.
- if (ua.status === ua.C.STATUS_USER_CLOSED &&
- (this._method !== JsSIP_C.BYE || this._method !== JsSIP_C.ACK))
- {
- this._eventHandlers.onTransportError();
- }
- }
-
- /**
- * Create the client transaction and send the message.
- */
- send()
- {
- const eventHandlers = {
- onRequestTimeout : () => { this._eventHandlers.onRequestTimeout(); },
- onTransportError : () => { this._eventHandlers.onTransportError(); },
- onReceiveResponse : (response) => { this._receiveResponse(response); }
- };
-
- switch (this._method)
- {
- case 'INVITE':
- this.clientTransaction = new Transactions.InviteClientTransaction(
- this._ua, this._ua.transport, this._request, eventHandlers);
- break;
- case 'ACK':
- this.clientTransaction = new Transactions.AckClientTransaction(
- this._ua, this._ua.transport, this._request, eventHandlers);
- break;
- default:
- this.clientTransaction = new Transactions.NonInviteClientTransaction(
- this._ua, this._ua.transport, this._request, eventHandlers);
- }
- // If authorization JWT is present, use it.
- if (this._ua._configuration.authorization_jwt)
- {
- this._request.setHeader('Authorization', this._ua._configuration.authorization_jwt);
- }
-
- this.clientTransaction.send();
- }
-
- /**
- * Called from client transaction when receiving a correct response to the request.
- * Authenticate request if needed or pass the response back to the applicant.
- */
- _receiveResponse(response)
- {
- let challenge;
- let authorization_header_name;
- const status_code = response.status_code;
-
- /*
- * Authentication
- * Authenticate once. _challenged_ flag used to avoid infinite authentications.
- */
- if ((status_code === 401 || status_code === 407) &&
- (this._ua.configuration.password !== null || this._ua.configuration.ha1 !== null))
- {
-
- // Get and parse the appropriate WWW-Authenticate or Proxy-Authenticate header.
- if (response.status_code === 401)
- {
- challenge = response.parseHeader('www-authenticate');
- authorization_header_name = 'authorization';
- }
- else
- {
- challenge = response.parseHeader('proxy-authenticate');
- authorization_header_name = 'proxy-authorization';
- }
-
- // Verify it seems a valid challenge.
- if (!challenge)
- {
- logger.debug(`${response.status_code} with wrong or missing challenge, cannot authenticate`);
- this._eventHandlers.onReceiveResponse(response);
-
- return;
- }
-
- if (!this._challenged || (!this._staled && challenge.stale === true))
- {
- if (!this._auth)
- {
- this._auth = new DigestAuthentication({
- username : this._ua.configuration.authorization_user,
- password : this._ua.configuration.password,
- realm : this._ua.configuration.realm,
- ha1 : this._ua.configuration.ha1
- });
- }
-
- // Verify that the challenge is really valid.
- if (!this._auth.authenticate(this._request, challenge))
- {
- this._eventHandlers.onReceiveResponse(response);
-
- return;
- }
- this._challenged = true;
-
- // Update ha1 and realm in the UA.
- this._ua.set('realm', this._auth.get('realm'));
- this._ua.set('ha1', this._auth.get('ha1'));
-
- if (challenge.stale)
- {
- this._staled = true;
- }
-
- this._request = this._request.clone();
- this._request.cseq += 1;
- this._request.setHeader('cseq', `${this._request.cseq} ${this._method}`);
- this._request.setHeader(authorization_header_name, this._auth.toString());
-
- this._eventHandlers.onAuthenticated(this._request);
- this.send();
- }
- else
- {
- this._eventHandlers.onReceiveResponse(response);
- }
- }
- else
- {
- this._eventHandlers.onReceiveResponse(response);
- }
- }
+module.exports = class RequestSender {
+ constructor(ua, request, eventHandlers) {
+ this._ua = ua;
+ this._eventHandlers = eventHandlers;
+ this._method = request.method;
+ this._request = request;
+ this._auth = null;
+ this._challenged = false;
+ this._staled = false;
+
+ // Define the undefined handlers.
+ for (const handler in EventHandlers) {
+ if (Object.prototype.hasOwnProperty.call(EventHandlers, handler)) {
+ if (!this._eventHandlers[handler]) {
+ this._eventHandlers[handler] = EventHandlers[handler];
+ }
+ }
+ }
+
+ // If ua is in closing process or even closed just allow sending Bye and ACK.
+ if (
+ ua.status === ua.C.STATUS_USER_CLOSED &&
+ (this._method !== JsSIP_C.BYE || this._method !== JsSIP_C.ACK)
+ ) {
+ this._eventHandlers.onTransportError();
+ }
+ }
+
+ /**
+ * Create the client transaction and send the message.
+ */
+ send() {
+ const eventHandlers = {
+ onRequestTimeout: () => {
+ this._eventHandlers.onRequestTimeout();
+ },
+ onTransportError: () => {
+ this._eventHandlers.onTransportError();
+ },
+ onReceiveResponse: response => {
+ this._receiveResponse(response);
+ },
+ };
+
+ switch (this._method) {
+ case 'INVITE': {
+ this.clientTransaction = new Transactions.InviteClientTransaction(
+ this._ua,
+ this._ua.transport,
+ this._request,
+ eventHandlers
+ );
+ break;
+ }
+ case 'ACK': {
+ this.clientTransaction = new Transactions.AckClientTransaction(
+ this._ua,
+ this._ua.transport,
+ this._request,
+ eventHandlers
+ );
+ break;
+ }
+ default: {
+ this.clientTransaction = new Transactions.NonInviteClientTransaction(
+ this._ua,
+ this._ua.transport,
+ this._request,
+ eventHandlers
+ );
+ }
+ }
+ // If authorization JWT is present, use it.
+ if (this._ua._configuration.authorization_jwt) {
+ this._request.setHeader(
+ 'Authorization',
+ this._ua._configuration.authorization_jwt
+ );
+ }
+
+ this.clientTransaction.send();
+ }
+
+ /**
+ * Called from client transaction when receiving a correct response to the request.
+ * Authenticate request if needed or pass the response back to the applicant.
+ */
+ _receiveResponse(response) {
+ let challenge;
+ let authorization_header_name;
+ const status_code = response.status_code;
+
+ /*
+ * Authentication
+ * Authenticate once. _challenged_ flag used to avoid infinite authentications.
+ */
+ if (
+ (status_code === 401 || status_code === 407) &&
+ (this._ua.configuration.password !== null ||
+ this._ua.configuration.ha1 !== null)
+ ) {
+ // Get and parse the appropriate WWW-Authenticate or Proxy-Authenticate header.
+ if (response.status_code === 401) {
+ challenge = response.parseHeader('www-authenticate');
+ authorization_header_name = 'authorization';
+ } else {
+ challenge = response.parseHeader('proxy-authenticate');
+ authorization_header_name = 'proxy-authorization';
+ }
+
+ // Verify it seems a valid challenge.
+ if (!challenge) {
+ logger.debug(
+ `${response.status_code} with wrong or missing challenge, cannot authenticate`
+ );
+ this._eventHandlers.onReceiveResponse(response);
+
+ return;
+ }
+
+ if (!this._challenged || (!this._staled && challenge.stale === true)) {
+ if (!this._auth) {
+ this._auth = new DigestAuthentication({
+ username: this._ua.configuration.authorization_user,
+ password: this._ua.configuration.password,
+ realm: this._ua.configuration.realm,
+ ha1: this._ua.configuration.ha1,
+ });
+ }
+
+ // Verify that the challenge is really valid.
+ if (!this._auth.authenticate(this._request, challenge)) {
+ this._eventHandlers.onReceiveResponse(response);
+
+ return;
+ }
+ this._challenged = true;
+
+ // Update ha1 and realm in the UA.
+ this._ua.set('realm', this._auth.get('realm'));
+ this._ua.set('ha1', this._auth.get('ha1'));
+
+ if (challenge.stale) {
+ this._staled = true;
+ }
+
+ this._request = this._request.clone();
+ this._request.cseq += 1;
+ this._request.setHeader(
+ 'cseq',
+ `${this._request.cseq} ${this._method}`
+ );
+ this._request.setHeader(
+ authorization_header_name,
+ this._auth.toString()
+ );
+
+ this._eventHandlers.onAuthenticated(this._request);
+ this.send();
+ } else {
+ this._eventHandlers.onReceiveResponse(response);
+ }
+ } else {
+ this._eventHandlers.onReceiveResponse(response);
+ }
+ }
};
diff --git a/src/SIPMessage.d.ts b/src/SIPMessage.d.ts
index 9b73733..31a20a9 100644
--- a/src/SIPMessage.d.ts
+++ b/src/SIPMessage.d.ts
@@ -1,52 +1,52 @@
-import {NameAddrHeader} from './NameAddrHeader'
-import {URI} from './URI'
+import { NameAddrHeader } from './NameAddrHeader';
+import { URI } from './URI';
declare class IncomingMessage {
- method: string
- from: NameAddrHeader
- to: NameAddrHeader
- body: string
+ method: string;
+ from: NameAddrHeader;
+ to: NameAddrHeader;
+ body: string;
- constructor();
+ constructor();
- countHeader(name: string): number;
+ countHeader(name: string): number;
- getHeader(name: string): string;
+ getHeader(name: string): string;
- getHeaders(name: string): string[];
+ getHeaders(name: string): string[];
- hasHeader(name: string): boolean;
+ hasHeader(name: string): boolean;
- parseHeader<T = unknown>(name: string, idx?: number): T;
+ parseHeader<T = unknown>(name: string, idx?: number): T;
- toString(): string;
+ toString(): string;
}
export class IncomingRequest extends IncomingMessage {
- ruri: URI
+ ruri: URI;
}
export class IncomingResponse extends IncomingMessage {
- status_code: number
- reason_phrase: string
+ status_code: number;
+ reason_phrase: string;
}
export class OutgoingRequest {
- method: string
- ruri: URI
- cseq: number
- call_id: string
- from: NameAddrHeader
- to: NameAddrHeader
- body: string
+ method: string;
+ ruri: URI;
+ cseq: number;
+ call_id: string;
+ from: NameAddrHeader;
+ to: NameAddrHeader;
+ body: string;
- setHeader(name: string, value: string | string[]): void;
+ setHeader(name: string, value: string | string[]): void;
- getHeader(name: string): string;
+ getHeader(name: string): string;
- getHeaders(name: string): string[];
+ getHeaders(name: string): string[];
- hasHeader(name: string): boolean;
+ hasHeader(name: string): boolean;
- toString(): string;
+ toString(): string;
}
diff --git a/src/SIPMessage.js b/src/SIPMessage.js
index 15d46e5..e2d205f 100644
--- a/src/SIPMessage.js
+++ b/src/SIPMessage.js
@@ -17,768 +17,668 @@ const logger = new Logger('SIPMessage');
* -param {Object} [headers] extra headers
* -param {String} [body]
*/
-class OutgoingRequest
-{
- constructor(method, ruri, ua, params, extraHeaders, body)
- {
- // Mandatory parameters check.
- if (!method || !ruri || !ua)
- {
- return null;
- }
-
- params = params || {};
-
- this.ua = ua;
- this.headers = {};
- this.method = method;
- this.ruri = ruri;
- this.body = body;
- this.extraHeaders = Utils.cloneArray(extraHeaders);
-
- if (this.ua.configuration.extra_headers)
- {
- this.extraHeaders = this.extraHeaders.concat(this.ua.configuration.extra_headers);
- }
-
- // Fill the Common SIP Request Headers.
-
- // Route.
- if (params.route_set)
- {
- this.setHeader('route', params.route_set);
- }
- else if (ua.configuration.use_preloaded_route)
- {
- this.setHeader('route', `<${ua.transport.sip_uri};lr>`);
- }
-
- // Via.
- // Empty Via header. Will be filled by the client transaction.
- this.setHeader('via', '');
-
- // Max-Forwards.
- this.setHeader('max-forwards', JsSIP_C.MAX_FORWARDS);
-
- // To
- const to_uri = params.to_uri || ruri;
- const to_params = params.to_tag ? { tag: params.to_tag } : null;
- const to_display_name = typeof params.to_display_name !== 'undefined' ? params.to_display_name : null;
-
- this.to = new NameAddrHeader(to_uri, to_display_name, to_params);
- this.setHeader('to', this.to.toString());
-
- // From.
- const from_uri = params.from_uri || ua.configuration.uri;
- const from_params = { tag: params.from_tag || Utils.newTag() };
- let display_name;
-
- if (typeof params.from_display_name !== 'undefined')
- {
- display_name = params.from_display_name;
- }
- else if (ua.configuration.display_name)
- {
- display_name = ua.configuration.display_name;
- }
- else
- {
- display_name = null;
- }
-
- this.from = new NameAddrHeader(from_uri, display_name, from_params);
- this.setHeader('from', this.from.toString());
-
- // Call-ID.
- const call_id = params.call_id ||
- (ua.configuration.jssip_id + Utils.createRandomToken(15));
-
- this.call_id = call_id;
- this.setHeader('call-id', call_id);
-
- // CSeq.
- const cseq = params.cseq || Math.floor(Math.random() * 10000);
-
- this.cseq = cseq;
- this.setHeader('cseq', `${cseq} ${method}`);
- }
-
- /**
- * Replace the the given header by the given value.
- * -param {String} name header name
- * -param {String | Array} value header value
- */
- setHeader(name, value)
- {
- // Remove the header from extraHeaders if present.
- const regexp = new RegExp(`^\\s*${name}\\s*:`, 'i');
-
- for (let idx=0; idx<this.extraHeaders.length; idx++)
- {
- if (regexp.test(this.extraHeaders[idx]))
- {
- this.extraHeaders.splice(idx, 1);
- }
- }
-
- this.headers[Utils.headerize(name)] = (Array.isArray(value)) ? value : [ value ];
- }
-
- /**
- * Get the value of the given header name at the given position.
- * -param {String} name header name
- * -returns {String|undefined} Returns the specified header, null if header doesn't exist.
- */
- getHeader(name)
- {
- const headers = this.headers[Utils.headerize(name)];
-
- if (headers)
- {
- if (headers[0])
- {
- return headers[0];
- }
- }
- else
- {
- const regexp = new RegExp(`^\\s*${name}\\s*:`, 'i');
-
- for (const header of this.extraHeaders)
- {
- if (regexp.test(header))
- {
- return header.substring(header.indexOf(':')+1).trim();
- }
- }
- }
-
- return;
- }
-
- /**
- * Get the header/s of the given name.
- * -param {String} name header name
- * -returns {Array} Array with all the headers of the specified name.
- */
- getHeaders(name)
- {
- const headers = this.headers[Utils.headerize(name)];
- const result = [];
-
- if (headers)
- {
- for (const header of headers)
- {
- result.push(header);
- }
-
- return result;
- }
- else
- {
- const regexp = new RegExp(`^\\s*${name}\\s*:`, 'i');
-
- for (const header of this.extraHeaders)
- {
- if (regexp.test(header))
- {
- result.push(header.substring(header.indexOf(':')+1).trim());
- }
- }
-
- return result;
- }
- }
-
- /**
- * Verify the existence of the given header.
- * -param {String} name header name
- * -returns {boolean} true if header with given name exists, false otherwise
- */
- hasHeader(name)
- {
- if (this.headers[Utils.headerize(name)])
- {
- return true;
- }
- else
- {
- const regexp = new RegExp(`^\\s*${name}\\s*:`, 'i');
-
- for (const header of this.extraHeaders)
- {
- if (regexp.test(header))
- {
- return true;
- }
- }
- }
-
- return false;
- }
-
- /**
- * Parse the current body as a SDP and store the resulting object
- * into this.sdp.
- * -param {Boolean} force: Parse even if this.sdp already exists.
- *
- * Returns this.sdp.
- */
- parseSDP(force)
- {
- if (!force && this.sdp)
- {
- return this.sdp;
- }
- else
- {
- this.sdp = sdp_transform.parse(this.body || '');
-
- return this.sdp;
- }
- }
-
- toString()
- {
- let msg = `${this.method} ${this.ruri} SIP/2.0\r\n`;
-
- for (const headerName in this.headers)
- {
- if (Object.prototype.hasOwnProperty.call(this.headers, headerName))
- {
- for (const headerValue of this.headers[headerName])
- {
- msg += `${headerName}: ${headerValue}\r\n`;
- }
- }
- }
-
- for (const header of this.extraHeaders)
- {
- msg += `${header.trim()}\r\n`;
- }
-
- // Supported.
- const supported = [];
-
- switch (this.method)
- {
- case JsSIP_C.REGISTER:
- supported.push('path', 'gruu');
- break;
- case JsSIP_C.INVITE:
- if (this.ua.configuration.session_timers)
- {
- supported.push('timer');
- }
- if (this.ua.contact.pub_gruu || this.ua.contact.temp_gruu)
- {
- supported.push('gruu');
- }
- supported.push('ice', 'replaces');
- break;
- case JsSIP_C.UPDATE:
- if (this.ua.configuration.session_timers)
- {
- supported.push('timer');
- }
- supported.push('ice');
- break;
- }
-
- supported.push('outbound');
-
- const userAgent = this.ua.configuration.user_agent || JsSIP_C.USER_AGENT;
-
- // Allow.
- msg += `Allow: ${JsSIP_C.ALLOWED_METHODS}\r\n`;
- msg += `Supported: ${supported}\r\n`;
- msg += `User-Agent: ${userAgent}\r\n`;
-
- if (this.body)
- {
- const length = Utils.str_utf8_length(this.body);
-
- msg += `Content-Length: ${length}\r\n\r\n`;
- msg += this.body;
- }
- else
- {
- msg += 'Content-Length: 0\r\n\r\n';
- }
-
- return msg;
- }
-
- clone()
- {
- const request = new OutgoingRequest(this.method, this.ruri, this.ua);
-
- Object.keys(this.headers).forEach(function(name)
- {
- request.headers[name] = this.headers[name].slice();
- }, this);
-
- request.body = this.body;
- request.extraHeaders = Utils.cloneArray(this.extraHeaders);
- request.to = this.to;
- request.from = this.from;
- request.call_id = this.call_id;
- request.cseq = this.cseq;
-
- return request;
- }
+class OutgoingRequest {
+ constructor(method, ruri, ua, params, extraHeaders, body) {
+ // Mandatory parameters check.
+ if (!method || !ruri || !ua) {
+ return null;
+ }
+
+ params = params || {};
+
+ this.ua = ua;
+ this.headers = {};
+ this.method = method;
+ this.ruri = ruri;
+ this.body = body;
+ this.extraHeaders = Utils.cloneArray(extraHeaders);
+
+ if (this.ua.configuration.extra_headers) {
+ this.extraHeaders = this.extraHeaders.concat(
+ this.ua.configuration.extra_headers
+ );
+ }
+
+ // Fill the Common SIP Request Headers.
+
+ // Route.
+ if (params.route_set) {
+ this.setHeader('route', params.route_set);
+ } else if (ua.configuration.use_preloaded_route) {
+ this.setHeader('route', `<${ua.transport.sip_uri};lr>`);
+ }
+
+ // Via.
+ // Empty Via header. Will be filled by the client transaction.
+ this.setHeader('via', '');
+
+ // Max-Forwards.
+ this.setHeader('max-forwards', JsSIP_C.MAX_FORWARDS);
+
+ // To
+ const to_uri = params.to_uri || ruri;
+ const to_params = params.to_tag ? { tag: params.to_tag } : null;
+ const to_display_name =
+ typeof params.to_display_name !== 'undefined'
+ ? params.to_display_name
+ : null;
+
+ this.to = new NameAddrHeader(to_uri, to_display_name, to_params);
+ this.setHeader('to', this.to.toString());
+
+ // From.
+ const from_uri = params.from_uri || ua.configuration.uri;
+ const from_params = { tag: params.from_tag || Utils.newTag() };
+ let display_name;
+
+ if (typeof params.from_display_name !== 'undefined') {
+ display_name = params.from_display_name;
+ } else if (ua.configuration.display_name) {
+ display_name = ua.configuration.display_name;
+ } else {
+ display_name = null;
+ }
+
+ this.from = new NameAddrHeader(from_uri, display_name, from_params);
+ this.setHeader('from', this.from.toString());
+
+ // Call-ID.
+ const call_id =
+ params.call_id || ua.configuration.jssip_id + Utils.createRandomToken(15);
+
+ this.call_id = call_id;
+ this.setHeader('call-id', call_id);
+
+ // CSeq.
+ const cseq = params.cseq || Math.floor(Math.random() * 10000);
+
+ this.cseq = cseq;
+ this.setHeader('cseq', `${cseq} ${method}`);
+ }
+
+ /**
+ * Replace the the given header by the given value.
+ * -param {String} name header name
+ * -param {String | Array} value header value
+ */
+ setHeader(name, value) {
+ // Remove the header from extraHeaders if present.
+ const regexp = new RegExp(`^\\s*${name}\\s*:`, 'i');
+
+ for (let idx = 0; idx < this.extraHeaders.length; idx++) {
+ if (regexp.test(this.extraHeaders[idx])) {
+ this.extraHeaders.splice(idx, 1);
+ }
+ }
+
+ this.headers[Utils.headerize(name)] = Array.isArray(value)
+ ? value
+ : [value];
+ }
+
+ /**
+ * Get the value of the given header name at the given position.
+ * -param {String} name header name
+ * -returns {String|undefined} Returns the specified header, null if header doesn't exist.
+ */
+ getHeader(name) {
+ const headers = this.headers[Utils.headerize(name)];
+
+ if (headers) {
+ if (headers[0]) {
+ return headers[0];
+ }
+ } else {
+ const regexp = new RegExp(`^\\s*${name}\\s*:`, 'i');
+
+ for (const header of this.extraHeaders) {
+ if (regexp.test(header)) {
+ return header.substring(header.indexOf(':') + 1).trim();
+ }
+ }
+ }
+
+ return;
+ }
+
+ /**
+ * Get the header/s of the given name.
+ * -param {String} name header name
+ * -returns {Array} Array with all the headers of the specified name.
+ */
+ getHeaders(name) {
+ const headers = this.headers[Utils.headerize(name)];
+ const result = [];
+
+ if (headers) {
+ for (const header of headers) {
+ result.push(header);
+ }
+
+ return result;
+ } else {
+ const regexp = new RegExp(`^\\s*${name}\\s*:`, 'i');
+
+ for (const header of this.extraHeaders) {
+ if (regexp.test(header)) {
+ result.push(header.substring(header.indexOf(':') + 1).trim());
+ }
+ }
+
+ return result;
+ }
+ }
+
+ /**
+ * Verify the existence of the given header.
+ * -param {String} name header name
+ * -returns {boolean} true if header with given name exists, false otherwise
+ */
+ hasHeader(name) {
+ if (this.headers[Utils.headerize(name)]) {
+ return true;
+ } else {
+ const regexp = new RegExp(`^\\s*${name}\\s*:`, 'i');
+
+ for (const header of this.extraHeaders) {
+ if (regexp.test(header)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Parse the current body as a SDP and store the resulting object
+ * into this.sdp.
+ * -param {Boolean} force: Parse even if this.sdp already exists.
+ *
+ * Returns this.sdp.
+ */
+ parseSDP(force) {
+ if (!force && this.sdp) {
+ return this.sdp;
+ } else {
+ this.sdp = sdp_transform.parse(this.body || '');
+
+ return this.sdp;
+ }
+ }
+
+ toString() {
+ let msg = `${this.method} ${this.ruri} SIP/2.0\r\n`;
+
+ for (const headerName in this.headers) {
+ if (Object.prototype.hasOwnProperty.call(this.headers, headerName)) {
+ for (const headerValue of this.headers[headerName]) {
+ msg += `${headerName}: ${headerValue}\r\n`;
+ }
+ }
+ }
+
+ for (const header of this.extraHeaders) {
+ msg += `${header.trim()}\r\n`;
+ }
+
+ // Supported.
+ const supported = [];
+
+ switch (this.method) {
+ case JsSIP_C.REGISTER: {
+ supported.push('path', 'gruu');
+ break;
+ }
+ case JsSIP_C.INVITE: {
+ if (this.ua.configuration.session_timers) {
+ supported.push('timer');
+ }
+ if (this.ua.contact.pub_gruu || this.ua.contact.temp_gruu) {
+ supported.push('gruu');
+ }
+ supported.push('ice', 'replaces');
+ break;
+ }
+ case JsSIP_C.UPDATE: {
+ if (this.ua.configuration.session_timers) {
+ supported.push('timer');
+ }
+ supported.push('ice');
+ break;
+ }
+ }
+
+ supported.push('outbound');
+
+ const userAgent = this.ua.configuration.user_agent || JsSIP_C.USER_AGENT;
+
+ // Allow.
+ msg += `Allow: ${JsSIP_C.ALLOWED_METHODS}\r\n`;
+ msg += `Supported: ${supported}\r\n`;
+ msg += `User-Agent: ${userAgent}\r\n`;
+
+ if (this.body) {
+ const length = Utils.str_utf8_length(this.body);
+
+ msg += `Content-Length: ${length}\r\n\r\n`;
+ msg += this.body;
+ } else {
+ msg += 'Content-Length: 0\r\n\r\n';
+ }
+
+ return msg;
+ }
+
+ clone() {
+ const request = new OutgoingRequest(this.method, this.ruri, this.ua);
+
+ Object.keys(this.headers).forEach(function (name) {
+ request.headers[name] = this.headers[name].slice();
+ }, this);
+
+ request.body = this.body;
+ request.extraHeaders = Utils.cloneArray(this.extraHeaders);
+ request.to = this.to;
+ request.from = this.from;
+ request.call_id = this.call_id;
+ request.cseq = this.cseq;
+
+ return request;
+ }
}
-class InitialOutgoingInviteRequest extends OutgoingRequest
-{
- constructor(ruri, ua, params, extraHeaders, body)
- {
- super(JsSIP_C.INVITE, ruri, ua, params, extraHeaders, body);
+class InitialOutgoingInviteRequest extends OutgoingRequest {
+ constructor(ruri, ua, params, extraHeaders, body) {
+ super(JsSIP_C.INVITE, ruri, ua, params, extraHeaders, body);
- this.transaction = null;
- }
+ this.transaction = null;
+ }
- cancel(reason)
- {
- this.transaction.cancel(reason);
- }
+ cancel(reason) {
+ this.transaction.cancel(reason);
+ }
- clone()
- {
- const request = new InitialOutgoingInviteRequest(this.ruri, this.ua);
+ clone() {
+ const request = new InitialOutgoingInviteRequest(this.ruri, this.ua);
- Object.keys(this.headers).forEach(function(name)
- {
- request.headers[name] = this.headers[name].slice();
- }, this);
+ Object.keys(this.headers).forEach(function (name) {
+ request.headers[name] = this.headers[name].slice();
+ }, this);
- request.body = this.body;
- request.extraHeaders = Utils.cloneArray(this.extraHeaders);
- request.to = this.to;
- request.from = this.from;
- request.call_id = this.call_id;
- request.cseq = this.cseq;
+ request.body = this.body;
+ request.extraHeaders = Utils.cloneArray(this.extraHeaders);
+ request.to = this.to;
+ request.from = this.from;
+ request.call_id = this.call_id;
+ request.cseq = this.cseq;
- request.transaction = this.transaction;
+ request.transaction = this.transaction;
- return request;
- }
+ return request;
+ }
}
-class IncomingMessage
-{
- constructor()
- {
- this.data = null;
- this.headers = null;
- this.method = null;
- this.via = null;
- this.via_branch = null;
- this.call_id = null;
- this.cseq = null;
- this.from = null;
- this.from_tag = null;
- this.to = null;
- this.to_tag = null;
- this.body = null;
- this.sdp = null;
- }
-
- /**
- * Insert a header of the given name and value into the last position of the
- * header array.
- */
- addHeader(name, value)
- {
- const header = { raw: value };
-
- name = Utils.headerize(name);
-
- if (this.headers[name])
- {
- this.headers[name].push(header);
- }
- else
- {
- this.headers[name] = [ header ];
- }
- }
-
- /**
- * Get the value of the given header name at the given position.
- */
- getHeader(name)
- {
- const header = this.headers[Utils.headerize(name)];
-
- if (header)
- {
- if (header[0])
- {
- return header[0].raw;
- }
- }
- else
- {
- return;
- }
- }
-
- /**
- * Get the header/s of the given name.
- */
- getHeaders(name)
- {
- const headers = this.headers[Utils.headerize(name)];
- const result = [];
-
- if (!headers)
- {
- return [];
- }
-
- for (const header of headers)
- {
- result.push(header.raw);
- }
-
- return result;
- }
-
- /**
- * Verify the existence of the given header.
- */
- hasHeader(name)
- {
- return (this.headers[Utils.headerize(name)]) ? true : false;
- }
-
- /**
- * Parse the given header on the given index.
- * -param {String} name header name
- * -param {Number} [idx=0] header index
- * -returns {Object|undefined} Parsed header object, undefined if the header
- * is not present or in case of a parsing error.
- */
- parseHeader(name, idx = 0)
- {
- name = Utils.headerize(name);
-
- if (!this.headers[name])
- {
- logger.debug(`header "${name}" not present`);
-
- return;
- }
- else if (idx >= this.headers[name].length)
- {
- logger.debug(`not so many "${name}" headers present`);
-
- return;
- }
-
- const header = this.headers[name][idx];
- const value = header.raw;
-
- if (header.parsed)
- {
- return header.parsed;
- }
-
- // Substitute '-' by '_' for grammar rule matching.
- const parsed = Grammar.parse(value, name.replace(/-/g, '_'));
-
- if (parsed === -1)
- {
- this.headers[name].splice(idx, 1); // delete from headers
- logger.debug(`error parsing "${name}" header field with value "${value}"`);
-
- return;
- }
- else
- {
- header.parsed = parsed;
-
- return parsed;
- }
- }
-
- /**
- * Message Header attribute selector. Alias of parseHeader.
- * -param {String} name header name
- * -param {Number} [idx=0] header index
- * -returns {Object|undefined} Parsed header object, undefined if the header
- * is not present or in case of a parsing error.
- *
- * -example
- * message.s('via',3).port
- */
- s(name, idx)
- {
- return this.parseHeader(name, idx);
- }
-
- /**
- * Replace the value of the given header by the value.
- * -param {String} name header name
- * -param {String} value header value
- */
- setHeader(name, value)
- {
- const header = { raw: value };
-
- this.headers[Utils.headerize(name)] = [ header ];
- }
-
- /**
- * Parse the current body as a SDP and store the resulting object
- * into this.sdp.
- * -param {Boolean} force: Parse even if this.sdp already exists.
- *
- * Returns this.sdp.
- */
- parseSDP(force)
- {
- if (!force && this.sdp)
- {
- return this.sdp;
- }
- else
- {
- this.sdp = sdp_transform.parse(this.body || '');
-
- return this.sdp;
- }
- }
-
- toString()
- {
- return this.data;
- }
+class IncomingMessage {
+ constructor() {
+ this.data = null;
+ this.headers = null;
+ this.method = null;
+ this.via = null;
+ this.via_branch = null;
+ this.call_id = null;
+ this.cseq = null;
+ this.from = null;
+ this.from_tag = null;
+ this.to = null;
+ this.to_tag = null;
+ this.body = null;
+ this.sdp = null;
+ }
+
+ /**
+ * Insert a header of the given name and value into the last position of the
+ * header array.
+ */
+ addHeader(name, value) {
+ const header = { raw: value };
+
+ name = Utils.headerize(name);
+
+ if (this.headers[name]) {
+ this.headers[name].push(header);
+ } else {
+ this.headers[name] = [header];
+ }
+ }
+
+ /**
+ * Get the value of the given header name at the given position.
+ */
+ getHeader(name) {
+ const header = this.headers[Utils.headerize(name)];
+
+ if (header) {
+ if (header[0]) {
+ return header[0].raw;
+ }
+ } else {
+ return;
+ }
+ }
+
+ /**
+ * Get the header/s of the given name.
+ */
+ getHeaders(name) {
+ const headers = this.headers[Utils.headerize(name)];
+ const result = [];
+
+ if (!headers) {
+ return [];
+ }
+
+ for (const header of headers) {
+ result.push(header.raw);
+ }
+
+ return result;
+ }
+
+ /**
+ * Verify the existence of the given header.
+ */
+ hasHeader(name) {
+ return this.headers[Utils.headerize(name)] ? true : false;
+ }
+
+ /**
+ * Parse the given header on the given index.
+ * -param {String} name header name
+ * -param {Number} [idx=0] header index
+ * -returns {Object|undefined} Parsed header object, undefined if the header
+ * is not present or in case of a parsing error.
+ */
+ parseHeader(name, idx = 0) {
+ name = Utils.headerize(name);
+
+ if (!this.headers[name]) {
+ logger.debug(`header "${name}" not present`);
+
+ return;
+ } else if (idx >= this.headers[name].length) {
+ logger.debug(`not so many "${name}" headers present`);
+
+ return;
+ }
+
+ const header = this.headers[name][idx];
+ const value = header.raw;
+
+ if (header.parsed) {
+ return header.parsed;
+ }
+
+ // Substitute '-' by '_' for grammar rule matching.
+ const parsed = Grammar.parse(value, name.replace(/-/g, '_'));
+
+ if (parsed === -1) {
+ this.headers[name].splice(idx, 1); // delete from headers
+ logger.debug(
+ `error parsing "${name}" header field with value "${value}"`
+ );
+
+ return;
+ } else {
+ header.parsed = parsed;
+
+ return parsed;
+ }
+ }
+
+ /**
+ * Message Header attribute selector. Alias of parseHeader.
+ * -param {String} name header name
+ * -param {Number} [idx=0] header index
+ * -returns {Object|undefined} Parsed header object, undefined if the header
+ * is not present or in case of a parsing error.
+ *
+ * -example
+ * message.s('via',3).port
+ */
+ s(name, idx) {
+ return this.parseHeader(name, idx);
+ }
+
+ /**
+ * Replace the value of the given header by the value.
+ * -param {String} name header name
+ * -param {String} value header value
+ */
+ setHeader(name, value) {
+ const header = { raw: value };
+
+ this.headers[Utils.headerize(name)] = [header];
+ }
+
+ /**
+ * Parse the current body as a SDP and store the resulting object
+ * into this.sdp.
+ * -param {Boolean} force: Parse even if this.sdp already exists.
+ *
+ * Returns this.sdp.
+ */
+ parseSDP(force) {
+ if (!force && this.sdp) {
+ return this.sdp;
+ } else {
+ this.sdp = sdp_transform.parse(this.body || '');
+
+ return this.sdp;
+ }
+ }
+
+ toString() {
+ return this.data;
+ }
}
-class IncomingRequest extends IncomingMessage
-{
- constructor(ua)
- {
- super();
-
- this.ua = ua;
- this.headers = {};
- this.ruri = null;
- this.transport = null;
- this.server_transaction = null;
- }
-
- /**
- * Stateful reply.
- * -param {Number} code status code
- * -param {String} reason reason phrase
- * -param {Object} headers extra headers
- * -param {String} body body
- * -param {Function} [onSuccess] onSuccess callback
- * -param {Function} [onFailure] onFailure callback
- */
- reply(code, reason, extraHeaders, body, onSuccess, onFailure)
- {
- const supported = [];
- let to = this.getHeader('To');
-
- code = code || null;
- reason = reason || null;
-
- // Validate code and reason values.
- if (!code || (code < 100 || code > 699))
- {
- throw new TypeError(`Invalid status_code: ${code}`);
- }
- else if (reason && typeof reason !== 'string' && !(reason instanceof String))
- {
- throw new TypeError(`Invalid reason_phrase: ${reason}`);
- }
-
- reason = reason || JsSIP_C.REASON_PHRASE[code] || '';
- extraHeaders = Utils.cloneArray(extraHeaders);
-
- if (this.ua.configuration.extra_headers)
- {
- extraHeaders = extraHeaders.concat(this.ua.configuration.extra_headers);
- }
-
- let response = `SIP/2.0 ${code} ${reason}\r\n`;
-
- if (this.method === JsSIP_C.INVITE && code > 100 && code <= 200)
- {
- const headers = this.getHeaders('record-route');
-
- for (const header of headers)
- {
- response += `Record-Route: ${header}\r\n`;
- }
- }
-
- const vias = this.getHeaders('via');
-
- for (const via of vias)
- {
- response += `Via: ${via}\r\n`;
- }
-
- if (!this.to_tag && code > 100)
- {
- to += `;tag=${Utils.newTag()}`;
- }
- else if (this.to_tag && !this.s('to').hasParam('tag'))
- {
- to += `;tag=${this.to_tag}`;
- }
-
- response += `To: ${to}\r\n`;
- response += `From: ${this.getHeader('From')}\r\n`;
- response += `Call-ID: ${this.call_id}\r\n`;
- response += `CSeq: ${this.cseq} ${this.method}\r\n`;
-
- for (const header of extraHeaders)
- {
- response += `${header.trim()}\r\n`;
- }
-
- // Supported.
- switch (this.method)
- {
- case JsSIP_C.INVITE:
- if (this.ua.configuration.session_timers)
- {
- supported.push('timer');
- }
- if (this.ua.contact.pub_gruu || this.ua.contact.temp_gruu)
- {
- supported.push('gruu');
- }
- supported.push('ice', 'replaces');
- break;
- case JsSIP_C.UPDATE:
- if (this.ua.configuration.session_timers)
- {
- supported.push('timer');
- }
- if (body)
- {
- supported.push('ice');
- }
- supported.push('replaces');
- }
-
- supported.push('outbound');
-
- // Allow and Accept.
- if (this.method === JsSIP_C.OPTIONS)
- {
- response += `Allow: ${JsSIP_C.ALLOWED_METHODS}\r\n`;
- response += `Accept: ${JsSIP_C.ACCEPTED_BODY_TYPES}\r\n`;
- }
- else if (code === 405)
- {
- response += `Allow: ${JsSIP_C.ALLOWED_METHODS}\r\n`;
- }
- else if (code === 415)
- {
- response += `Accept: ${JsSIP_C.ACCEPTED_BODY_TYPES}\r\n`;
- }
-
- response += `Supported: ${supported}\r\n`;
-
- if (body)
- {
- const length = Utils.str_utf8_length(body);
-
- response += 'Content-Type: application/sdp\r\n';
- response += `Content-Length: ${length}\r\n\r\n`;
- response += body;
- }
- else
- {
- response += `Content-Length: ${0}\r\n\r\n`;
- }
-
- this.server_transaction.receiveResponse(code, response, onSuccess, onFailure);
- }
-
- /**
- * Stateless reply.
- * -param {Number} code status code
- * -param {String} reason reason phrase
- */
- reply_sl(code = null, reason = null)
- {
- const vias = this.getHeaders('via');
-
- // Validate code and reason values.
- if (!code || (code < 100 || code > 699))
- {
- throw new TypeError(`Invalid status_code: ${code}`);
- }
- else if (reason && typeof reason !== 'string' && !(reason instanceof String))
- {
- throw new TypeError(`Invalid reason_phrase: ${reason}`);
- }
-
- reason = reason || JsSIP_C.REASON_PHRASE[code] || '';
-
- let response = `SIP/2.0 ${code} ${reason}\r\n`;
-
- for (const via of vias)
- {
- response += `Via: ${via}\r\n`;
- }
-
- let to = this.getHeader('To');
-
- if (!this.to_tag && code > 100)
- {
- to += `;tag=${Utils.newTag()}`;
- }
- else if (this.to_tag && !this.s('to').hasParam('tag'))
- {
- to += `;tag=${this.to_tag}`;
- }
-
- response += `To: ${to}\r\n`;
- response += `From: ${this.getHeader('From')}\r\n`;
- response += `Call-ID: ${this.call_id}\r\n`;
- response += `CSeq: ${this.cseq} ${this.method}\r\n`;
-
- if (this.ua.configuration.extra_headers)
- {
- for (const header of this.ua.configuration.extra_headers)
- {
- response += `${header.trim()}\r\n`;
- }
- }
-
- response += `Content-Length: ${0}\r\n\r\n`;
-
- this.transport.send(response);
- }
+class IncomingRequest extends IncomingMessage {
+ constructor(ua) {
+ super();
+
+ this.ua = ua;
+ this.headers = {};
+ this.ruri = null;
+ this.transport = null;
+ this.server_transaction = null;
+ }
+
+ /**
+ * Stateful reply.
+ * -param {Number} code status code
+ * -param {String} reason reason phrase
+ * -param {Object} headers extra headers
+ * -param {String} body body
+ * -param {Function} [onSuccess] onSuccess callback
+ * -param {Function} [onFailure] onFailure callback
+ */
+ reply(code, reason, extraHeaders, body, onSuccess, onFailure) {
+ const supported = [];
+ let to = this.getHeader('To');
+
+ code = code || null;
+ reason = reason || null;
+
+ // Validate code and reason values.
+ if (!code || code < 100 || code > 699) {
+ throw new TypeError(`Invalid status_code: ${code}`);
+ } else if (
+ reason &&
+ typeof reason !== 'string' &&
+ !(reason instanceof String)
+ ) {
+ throw new TypeError(`Invalid reason_phrase: ${reason}`);
+ }
+
+ reason = reason || JsSIP_C.REASON_PHRASE[code] || '';
+ extraHeaders = Utils.cloneArray(extraHeaders);
+
+ if (this.ua.configuration.extra_headers) {
+ extraHeaders = extraHeaders.concat(this.ua.configuration.extra_headers);
+ }
+
+ let response = `SIP/2.0 ${code} ${reason}\r\n`;
+
+ if (this.method === JsSIP_C.INVITE && code > 100 && code <= 200) {
+ const headers = this.getHeaders('record-route');
+
+ for (const header of headers) {
+ response += `Record-Route: ${header}\r\n`;
+ }
+ }
+
+ const vias = this.getHeaders('via');
+
+ for (const via of vias) {
+ response += `Via: ${via}\r\n`;
+ }
+
+ if (!this.to_tag && code > 100) {
+ to += `;tag=${Utils.newTag()}`;
+ } else if (this.to_tag && !this.s('to').hasParam('tag')) {
+ to += `;tag=${this.to_tag}`;
+ }
+
+ response += `To: ${to}\r\n`;
+ response += `From: ${this.getHeader('From')}\r\n`;
+ response += `Call-ID: ${this.call_id}\r\n`;
+ response += `CSeq: ${this.cseq} ${this.method}\r\n`;
+
+ for (const header of extraHeaders) {
+ response += `${header.trim()}\r\n`;
+ }
+
+ // Supported.
+ switch (this.method) {
+ case JsSIP_C.INVITE: {
+ if (this.ua.configuration.session_timers) {
+ supported.push('timer');
+ }
+ if (this.ua.contact.pub_gruu || this.ua.contact.temp_gruu) {
+ supported.push('gruu');
+ }
+ supported.push('ice', 'replaces');
+ break;
+ }
+ case JsSIP_C.UPDATE: {
+ if (this.ua.configuration.session_timers) {
+ supported.push('timer');
+ }
+ if (body) {
+ supported.push('ice');
+ }
+ supported.push('replaces');
+ }
+ }
+
+ supported.push('outbound');
+
+ // Allow and Accept.
+ if (this.method === JsSIP_C.OPTIONS) {
+ response += `Allow: ${JsSIP_C.ALLOWED_METHODS}\r\n`;
+ response += `Accept: ${JsSIP_C.ACCEPTED_BODY_TYPES}\r\n`;
+ } else if (code === 405) {
+ response += `Allow: ${JsSIP_C.ALLOWED_METHODS}\r\n`;
+ } else if (code === 415) {
+ response += `Accept: ${JsSIP_C.ACCEPTED_BODY_TYPES}\r\n`;
+ }
+
+ response += `Supported: ${supported}\r\n`;
+
+ if (body) {
+ const length = Utils.str_utf8_length(body);
+
+ response += 'Content-Type: application/sdp\r\n';
+ response += `Content-Length: ${length}\r\n\r\n`;
+ response += body;
+ } else {
+ response += `Content-Length: ${0}\r\n\r\n`;
+ }
+
+ this.server_transaction.receiveResponse(
+ code,
+ response,
+ onSuccess,
+ onFailure
+ );
+ }
+
+ /**
+ * Stateless reply.
+ * -param {Number} code status code
+ * -param {String} reason reason phrase
+ */
+ reply_sl(code = null, reason = null) {
+ const vias = this.getHeaders('via');
+
+ // Validate code and reason values.
+ if (!code || code < 100 || code > 699) {
+ throw new TypeError(`Invalid status_code: ${code}`);
+ } else if (
+ reason &&
+ typeof reason !== 'string' &&
+ !(reason instanceof String)
+ ) {
+ throw new TypeError(`Invalid reason_phrase: ${reason}`);
+ }
+
+ reason = reason || JsSIP_C.REASON_PHRASE[code] || '';
+
+ let response = `SIP/2.0 ${code} ${reason}\r\n`;
+
+ for (const via of vias) {
+ response += `Via: ${via}\r\n`;
+ }
+
+ let to = this.getHeader('To');
+
+ if (!this.to_tag && code > 100) {
+ to += `;tag=${Utils.newTag()}`;
+ } else if (this.to_tag && !this.s('to').hasParam('tag')) {
+ to += `;tag=${this.to_tag}`;
+ }
+
+ response += `To: ${to}\r\n`;
+ response += `From: ${this.getHeader('From')}\r\n`;
+ response += `Call-ID: ${this.call_id}\r\n`;
+ response += `CSeq: ${this.cseq} ${this.method}\r\n`;
+
+ if (this.ua.configuration.extra_headers) {
+ for (const header of this.ua.configuration.extra_headers) {
+ response += `${header.trim()}\r\n`;
+ }
+ }
+
+ response += `Content-Length: ${0}\r\n\r\n`;
+
+ this.transport.send(response);
+ }
}
-class IncomingResponse extends IncomingMessage
-{
- constructor()
- {
- super();
+class IncomingResponse extends IncomingMessage {
+ constructor() {
+ super();
- this.headers = {};
- this.status_code = null;
- this.reason_phrase = null;
- }
+ this.headers = {};
+ this.status_code = null;
+ this.reason_phrase = null;
+ }
}
module.exports = {
- OutgoingRequest,
- InitialOutgoingInviteRequest,
- IncomingRequest,
- IncomingResponse
+ OutgoingRequest,
+ InitialOutgoingInviteRequest,
+ IncomingRequest,
+ IncomingResponse,
};
diff --git a/src/Socket.d.ts b/src/Socket.d.ts
index 6f63543..034ac62 100644
--- a/src/Socket.d.ts
+++ b/src/Socket.d.ts
@@ -1,29 +1,29 @@
-export interface WeightedSocket {
- socket: Socket;
- weight: number
+export interface WeightedSocket {
+ socket: Socket;
+ weight: number;
}
export class Socket {
- get via_transport(): string;
- set via_transport(value: string);
+ get via_transport(): string;
+ set via_transport(value: string);
- get url(): string;
+ get url(): string;
- get sip_uri(): string;
+ get sip_uri(): string;
- connect(): void;
+ connect(): void;
- disconnect(): void;
+ disconnect(): void;
- send(message: string | ArrayBufferLike | Blob | ArrayBufferView): boolean;
+ send(message: string | ArrayBufferLike | Blob | ArrayBufferView): boolean;
- isConnected(): boolean;
+ isConnected(): boolean;
- isConnecting(): boolean;
+ isConnecting(): boolean;
- onconnect(): void;
+ onconnect(): void;
- ondisconnect(error: boolean, code?: number, reason?: string): void;
+ ondisconnect(error: boolean, code?: number, reason?: string): void;
- ondata<T>(event: T): void;
+ ondata<T>(event: T): void;
}
diff --git a/src/Socket.js b/src/Socket.js
index a45b4ad..e502360 100644
--- a/src/Socket.js
+++ b/src/Socket.js
@@ -23,65 +23,49 @@ const logger = new Logger('Socket');
*
*/
-exports.isSocket = (socket) =>
-{
- // Ignore if an array is given.
- if (Array.isArray(socket))
- {
- return false;
- }
+exports.isSocket = socket => {
+ // Ignore if an array is given.
+ if (Array.isArray(socket)) {
+ return false;
+ }
- if (typeof socket === 'undefined')
- {
- logger.warn('undefined JsSIP.Socket instance');
+ if (typeof socket === 'undefined') {
+ logger.warn('undefined JsSIP.Socket instance');
- return false;
- }
+ return false;
+ }
- // Check Properties.
- try
- {
- if (!Utils.isString(socket.url))
- {
- logger.warn('missing or invalid JsSIP.Socket url property');
- throw new Error('Missing or invalid JsSIP.Socket url property');
- }
+ // Check Properties.
+ try {
+ if (!Utils.isString(socket.url)) {
+ logger.warn('missing or invalid JsSIP.Socket url property');
+ throw new Error('Missing or invalid JsSIP.Socket url property');
+ }
- if (!Utils.isString(socket.via_transport))
- {
- logger.warn('missing or invalid JsSIP.Socket via_transport property');
- throw new Error('Missing or invalid JsSIP.Socket via_transport property');
- }
+ if (!Utils.isString(socket.via_transport)) {
+ logger.warn('missing or invalid JsSIP.Socket via_transport property');
+ throw new Error('Missing or invalid JsSIP.Socket via_transport property');
+ }
- if (Grammar.parse(socket.sip_uri, 'SIP_URI') === -1)
- {
- logger.warn('missing or invalid JsSIP.Socket sip_uri property');
- throw new Error('missing or invalid JsSIP.Socket sip_uri property');
- }
- }
- // eslint-disable-next-line no-unused-vars
- catch (error)
- {
- return false;
- }
+ if (Grammar.parse(socket.sip_uri, 'SIP_URI') === -1) {
+ logger.warn('missing or invalid JsSIP.Socket sip_uri property');
+ throw new Error('missing or invalid JsSIP.Socket sip_uri property');
+ }
+ } catch (error) {
+ return false;
+ }
- // Check Methods.
- try
- {
- [ 'connect', 'disconnect', 'send' ].forEach((method) =>
- {
- if (!Utils.isFunction(socket[method]))
- {
- logger.warn(`missing or invalid JsSIP.Socket method: ${method}`);
- throw new Error(`Missing or invalid JsSIP.Socket method: ${method}`);
- }
- });
- }
- // eslint-disable-next-line no-unused-vars
- catch (error)
- {
- return false;
- }
+ // Check Methods.
+ try {
+ ['connect', 'disconnect', 'send'].forEach(method => {
+ if (!Utils.isFunction(socket[method])) {
+ logger.warn(`missing or invalid JsSIP.Socket method: ${method}`);
+ throw new Error(`Missing or invalid JsSIP.Socket method: ${method}`);
+ }
+ });
+ } catch (error) {
+ return false;
+ }
- return true;
+ return true;
};
diff --git a/src/Subscriber.d.ts b/src/Subscriber.d.ts
index 060f0ea..20bb039 100644
--- a/src/Subscriber.d.ts
+++ b/src/Subscriber.d.ts
@@ -1,42 +1,60 @@
-import {EventEmitter} from 'events'
-import {IncomingRequest} from './SIPMessage'
-import {UA} from './UA'
+import { EventEmitter } from 'events';
+import { IncomingRequest } from './SIPMessage';
+import { UA } from './UA';
declare enum SubscriberTerminatedCode {
- SUBSCRIBE_RESPONSE_TIMEOUT = 0,
- SUBSCRIBE_TRANSPORT_ERROR = 1,
- SUBSCRIBE_NON_OK_RESPONSE = 2,
- SUBSCRIBE_WRONG_OK_RESPONSE = 3,
- SUBSCRIBE_AUTHENTICATION_FAILED = 4,
- UNSUBSCRIBE_TIMEOUT = 5,
- FINAL_NOTIFY_RECEIVED = 6,
- WRONG_NOTIFY_RECEIVED = 7
+ SUBSCRIBE_RESPONSE_TIMEOUT = 0,
+ SUBSCRIBE_TRANSPORT_ERROR = 1,
+ SUBSCRIBE_NON_OK_RESPONSE = 2,
+ SUBSCRIBE_WRONG_OK_RESPONSE = 3,
+ SUBSCRIBE_AUTHENTICATION_FAILED = 4,
+ UNSUBSCRIBE_TIMEOUT = 5,
+ FINAL_NOTIFY_RECEIVED = 6,
+ WRONG_NOTIFY_RECEIVED = 7,
}
export interface MessageEventMap {
- pending: [];
- accepted: [];
- active: [];
- terminated: [terminationCode: SubscriberTerminatedCode, reason: string | undefined, retryAfter: number | undefined];
- notify: [isFinal: boolean, request: IncomingRequest, body: string | undefined, contentType: string | undefined];
+ pending: [];
+ accepted: [];
+ active: [];
+ terminated: [
+ terminationCode: SubscriberTerminatedCode,
+ reason: string | undefined,
+ retryAfter: number | undefined,
+ ];
+ notify: [
+ isFinal: boolean,
+ request: IncomingRequest,
+ body: string | undefined,
+ contentType: string | undefined,
+ ];
}
interface SubscriberOptions {
- expires?: number;
- contentType: string;
- allowEvents?: string;
- params?: Record<string, any>;
- extraHeaders?: Array<string>;
+ expires?: number;
+ contentType: string;
+ allowEvents?: string;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ params?: Record<string, any>;
+ extraHeaders?: string[];
}
export class Subscriber extends EventEmitter<MessageEventMap> {
- constructor(ua: UA, target: string, eventName: string, accept: string, options: SubscriberOptions)
- subscribe(body?: string): void;
- terminate(body?: string): void;
- get state(): string;
- get id(): string;
- set data(_data: any);
- get data(): any;
- static get C(): typeof SubscriberTerminatedCode;
- get C(): typeof SubscriberTerminatedCode;
+ constructor(
+ ua: UA,
+ target: string,
+ eventName: string,
+ accept: string,
+ options: SubscriberOptions
+ );
+ subscribe(body?: string): void;
+ terminate(body?: string): void;
+ get state(): string;
+ get id(): string;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ set data(_data: any);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ get data(): any;
+ static get C(): typeof SubscriberTerminatedCode;
+ get C(): typeof SubscriberTerminatedCode;
}
diff --git a/src/Subscriber.js b/src/Subscriber.js
index f8d5fe9..13f8bde 100644
--- a/src/Subscriber.js
+++ b/src/Subscriber.js
@@ -14,571 +14,515 @@ const logger = new Logger('Subscriber');
* Termination codes.
*/
const C = {
- // Termination codes.
- SUBSCRIBE_RESPONSE_TIMEOUT : 0,
- SUBSCRIBE_TRANSPORT_ERROR : 1,
- SUBSCRIBE_NON_OK_RESPONSE : 2,
- SUBSCRIBE_WRONG_OK_RESPONSE : 3,
- SUBSCRIBE_AUTHENTICATION_FAILED : 4,
- UNSUBSCRIBE_TIMEOUT : 5,
- FINAL_NOTIFY_RECEIVED : 6,
- WRONG_NOTIFY_RECEIVED : 7,
-
- // Subscriber states.
- STATE_PENDING : 0,
- STATE_ACTIVE : 1,
- STATE_TERMINATED : 2,
- STATE_INIT : 3,
- STATE_WAITING_NOTIFY : 4,
-
- // RFC 6665 3.1.1, default expires value.
- DEFAULT_EXPIRES_SEC : 900
+ // Termination codes.
+ SUBSCRIBE_RESPONSE_TIMEOUT: 0,
+ SUBSCRIBE_TRANSPORT_ERROR: 1,
+ SUBSCRIBE_NON_OK_RESPONSE: 2,
+ SUBSCRIBE_WRONG_OK_RESPONSE: 3,
+ SUBSCRIBE_AUTHENTICATION_FAILED: 4,
+ UNSUBSCRIBE_TIMEOUT: 5,
+ FINAL_NOTIFY_RECEIVED: 6,
+ WRONG_NOTIFY_RECEIVED: 7,
+
+ // Subscriber states.
+ STATE_PENDING: 0,
+ STATE_ACTIVE: 1,
+ STATE_TERMINATED: 2,
+ STATE_INIT: 3,
+ STATE_WAITING_NOTIFY: 4,
+
+ // RFC 6665 3.1.1, default expires value.
+ DEFAULT_EXPIRES_SEC: 900,
};
/**
* RFC 6665 Subscriber implementation.
*/
-module.exports = class Subscriber extends EventEmitter
-{
- /**
- * Expose C object.
- */
- static get C()
- {
- return C;
- }
-
- /**
- * @param {UA} ua - reference to JsSIP.UA
- * @param {string} target
- * @param {string} eventName - Event header value. May end with optional ;id=xxx
- * @param {string} accept - Accept header value.
- *
- * @param {SubscriberOption} options - optional parameters.
- * @param {number} expires - Expires header value. Default is 900.
- * @param {string} contentType - Content-Type header value. Used for SUBSCRIBE with body
- * @param {string} allowEvents - Allow-Events header value.
- * @param {RequestParams} params - Will have priority over ua.configuration.
- * If set please define: to_uri, to_display_name, from_uri, from_display_name
- * @param {Array<string>} extraHeaders - Additional SIP headers.
- */
- constructor(ua, target, eventName, accept, { expires, contentType,
- allowEvents, params, extraHeaders })
- {
- logger.debug('new');
-
- super();
-
- // Check that arguments are defined.
- if (!target)
- {
- throw new TypeError('Not enough arguments: Missing target');
- }
-
- if (!eventName)
- {
- throw new TypeError('Not enough arguments: Missing eventName');
- }
-
- if (!accept)
- {
- throw new TypeError('Not enough arguments: Missing accept');
- }
-
- const event_header = Grammar.parse(eventName, 'Event');
-
- if (event_header === -1)
- {
- throw new TypeError('Missing Event header field');
- }
-
- this._ua = ua;
- this._target = target;
-
- if (!Utils.isDecimal(expires) || expires <= 0)
- {
- expires = C.DEFAULT_EXPIRES_SEC;
- }
-
- this._expires = expires;
-
- // Used to subscribe with body.
- this._content_type = contentType;
-
- // Set initial subscribe parameters.
- this._params = Utils.cloneObject(params);
-
- if (!this._params.from_uri)
- {
- this._params.from_uri = this._ua.configuration.uri;
- }
-
- this._params.from_tag = Utils.newTag();
- this._params.to_tag = null;
- this._params.call_id = Utils.createRandomToken(20);
-
- // Create subscribe cseq if not defined custom cseq.
- if (this._params.cseq === undefined)
- {
- this._params.cseq = Math.floor((Math.random() * 10000) + 1);
- }
-
- // Subscriber state.
- this._state = C.STATE_INIT;
-
- // Dialog.
- this._dialog = null;
-
- // To refresh subscription.
- this._expires_timer = null;
- this._expires_timestamp = null;
-
- // To prevent duplicate terminated call.
- this._terminated = false;
-
- this._event_name = event_header.event;
- this._event_id = event_header.params && event_header.params.id;
-
- let eventValue = this._event_name;
-
- if (this._event_id)
- {
- eventValue += `;id=${this._event_id}`;
- }
-
- this._headers = Utils.cloneArray(extraHeaders);
- this._headers = this._headers.concat([
- `Event: ${eventValue}`,
- `Expires: ${this._expires}`,
- `Accept: ${accept}`
- ]);
-
- if (!this._headers.find((header) => header.startsWith('Contact')))
- {
- const contact = `Contact: ${this._ua._contact.toString()}`;
-
- this._headers.push(contact);
- }
-
- if (allowEvents)
- {
- this._headers.push(`Allow-Events: ${allowEvents}`);
- }
-
- // To enqueue SUBSCRIBE requests created before the reception of the initial subscribe OK response.
- this._queue = [];
-
- // Custom session empty object for high level use.
- this._data = {};
- }
-
- // Expose Subscriber constants as a property of the Subscriber instance.
- get C()
- {
- return C;
- }
-
- /**
- * Get dialog state.
- */
- get state()
- {
- return this._state;
- }
-
- /**
- * Get dialog id.
- */
- get id()
- {
- return this._dialog ? this._dialog.id : null;
- }
-
- get data()
- {
- return this._data;
- }
-
- set data(_data)
- {
- this._data = _data;
- }
-
- onRequestTimeout()
- {
- this._terminateDialog(C.SUBSCRIBE_RESPONSE_TIMEOUT);
- }
-
- onTransportError()
- {
- this._terminateDialog(C.SUBSCRIBE_TRANSPORT_ERROR);
- }
-
- /**
- * Dialog callback.
- */
- receiveRequest(request)
- {
- if (request.method !== JsSIP_C.NOTIFY)
- {
- logger.warn('received non-NOTIFY request');
- request.reply(405);
-
- return;
- }
-
- // RFC 6665 8.2.1. Check if event header matches.
- const event_header = request.parseHeader('Event');
-
- if (!event_header)
- {
- logger.warn('missing Event header');
- request.reply(400);
- this._terminateDialog(C.WRONG_NOTIFY_RECEIVED);
-
- return;
- }
-
- const event_name = event_header.event;
- const event_id = event_header.params && event_header.params.id;
-
- if (event_name !== this._event_name || event_id !== this._event_id)
- {
- logger.warn('Event header does not match the one in SUBSCRIBE request');
- request.reply(489);
- this._terminateDialog(C.WRONG_NOTIFY_RECEIVED);
-
- return;
- }
-
- // Process Subscription-State header.
- const subs_state = request.parseHeader('subscription-state');
-
- if (!subs_state)
- {
- logger.warn('missing Subscription-State header');
- request.reply(400);
- this._terminateDialog(C.WRONG_NOTIFY_RECEIVED);
-
- return;
- }
-
- const new_state = this._parseSubscriptionState(subs_state.state);
-
- if (new_state === undefined)
- {
- logger.warn(`Invalid Subscription-State header value: ${subs_state.state}`);
- request.reply(400);
- this._terminateDialog(C.WRONG_NOTIFY_RECEIVED);
-
- return;
- }
- request.reply(200);
-
- const prev_state = this._state;
-
- if (prev_state !== C.STATE_TERMINATED && new_state !== C.STATE_TERMINATED)
- {
- this._state = new_state;
-
- if (subs_state.expires !== undefined)
- {
- const expires = subs_state.expires;
- const expires_timestamp = new Date().getTime() + (expires * 1000);
- const max_time_deviation = 2000;
-
- // Expiration time is shorter and the difference is not too small.
- if (this._expires_timestamp - expires_timestamp > max_time_deviation)
- {
- logger.debug('update sending re-SUBSCRIBE time');
-
- this._scheduleSubscribe(expires);
- }
- }
- }
-
- if (prev_state !== C.STATE_PENDING && new_state === C.STATE_PENDING)
- {
- logger.debug('emit "pending"');
-
- this.emit('pending');
- }
- else if (prev_state !== C.STATE_ACTIVE && new_state === C.STATE_ACTIVE)
- {
- logger.debug('emit "active"');
-
- this.emit('active');
- }
-
- const body = request.body;
-
- // Check if the notify is final.
- const is_final = new_state === C.STATE_TERMINATED;
-
- // Notify event fired only for notify with body.
- if (body)
- {
- const content_type = request.getHeader('content-type');
-
- logger.debug('emit "notify"');
-
- this.emit('notify', is_final, request, body, content_type);
- }
-
- if (is_final)
- {
- const reason = subs_state.reason;
- let retry_after = undefined;
-
- if (subs_state.params && subs_state.params['retry-after'] !== undefined)
- {
- retry_after = parseInt(subs_state.params['retry-after']);
- }
-
- this._terminateDialog(C.FINAL_NOTIFY_RECEIVED, reason, retry_after);
- }
- }
-
- /**
- * User API
- */
-
- /**
- * Send the initial (non-fetch) and subsequent subscribe.
- * @param {string} body - subscribe request body.
- */
- subscribe(body = null)
- {
- logger.debug('subscribe()');
-
- if (this._state === C.STATE_INIT)
- {
- this._sendInitialSubscribe(body, this._headers);
- }
- else
- {
- this._sendSubsequentSubscribe(body, this._headers);
- }
- }
-
- /**
- * terminate.
- * Send un-subscribe or fetch-subscribe (with Expires: 0).
- * @param {string} body - un-subscribe request body
- */
- terminate(body = null)
- {
- logger.debug('terminate()');
-
- if (this._state === C.STATE_INIT)
- {
- throw new Exceptions.InvalidStateError(this._state);
- }
-
- // Prevent duplication un-subscribe sending.
- if (this._terminated)
- {
- return;
- }
- this._terminated = true;
-
- // Set header Expires: 0.
- const headers = this._headers.map((header) =>
- {
- return header.startsWith('Expires') ? 'Expires: 0' : header;
- });
-
- this._sendSubsequentSubscribe(body, headers);
- }
-
- /**
- * Private API.
- */
- _terminateDialog(terminationCode, reason = undefined, retryAfter = undefined)
- {
- // To prevent duplicate emit terminated event.
- if (this._state === C.STATE_TERMINATED)
- {
- return;
- }
-
- this._state = C.STATE_TERMINATED;
-
- // Clear timers.
- clearTimeout(this._expires_timer);
-
- if (this._dialog)
- {
- this._dialog.terminate();
- this._dialog = null;
- }
-
- logger.debug(`emit "terminated" code=${terminationCode}`);
-
- this.emit('terminated', terminationCode, reason, retryAfter);
- }
-
- _sendInitialSubscribe(body, headers)
- {
- if (body)
- {
- if (!this._content_type)
- {
- throw new TypeError('content_type is undefined');
- }
-
- headers = Utils.cloneArray(headers);
- headers.push(`Content-Type: ${this._content_type}`);
- }
-
- this._state = C.STATE_WAITING_NOTIFY;
-
- const request = new SIPMessage.OutgoingRequest(JsSIP_C.SUBSCRIBE,
- this._ua.normalizeTarget(this._target), this._ua, this._params, headers, body);
-
- const request_sender = new RequestSender(this._ua, request, {
- onRequestTimeout : () =>
- {
- this.onRequestTimeout();
- },
- onTransportError : () =>
- {
- this.onTransportError();
- },
- onReceiveResponse : (response) =>
- {
- this._receiveSubscribeResponse(response);
- }
- });
-
- request_sender.send();
- }
-
- _sendSubsequentSubscribe(body, headers)
- {
- if (this._state === C.STATE_TERMINATED)
- {
- return;
- }
-
- if (!this._dialog)
- {
- logger.debug('enqueue subscribe');
-
- this._queue.push({ body, headers: Utils.cloneArray(headers) });
-
- return;
- }
-
- if (body)
- {
- if (!this._content_type)
- {
- throw new TypeError('content_type is undefined');
- }
-
- headers = Utils.cloneArray(headers);
- headers.push(`Content-Type: ${this._content_type}`);
- }
-
- this._dialog.sendRequest(JsSIP_C.SUBSCRIBE, {
- body,
- extraHeaders : headers,
- eventHandlers : {
- onRequestTimeout : () =>
- {
- this.onRequestTimeout();
- },
- onTransportError : () =>
- {
- this.onTransportError();
- },
- onSuccessResponse : (response) =>
- {
- this._receiveSubscribeResponse(response);
- },
- onErrorResponse : (response) =>
- {
- this._receiveSubscribeResponse(response);
- },
- onDialogError : (response) =>
- {
- this._receiveSubscribeResponse(response);
- }
- }
- });
- }
-
- _receiveSubscribeResponse(response)
- {
- if (this._state === C.STATE_TERMINATED)
- {
- return;
- }
-
- if (response.status_code >= 200 && response.status_code < 300)
- {
- // Create dialog.
- if (this._dialog === null)
- {
- const dialog = new Dialog(this, response, 'UAC');
-
- if (dialog.error)
- {
- // OK response without Contact.
- logger.warn(dialog.error);
- this._terminateDialog(C.SUBSCRIBE_WRONG_OK_RESPONSE);
-
- return;
- }
-
- this._dialog = dialog;
-
- logger.debug('emit "accepted"');
-
- this.emit('accepted');
-
- // Subsequent subscribes saved in the queue until dialog created.
- for (const subscribe of this._queue)
- {
- logger.debug('dequeue subscribe');
-
- this._sendSubsequentSubscribe(subscribe.body, subscribe.headers);
- }
- }
-
- // Check expires value.
- const expires_value = response.getHeader('expires');
-
- let expires = parseInt(expires_value);
-
- if (!Utils.isDecimal(expires) || expires <= 0)
- {
- logger.warn(`response without Expires header, setting a default value of ${C.DEFAULT_EXPIRES_SEC}`);
-
- // RFC 6665 3.1.1 subscribe OK response must contain Expires header.
- // Use workaround expires value.
- expires = C.DEFAULT_EXPIRES_SEC;
- }
-
- if (expires > 0)
- {
- this._scheduleSubscribe(expires);
- }
- }
- else if (response.status_code === 401 || response.status_code === 407)
- {
- this._terminateDialog(C.SUBSCRIBE_AUTHENTICATION_FAILED);
- }
- else if (response.status_code >= 300)
- {
- this._terminateDialog(C.SUBSCRIBE_NON_OK_RESPONSE);
- }
- }
-
- _scheduleSubscribe(expires)
- {
- /*
+module.exports = class Subscriber extends EventEmitter {
+ /**
+ * Expose C object.
+ */
+ static get C() {
+ return C;
+ }
+
+ /**
+ * @param {UA} ua - reference to JsSIP.UA
+ * @param {string} target
+ * @param {string} eventName - Event header value. May end with optional ;id=xxx
+ * @param {string} accept - Accept header value.
+ *
+ * @param {SubscriberOption} options - optional parameters.
+ * @param {number} expires - Expires header value. Default is 900.
+ * @param {string} contentType - Content-Type header value. Used for SUBSCRIBE with body
+ * @param {string} allowEvents - Allow-Events header value.
+ * @param {RequestParams} params - Will have priority over ua.configuration.
+ * If set please define: to_uri, to_display_name, from_uri, from_display_name
+ * @param {Array<string>} extraHeaders - Additional SIP headers.
+ */
+ constructor(
+ ua,
+ target,
+ eventName,
+ accept,
+ { expires, contentType, allowEvents, params, extraHeaders }
+ ) {
+ logger.debug('new');
+
+ super();
+
+ // Check that arguments are defined.
+ if (!target) {
+ throw new TypeError('Not enough arguments: Missing target');
+ }
+
+ if (!eventName) {
+ throw new TypeError('Not enough arguments: Missing eventName');
+ }
+
+ if (!accept) {
+ throw new TypeError('Not enough arguments: Missing accept');
+ }
+
+ const event_header = Grammar.parse(eventName, 'Event');
+
+ if (event_header === -1) {
+ throw new TypeError('Missing Event header field');
+ }
+
+ this._ua = ua;
+ this._target = target;
+
+ if (!Utils.isDecimal(expires) || expires <= 0) {
+ expires = C.DEFAULT_EXPIRES_SEC;
+ }
+
+ this._expires = expires;
+
+ // Used to subscribe with body.
+ this._content_type = contentType;
+
+ // Set initial subscribe parameters.
+ this._params = Utils.cloneObject(params);
+
+ if (!this._params.from_uri) {
+ this._params.from_uri = this._ua.configuration.uri;
+ }
+
+ this._params.from_tag = Utils.newTag();
+ this._params.to_tag = null;
+ this._params.call_id = Utils.createRandomToken(20);
+
+ // Create subscribe cseq if not defined custom cseq.
+ if (this._params.cseq === undefined) {
+ this._params.cseq = Math.floor(Math.random() * 10000 + 1);
+ }
+
+ // Subscriber state.
+ this._state = C.STATE_INIT;
+
+ // Dialog.
+ this._dialog = null;
+
+ // To refresh subscription.
+ this._expires_timer = null;
+ this._expires_timestamp = null;
+
+ // To prevent duplicate terminated call.
+ this._terminated = false;
+
+ this._event_name = event_header.event;
+ this._event_id = event_header.params && event_header.params.id;
+
+ let eventValue = this._event_name;
+
+ if (this._event_id) {
+ eventValue += `;id=${this._event_id}`;
+ }
+
+ this._headers = Utils.cloneArray(extraHeaders);
+ this._headers = this._headers.concat([
+ `Event: ${eventValue}`,
+ `Expires: ${this._expires}`,
+ `Accept: ${accept}`,
+ ]);
+
+ if (!this._headers.find(header => header.startsWith('Contact'))) {
+ const contact = `Contact: ${this._ua._contact.toString()}`;
+
+ this._headers.push(contact);
+ }
+
+ if (allowEvents) {
+ this._headers.push(`Allow-Events: ${allowEvents}`);
+ }
+
+ // To enqueue SUBSCRIBE requests created before the reception of the initial subscribe OK response.
+ this._queue = [];
+
+ // Custom session empty object for high level use.
+ this._data = {};
+ }
+
+ // Expose Subscriber constants as a property of the Subscriber instance.
+ get C() {
+ return C;
+ }
+
+ /**
+ * Get dialog state.
+ */
+ get state() {
+ return this._state;
+ }
+
+ /**
+ * Get dialog id.
+ */
+ get id() {
+ return this._dialog ? this._dialog.id : null;
+ }
+
+ get data() {
+ return this._data;
+ }
+
+ set data(_data) {
+ this._data = _data;
+ }
+
+ onRequestTimeout() {
+ this._terminateDialog(C.SUBSCRIBE_RESPONSE_TIMEOUT);
+ }
+
+ onTransportError() {
+ this._terminateDialog(C.SUBSCRIBE_TRANSPORT_ERROR);
+ }
+
+ /**
+ * Dialog callback.
+ */
+ receiveRequest(request) {
+ if (request.method !== JsSIP_C.NOTIFY) {
+ logger.warn('received non-NOTIFY request');
+ request.reply(405);
+
+ return;
+ }
+
+ // RFC 6665 8.2.1. Check if event header matches.
+ const event_header = request.parseHeader('Event');
+
+ if (!event_header) {
+ logger.warn('missing Event header');
+ request.reply(400);
+ this._terminateDialog(C.WRONG_NOTIFY_RECEIVED);
+
+ return;
+ }
+
+ const event_name = event_header.event;
+ const event_id = event_header.params && event_header.params.id;
+
+ if (event_name !== this._event_name || event_id !== this._event_id) {
+ logger.warn('Event header does not match the one in SUBSCRIBE request');
+ request.reply(489);
+ this._terminateDialog(C.WRONG_NOTIFY_RECEIVED);
+
+ return;
+ }
+
+ // Process Subscription-State header.
+ const subs_state = request.parseHeader('subscription-state');
+
+ if (!subs_state) {
+ logger.warn('missing Subscription-State header');
+ request.reply(400);
+ this._terminateDialog(C.WRONG_NOTIFY_RECEIVED);
+
+ return;
+ }
+
+ const new_state = this._parseSubscriptionState(subs_state.state);
+
+ if (new_state === undefined) {
+ logger.warn(
+ `Invalid Subscription-State header value: ${subs_state.state}`
+ );
+ request.reply(400);
+ this._terminateDialog(C.WRONG_NOTIFY_RECEIVED);
+
+ return;
+ }
+ request.reply(200);
+
+ const prev_state = this._state;
+
+ if (prev_state !== C.STATE_TERMINATED && new_state !== C.STATE_TERMINATED) {
+ this._state = new_state;
+
+ if (subs_state.expires !== undefined) {
+ const expires = subs_state.expires;
+ const expires_timestamp = new Date().getTime() + expires * 1000;
+ const max_time_deviation = 2000;
+
+ // Expiration time is shorter and the difference is not too small.
+ if (this._expires_timestamp - expires_timestamp > max_time_deviation) {
+ logger.debug('update sending re-SUBSCRIBE time');
+
+ this._scheduleSubscribe(expires);
+ }
+ }
+ }
+
+ if (prev_state !== C.STATE_PENDING && new_state === C.STATE_PENDING) {
+ logger.debug('emit "pending"');
+
+ this.emit('pending');
+ } else if (prev_state !== C.STATE_ACTIVE && new_state === C.STATE_ACTIVE) {
+ logger.debug('emit "active"');
+
+ this.emit('active');
+ }
+
+ const body = request.body;
+
+ // Check if the notify is final.
+ const is_final = new_state === C.STATE_TERMINATED;
+
+ // Notify event fired only for notify with body.
+ if (body) {
+ const content_type = request.getHeader('content-type');
+
+ logger.debug('emit "notify"');
+
+ this.emit('notify', is_final, request, body, content_type);
+ }
+
+ if (is_final) {
+ const reason = subs_state.reason;
+ let retry_after = undefined;
+
+ if (subs_state.params && subs_state.params['retry-after'] !== undefined) {
+ retry_after = parseInt(subs_state.params['retry-after']);
+ }
+
+ this._terminateDialog(C.FINAL_NOTIFY_RECEIVED, reason, retry_after);
+ }
+ }
+
+ /**
+ * User API
+ */
+
+ /**
+ * Send the initial (non-fetch) and subsequent subscribe.
+ * @param {string} body - subscribe request body.
+ */
+ subscribe(body = null) {
+ logger.debug('subscribe()');
+
+ if (this._state === C.STATE_INIT) {
+ this._sendInitialSubscribe(body, this._headers);
+ } else {
+ this._sendSubsequentSubscribe(body, this._headers);
+ }
+ }
+
+ /**
+ * terminate.
+ * Send un-subscribe or fetch-subscribe (with Expires: 0).
+ * @param {string} body - un-subscribe request body
+ */
+ terminate(body = null) {
+ logger.debug('terminate()');
+
+ if (this._state === C.STATE_INIT) {
+ throw new Exceptions.InvalidStateError(this._state);
+ }
+
+ // Prevent duplication un-subscribe sending.
+ if (this._terminated) {
+ return;
+ }
+ this._terminated = true;
+
+ // Set header Expires: 0.
+ const headers = this._headers.map(header => {
+ return header.startsWith('Expires') ? 'Expires: 0' : header;
+ });
+
+ this._sendSubsequentSubscribe(body, headers);
+ }
+
+ /**
+ * Private API.
+ */
+ _terminateDialog(
+ terminationCode,
+ reason = undefined,
+ retryAfter = undefined
+ ) {
+ // To prevent duplicate emit terminated event.
+ if (this._state === C.STATE_TERMINATED) {
+ return;
+ }
+
+ this._state = C.STATE_TERMINATED;
+
+ // Clear timers.
+ clearTimeout(this._expires_timer);
+
+ if (this._dialog) {
+ this._dialog.terminate();
+ this._dialog = null;
+ }
+
+ logger.debug(`emit "terminated" code=${terminationCode}`);
+
+ this.emit('terminated', terminationCode, reason, retryAfter);
+ }
+
+ _sendInitialSubscribe(body, headers) {
+ if (body) {
+ if (!this._content_type) {
+ throw new TypeError('content_type is undefined');
+ }
+
+ headers = Utils.cloneArray(headers);
+ headers.push(`Content-Type: ${this._content_type}`);
+ }
+
+ this._state = C.STATE_WAITING_NOTIFY;
+
+ const request = new SIPMessage.OutgoingRequest(
+ JsSIP_C.SUBSCRIBE,
+ this._ua.normalizeTarget(this._target),
+ this._ua,
+ this._params,
+ headers,
+ body
+ );
+
+ const request_sender = new RequestSender(this._ua, request, {
+ onRequestTimeout: () => {
+ this.onRequestTimeout();
+ },
+ onTransportError: () => {
+ this.onTransportError();
+ },
+ onReceiveResponse: response => {
+ this._receiveSubscribeResponse(response);
+ },
+ });
+
+ request_sender.send();
+ }
+
+ _sendSubsequentSubscribe(body, headers) {
+ if (this._state === C.STATE_TERMINATED) {
+ return;
+ }
+
+ if (!this._dialog) {
+ logger.debug('enqueue subscribe');
+
+ this._queue.push({ body, headers: Utils.cloneArray(headers) });
+
+ return;
+ }
+
+ if (body) {
+ if (!this._content_type) {
+ throw new TypeError('content_type is undefined');
+ }
+
+ headers = Utils.cloneArray(headers);
+ headers.push(`Content-Type: ${this._content_type}`);
+ }
+
+ this._dialog.sendRequest(JsSIP_C.SUBSCRIBE, {
+ body,
+ extraHeaders: headers,
+ eventHandlers: {
+ onRequestTimeout: () => {
+ this.onRequestTimeout();
+ },
+ onTransportError: () => {
+ this.onTransportError();
+ },
+ onSuccessResponse: response => {
+ this._receiveSubscribeResponse(response);
+ },
+ onErrorResponse: response => {
+ this._receiveSubscribeResponse(response);
+ },
+ onDialogError: response => {
+ this._receiveSubscribeResponse(response);
+ },
+ },
+ });
+ }
+
+ _receiveSubscribeResponse(response) {
+ if (this._state === C.STATE_TERMINATED) {
+ return;
+ }
+
+ if (response.status_code >= 200 && response.status_code < 300) {
+ // Create dialog.
+ if (this._dialog === null) {
+ const dialog = new Dialog(this, response, 'UAC');
+
+ if (dialog.error) {
+ // OK response without Contact.
+ logger.warn(dialog.error);
+ this._terminateDialog(C.SUBSCRIBE_WRONG_OK_RESPONSE);
+
+ return;
+ }
+
+ this._dialog = dialog;
+
+ logger.debug('emit "accepted"');
+
+ this.emit('accepted');
+
+ // Subsequent subscribes saved in the queue until dialog created.
+ for (const subscribe of this._queue) {
+ logger.debug('dequeue subscribe');
+
+ this._sendSubsequentSubscribe(subscribe.body, subscribe.headers);
+ }
+ }
+
+ // Check expires value.
+ const expires_value = response.getHeader('expires');
+
+ let expires = parseInt(expires_value);
+
+ if (!Utils.isDecimal(expires) || expires <= 0) {
+ logger.warn(
+ `response without Expires header, setting a default value of ${C.DEFAULT_EXPIRES_SEC}`
+ );
+
+ // RFC 6665 3.1.1 subscribe OK response must contain Expires header.
+ // Use workaround expires value.
+ expires = C.DEFAULT_EXPIRES_SEC;
+ }
+
+ if (expires > 0) {
+ this._scheduleSubscribe(expires);
+ }
+ } else if (response.status_code === 401 || response.status_code === 407) {
+ this._terminateDialog(C.SUBSCRIBE_AUTHENTICATION_FAILED);
+ } else if (response.status_code >= 300) {
+ this._terminateDialog(C.SUBSCRIBE_NON_OK_RESPONSE);
+ }
+ }
+
+ _scheduleSubscribe(expires) {
+ /*
If the expires time is less than 140 seconds we do not support Chrome intensive timer throttling mode.
In this case, the re-subcribe is sent 5 seconds before the subscription expiration.
@@ -592,31 +536,45 @@ module.exports = class Subscriber extends EventEmitter
expires is 600, re-subscribe will be ordered to send in 300 + (0 .. 230) seconds.
*/
- const timeout = expires >= 140 ? (expires * 1000 / 2)
- + Math.floor(((expires / 2) - 70) * 1000 * Math.random()) : (expires * 1000) - 5000;
-
- this._expires_timestamp = new Date().getTime() + (expires * 1000);
-
- logger.debug(`next SUBSCRIBE will be sent in ${Math.floor(timeout / 1000)} sec`);
-
- clearTimeout(this._expires_timer);
- this._expires_timer = setTimeout(() =>
- {
- this._expires_timer = null;
- this._sendSubsequentSubscribe(null, this._headers);
- }, timeout);
- }
-
- _parseSubscriptionState(strState)
- {
- switch (strState)
- {
- case 'pending': return C.STATE_PENDING;
- case 'active': return C.STATE_ACTIVE;
- case 'terminated': return C.STATE_TERMINATED;
- case 'init': return C.STATE_INIT;
- case 'notify_wait': return C.STATE_WAITING_NOTIFY;
- default: return undefined;
- }
- }
+ const timeout =
+ expires >= 140
+ ? (expires * 1000) / 2 +
+ Math.floor((expires / 2 - 70) * 1000 * Math.random())
+ : expires * 1000 - 5000;
+
+ this._expires_timestamp = new Date().getTime() + expires * 1000;
+
+ logger.debug(
+ `next SUBSCRIBE will be sent in ${Math.floor(timeout / 1000)} sec`
+ );
+
+ clearTimeout(this._expires_timer);
+ this._expires_timer = setTimeout(() => {
+ this._expires_timer = null;
+ this._sendSubsequentSubscribe(null, this._headers);
+ }, timeout);
+ }
+
+ _parseSubscriptionState(strState) {
+ switch (strState) {
+ case 'pending': {
+ return C.STATE_PENDING;
+ }
+ case 'active': {
+ return C.STATE_ACTIVE;
+ }
+ case 'terminated': {
+ return C.STATE_TERMINATED;
+ }
+ case 'init': {
+ return C.STATE_INIT;
+ }
+ case 'notify_wait': {
+ return C.STATE_WAITING_NOTIFY;
+ }
+ default: {
+ return undefined;
+ }
+ }
+ }
};
diff --git a/src/Timers.js b/src/Timers.js
index 48fb254..2af8bfa 100644
--- a/src/Timers.js
+++ b/src/Timers.js
@@ -1,17 +1,19 @@
-const T1 = 500, T2 = 4000, T4 = 5000;
+const T1 = 500,
+ T2 = 4000,
+ T4 = 5000;
module.exports = {
- T1,
- T2,
- T4,
- TIMER_B : 64 * T1,
- TIMER_D : 0 * T1,
- TIMER_F : 64 * T1,
- TIMER_H : 64 * T1,
- TIMER_I : 0 * T1,
- TIMER_J : 0 * T1,
- TIMER_K : 0 * T4,
- TIMER_L : 64 * T1,
- TIMER_M : 64 * T1,
- PROVISIONAL_RESPONSE_INTERVAL : 60000 // See RFC 3261 Section 13.3.1.1
+ T1,
+ T2,
+ T4,
+ TIMER_B: 64 * T1,
+ TIMER_D: 0 * T1,
+ TIMER_F: 64 * T1,
+ TIMER_H: 64 * T1,
+ TIMER_I: 0 * T1,
+ TIMER_J: 0 * T1,
+ TIMER_K: 0 * T4,
+ TIMER_L: 64 * T1,
+ TIMER_M: 64 * T1,
+ PROVISIONAL_RESPONSE_INTERVAL: 60000, // See RFC 3261 Section 13.3.1.1
};
diff --git a/src/Transactions.js b/src/Transactions.js
index 1f9ded3..4978bc9 100644
--- a/src/Transactions.js
+++ b/src/Transactions.js
@@ -11,678 +11,605 @@ const loggernist = new Logger('NonInviteServerTransaction');
const loggerist = new Logger('InviteServerTransaction');
const C = {
- // Transaction states.
- STATUS_TRYING : 1,
- STATUS_PROCEEDING : 2,
- STATUS_CALLING : 3,
- STATUS_ACCEPTED : 4,
- STATUS_COMPLETED : 5,
- STATUS_TERMINATED : 6,
- STATUS_CONFIRMED : 7,
-
- // Transaction types.
- NON_INVITE_CLIENT : 'nict',
- NON_INVITE_SERVER : 'nist',
- INVITE_CLIENT : 'ict',
- INVITE_SERVER : 'ist'
+ // Transaction states.
+ STATUS_TRYING: 1,
+ STATUS_PROCEEDING: 2,
+ STATUS_CALLING: 3,
+ STATUS_ACCEPTED: 4,
+ STATUS_COMPLETED: 5,
+ STATUS_TERMINATED: 6,
+ STATUS_CONFIRMED: 7,
+
+ // Transaction types.
+ NON_INVITE_CLIENT: 'nict',
+ NON_INVITE_SERVER: 'nist',
+ INVITE_CLIENT: 'ict',
+ INVITE_SERVER: 'ist',
};
-class NonInviteClientTransaction extends EventEmitter
-{
- constructor(ua, transport, request, eventHandlers)
- {
- super();
-
- this.type = C.NON_INVITE_CLIENT;
- this.id = `z9hG4bK${Math.floor(Math.random() * 10000000)}`;
- this.ua = ua;
- this.transport = transport;
- this.request = request;
- this.eventHandlers = eventHandlers;
-
- let via = `SIP/2.0/${transport.via_transport}`;
-
- via += ` ${ua.configuration.via_host};branch=${this.id}`;
-
- this.request.setHeader('via', via);
-
- this.ua.newTransaction(this);
- }
-
- get C()
- {
- return C;
- }
-
- stateChanged(state)
- {
- this.state = state;
- this.emit('stateChanged');
- }
-
- send()
- {
- this.stateChanged(C.STATUS_TRYING);
- this.F = setTimeout(() => { this.timer_F(); }, Timers.TIMER_F);
-
- if (!this.transport.send(this.request))
- {
- this.onTransportError();
- }
- }
-
- onTransportError()
- {
- loggernict.debug(`transport error occurred, deleting transaction ${this.id}`);
- clearTimeout(this.F);
- clearTimeout(this.K);
- this.stateChanged(C.STATUS_TERMINATED);
- this.ua.destroyTransaction(this);
- this.eventHandlers.onTransportError();
- }
-
- timer_F()
- {
- loggernict.debug(`Timer F expired for transaction ${this.id}`);
- this.stateChanged(C.STATUS_TERMINATED);
- this.ua.destroyTransaction(this);
- this.eventHandlers.onRequestTimeout();
- }
-
- timer_K()
- {
- this.stateChanged(C.STATUS_TERMINATED);
- this.ua.destroyTransaction(this);
- }
-
- receiveResponse(response)
- {
- const status_code = response.status_code;
-
- if (status_code < 200)
- {
- switch (this.state)
- {
- case C.STATUS_TRYING:
- case C.STATUS_PROCEEDING:
- this.stateChanged(C.STATUS_PROCEEDING);
- this.eventHandlers.onReceiveResponse(response);
- break;
- }
- }
- else
- {
- switch (this.state)
- {
- case C.STATUS_TRYING:
- case C.STATUS_PROCEEDING:
- this.stateChanged(C.STATUS_COMPLETED);
- clearTimeout(this.F);
-
- if (status_code === 408)
- {
- this.eventHandlers.onRequestTimeout();
- }
- else
- {
- this.eventHandlers.onReceiveResponse(response);
- }
-
- this.K = setTimeout(() => { this.timer_K(); }, Timers.TIMER_K);
- break;
- case C.STATUS_COMPLETED:
- break;
- }
- }
- }
+class NonInviteClientTransaction extends EventEmitter {
+ constructor(ua, transport, request, eventHandlers) {
+ super();
+
+ this.type = C.NON_INVITE_CLIENT;
+ this.id = `z9hG4bK${Math.floor(Math.random() * 10000000)}`;
+ this.ua = ua;
+ this.transport = transport;
+ this.request = request;
+ this.eventHandlers = eventHandlers;
+
+ let via = `SIP/2.0/${transport.via_transport}`;
+
+ via += ` ${ua.configuration.via_host};branch=${this.id}`;
+
+ this.request.setHeader('via', via);
+
+ this.ua.newTransaction(this);
+ }
+
+ get C() {
+ return C;
+ }
+
+ stateChanged(state) {
+ this.state = state;
+ this.emit('stateChanged');
+ }
+
+ send() {
+ this.stateChanged(C.STATUS_TRYING);
+ this.F = setTimeout(() => {
+ this.timer_F();
+ }, Timers.TIMER_F);
+
+ if (!this.transport.send(this.request)) {
+ this.onTransportError();
+ }
+ }
+
+ onTransportError() {
+ loggernict.debug(
+ `transport error occurred, deleting transaction ${this.id}`
+ );
+ clearTimeout(this.F);
+ clearTimeout(this.K);
+ this.stateChanged(C.STATUS_TERMINATED);
+ this.ua.destroyTransaction(this);
+ this.eventHandlers.onTransportError();
+ }
+
+ timer_F() {
+ loggernict.debug(`Timer F expired for transaction ${this.id}`);
+ this.stateChanged(C.STATUS_TERMINATED);
+ this.ua.destroyTransaction(this);
+ this.eventHandlers.onRequestTimeout();
+ }
+
+ timer_K() {
+ this.stateChanged(C.STATUS_TERMINATED);
+ this.ua.destroyTransaction(this);
+ }
+
+ receiveResponse(response) {
+ const status_code = response.status_code;
+
+ if (status_code < 200) {
+ switch (this.state) {
+ case C.STATUS_TRYING:
+ case C.STATUS_PROCEEDING: {
+ this.stateChanged(C.STATUS_PROCEEDING);
+ this.eventHandlers.onReceiveResponse(response);
+ break;
+ }
+ }
+ } else {
+ switch (this.state) {
+ case C.STATUS_TRYING:
+ case C.STATUS_PROCEEDING: {
+ this.stateChanged(C.STATUS_COMPLETED);
+ clearTimeout(this.F);
+
+ if (status_code === 408) {
+ this.eventHandlers.onRequestTimeout();
+ } else {
+ this.eventHandlers.onReceiveResponse(response);
+ }
+
+ this.K = setTimeout(() => {
+ this.timer_K();
+ }, Timers.TIMER_K);
+ break;
+ }
+ case C.STATUS_COMPLETED: {
+ break;
+ }
+ }
+ }
+ }
}
-class InviteClientTransaction extends EventEmitter
-{
- constructor(ua, transport, request, eventHandlers)
- {
- super();
-
- this.type = C.INVITE_CLIENT;
- this.id = `z9hG4bK${Math.floor(Math.random() * 10000000)}`;
- this.ua = ua;
- this.transport = transport;
- this.request = request;
- this.eventHandlers = eventHandlers;
- request.transaction = this;
-
- let via = `SIP/2.0/${transport.via_transport}`;
-
- via += ` ${ua.configuration.via_host};branch=${this.id}`;
-
- this.request.setHeader('via', via);
-
- this.ua.newTransaction(this);
- }
-
- get C()
- {
- return C;
- }
-
- stateChanged(state)
- {
- this.state = state;
- this.emit('stateChanged');
- }
-
- send()
- {
- this.stateChanged(C.STATUS_CALLING);
- this.B = setTimeout(() =>
- {
- this.timer_B();
- }, Timers.TIMER_B);
-
- if (!this.transport.send(this.request))
- {
- this.onTransportError();
- }
- }
-
- onTransportError()
- {
- clearTimeout(this.B);
- clearTimeout(this.D);
- clearTimeout(this.M);
-
- if (this.state !== C.STATUS_ACCEPTED)
- {
- loggerict.debug(`transport error occurred, deleting transaction ${this.id}`);
- this.eventHandlers.onTransportError();
- }
-
- this.stateChanged(C.STATUS_TERMINATED);
- this.ua.destroyTransaction(this);
- }
-
- // RFC 6026 7.2.
- timer_M()
- {
- loggerict.debug(`Timer M expired for transaction ${this.id}`);
-
- if (this.state === C.STATUS_ACCEPTED)
- {
- clearTimeout(this.B);
- this.stateChanged(C.STATUS_TERMINATED);
- this.ua.destroyTransaction(this);
- }
- }
-
- // RFC 3261 17.1.1.
- timer_B()
- {
- loggerict.debug(`Timer B expired for transaction ${this.id}`);
- if (this.state === C.STATUS_CALLING)
- {
- this.stateChanged(C.STATUS_TERMINATED);
- this.ua.destroyTransaction(this);
- this.eventHandlers.onRequestTimeout();
- }
- }
-
- timer_D()
- {
- loggerict.debug(`Timer D expired for transaction ${this.id}`);
- clearTimeout(this.B);
- this.stateChanged(C.STATUS_TERMINATED);
- this.ua.destroyTransaction(this);
- }
-
- sendACK(response)
- {
- const ack = new SIPMessage.OutgoingRequest(JsSIP_C.ACK, this.request.ruri,
- this.ua, {
- 'route_set' : this.request.getHeaders('route'),
- 'call_id' : this.request.getHeader('call-id'),
- 'cseq' : this.request.cseq
- });
-
- ack.setHeader('from', this.request.getHeader('from'));
- ack.setHeader('via', this.request.getHeader('via'));
- ack.setHeader('to', response.getHeader('to'));
-
- this.D = setTimeout(() => { this.timer_D(); }, Timers.TIMER_D);
-
- this.transport.send(ack);
- }
-
- cancel(reason)
- {
- // Send only if a provisional response (>100) has been received.
- if (this.state !== C.STATUS_PROCEEDING)
- {
- return;
- }
-
- const cancel = new SIPMessage.OutgoingRequest(JsSIP_C.CANCEL, this.request.ruri,
- this.ua, {
- 'route_set' : this.request.getHeaders('route'),
- 'call_id' : this.request.getHeader('call-id'),
- 'cseq' : this.request.cseq
- });
-
- cancel.setHeader('from', this.request.getHeader('from'));
- cancel.setHeader('via', this.request.getHeader('via'));
- cancel.setHeader('to', this.request.getHeader('to'));
-
- if (reason)
- {
- cancel.setHeader('reason', reason);
- }
-
- this.transport.send(cancel);
- }
-
- receiveResponse(response)
- {
- const status_code = response.status_code;
-
- if (status_code >= 100 && status_code <= 199)
- {
- switch (this.state)
- {
- case C.STATUS_CALLING:
- this.stateChanged(C.STATUS_PROCEEDING);
- this.eventHandlers.onReceiveResponse(response);
- break;
- case C.STATUS_PROCEEDING:
- this.eventHandlers.onReceiveResponse(response);
- break;
- }
- }
- else if (status_code >= 200 && status_code <= 299)
- {
- switch (this.state)
- {
- case C.STATUS_CALLING:
- case C.STATUS_PROCEEDING:
- this.stateChanged(C.STATUS_ACCEPTED);
- this.M = setTimeout(() =>
- {
- this.timer_M();
- }, Timers.TIMER_M);
- this.eventHandlers.onReceiveResponse(response);
- break;
- case C.STATUS_ACCEPTED:
- this.eventHandlers.onReceiveResponse(response);
- break;
- }
- }
- else if (status_code >= 300 && status_code <= 699)
- {
- switch (this.state)
- {
- case C.STATUS_CALLING:
- case C.STATUS_PROCEEDING:
- this.stateChanged(C.STATUS_COMPLETED);
- this.sendACK(response);
- this.eventHandlers.onReceiveResponse(response);
- break;
- case C.STATUS_COMPLETED:
- this.sendACK(response);
- break;
- }
- }
- }
+class InviteClientTransaction extends EventEmitter {
+ constructor(ua, transport, request, eventHandlers) {
+ super();
+
+ this.type = C.INVITE_CLIENT;
+ this.id = `z9hG4bK${Math.floor(Math.random() * 10000000)}`;
+ this.ua = ua;
+ this.transport = transport;
+ this.request = request;
+ this.eventHandlers = eventHandlers;
+ request.transaction = this;
+
+ let via = `SIP/2.0/${transport.via_transport}`;
+
+ via += ` ${ua.configuration.via_host};branch=${this.id}`;
+
+ this.request.setHeader('via', via);
+
+ this.ua.newTransaction(this);
+ }
+
+ get C() {
+ return C;
+ }
+
+ stateChanged(state) {
+ this.state = state;
+ this.emit('stateChanged');
+ }
+
+ send() {
+ this.stateChanged(C.STATUS_CALLING);
+ this.B = setTimeout(() => {
+ this.timer_B();
+ }, Timers.TIMER_B);
+
+ if (!this.transport.send(this.request)) {
+ this.onTransportError();
+ }
+ }
+
+ onTransportError() {
+ clearTimeout(this.B);
+ clearTimeout(this.D);
+ clearTimeout(this.M);
+
+ if (this.state !== C.STATUS_ACCEPTED) {
+ loggerict.debug(
+ `transport error occurred, deleting transaction ${this.id}`
+ );
+ this.eventHandlers.onTransportError();
+ }
+
+ this.stateChanged(C.STATUS_TERMINATED);
+ this.ua.destroyTransaction(this);
+ }
+
+ // RFC 6026 7.2.
+ timer_M() {
+ loggerict.debug(`Timer M expired for transaction ${this.id}`);
+
+ if (this.state === C.STATUS_ACCEPTED) {
+ clearTimeout(this.B);
+ this.stateChanged(C.STATUS_TERMINATED);
+ this.ua.destroyTransaction(this);
+ }
+ }
+
+ // RFC 3261 17.1.1.
+ timer_B() {
+ loggerict.debug(`Timer B expired for transaction ${this.id}`);
+ if (this.state === C.STATUS_CALLING) {
+ this.stateChanged(C.STATUS_TERMINATED);
+ this.ua.destroyTransaction(this);
+ this.eventHandlers.onRequestTimeout();
+ }
+ }
+
+ timer_D() {
+ loggerict.debug(`Timer D expired for transaction ${this.id}`);
+ clearTimeout(this.B);
+ this.stateChanged(C.STATUS_TERMINATED);
+ this.ua.destroyTransaction(this);
+ }
+
+ sendACK(response) {
+ const ack = new SIPMessage.OutgoingRequest(
+ JsSIP_C.ACK,
+ this.request.ruri,
+ this.ua,
+ {
+ route_set: this.request.getHeaders('route'),
+ call_id: this.request.getHeader('call-id'),
+ cseq: this.request.cseq,
+ }
+ );
+
+ ack.setHeader('from', this.request.getHeader('from'));
+ ack.setHeader('via', this.request.getHeader('via'));
+ ack.setHeader('to', response.getHeader('to'));
+
+ this.D = setTimeout(() => {
+ this.timer_D();
+ }, Timers.TIMER_D);
+
+ this.transport.send(ack);
+ }
+
+ cancel(reason) {
+ // Send only if a provisional response (>100) has been received.
+ if (this.state !== C.STATUS_PROCEEDING) {
+ return;
+ }
+
+ const cancel = new SIPMessage.OutgoingRequest(
+ JsSIP_C.CANCEL,
+ this.request.ruri,
+ this.ua,
+ {
+ route_set: this.request.getHeaders('route'),
+ call_id: this.request.getHeader('call-id'),
+ cseq: this.request.cseq,
+ }
+ );
+
+ cancel.setHeader('from', this.request.getHeader('from'));
+ cancel.setHeader('via', this.request.getHeader('via'));
+ cancel.setHeader('to', this.request.getHeader('to'));
+
+ if (reason) {
+ cancel.setHeader('reason', reason);
+ }
+
+ this.transport.send(cancel);
+ }
+
+ receiveResponse(response) {
+ const status_code = response.status_code;
+
+ if (status_code >= 100 && status_code <= 199) {
+ switch (this.state) {
+ case C.STATUS_CALLING: {
+ this.stateChanged(C.STATUS_PROCEEDING);
+ this.eventHandlers.onReceiveResponse(response);
+ break;
+ }
+ case C.STATUS_PROCEEDING: {
+ this.eventHandlers.onReceiveResponse(response);
+ break;
+ }
+ }
+ } else if (status_code >= 200 && status_code <= 299) {
+ switch (this.state) {
+ case C.STATUS_CALLING:
+ case C.STATUS_PROCEEDING: {
+ this.stateChanged(C.STATUS_ACCEPTED);
+ this.M = setTimeout(() => {
+ this.timer_M();
+ }, Timers.TIMER_M);
+ this.eventHandlers.onReceiveResponse(response);
+ break;
+ }
+ case C.STATUS_ACCEPTED: {
+ this.eventHandlers.onReceiveResponse(response);
+ break;
+ }
+ }
+ } else if (status_code >= 300 && status_code <= 699) {
+ switch (this.state) {
+ case C.STATUS_CALLING:
+ case C.STATUS_PROCEEDING: {
+ this.stateChanged(C.STATUS_COMPLETED);
+ this.sendACK(response);
+ this.eventHandlers.onReceiveResponse(response);
+ break;
+ }
+ case C.STATUS_COMPLETED: {
+ this.sendACK(response);
+ break;
+ }
+ }
+ }
+ }
}
-class AckClientTransaction extends EventEmitter
-{
- constructor(ua, transport, request, eventHandlers)
- {
- super();
-
- this.id = `z9hG4bK${Math.floor(Math.random() * 10000000)}`;
- this.transport = transport;
- this.request = request;
- this.eventHandlers = eventHandlers;
-
- let via = `SIP/2.0/${transport.via_transport}`;
-
- via += ` ${ua.configuration.via_host};branch=${this.id}`;
-
- this.request.setHeader('via', via);
- }
-
- get C()
- {
- return C;
- }
-
- send()
- {
- if (!this.transport.send(this.request))
- {
- this.onTransportError();
- }
- }
-
- onTransportError()
- {
- loggeract.debug(`transport error occurred for transaction ${this.id}`);
- this.eventHandlers.onTransportError();
- }
+class AckClientTransaction extends EventEmitter {
+ constructor(ua, transport, request, eventHandlers) {
+ super();
+
+ this.id = `z9hG4bK${Math.floor(Math.random() * 10000000)}`;
+ this.transport = transport;
+ this.request = request;
+ this.eventHandlers = eventHandlers;
+
+ let via = `SIP/2.0/${transport.via_transport}`;
+
+ via += ` ${ua.configuration.via_host};branch=${this.id}`;
+
+ this.request.setHeader('via', via);
+ }
+
+ get C() {
+ return C;
+ }
+
+ send() {
+ if (!this.transport.send(this.request)) {
+ this.onTransportError();
+ }
+ }
+
+ onTransportError() {
+ loggeract.debug(`transport error occurred for transaction ${this.id}`);
+ this.eventHandlers.onTransportError();
+ }
}
-class NonInviteServerTransaction extends EventEmitter
-{
- constructor(ua, transport, request)
- {
- super();
-
- this.type = C.NON_INVITE_SERVER;
- this.id = request.via_branch;
- this.ua = ua;
- this.transport = transport;
- this.request = request;
- this.last_response = '';
- request.server_transaction = this;
-
- this.state = C.STATUS_TRYING;
-
- ua.newTransaction(this);
- }
-
- get C()
- {
- return C;
- }
-
- stateChanged(state)
- {
- this.state = state;
- this.emit('stateChanged');
- }
-
- timer_J()
- {
- loggernist.debug(`Timer J expired for transaction ${this.id}`);
- this.stateChanged(C.STATUS_TERMINATED);
- this.ua.destroyTransaction(this);
- }
-
- onTransportError()
- {
- if (!this.transportError)
- {
- this.transportError = true;
-
- loggernist.debug(`transport error occurred, deleting transaction ${this.id}`);
-
- clearTimeout(this.J);
- this.stateChanged(C.STATUS_TERMINATED);
- this.ua.destroyTransaction(this);
- }
- }
-
- receiveResponse(status_code, response, onSuccess, onFailure)
- {
- if (status_code === 100)
- {
- /* RFC 4320 4.1
- * 'A SIP element MUST NOT
- * send any provisional response with a
- * Status-Code other than 100 to a non-INVITE request.'
- */
- switch (this.state)
- {
- case C.STATUS_TRYING:
- this.stateChanged(C.STATUS_PROCEEDING);
- if (!this.transport.send(response))
- {
- this.onTransportError();
- }
- break;
- case C.STATUS_PROCEEDING:
- this.last_response = response;
- if (!this.transport.send(response))
- {
- this.onTransportError();
- if (onFailure)
- {
- onFailure();
- }
- }
- else if (onSuccess)
- {
- onSuccess();
- }
- break;
- }
- }
- else if (status_code >= 200 && status_code <= 699)
- {
- switch (this.state)
- {
- case C.STATUS_TRYING:
- case C.STATUS_PROCEEDING:
- this.stateChanged(C.STATUS_COMPLETED);
- this.last_response = response;
- this.J = setTimeout(() =>
- {
- this.timer_J();
- }, Timers.TIMER_J);
- if (!this.transport.send(response))
- {
- this.onTransportError();
- if (onFailure)
- {
- onFailure();
- }
- }
- else if (onSuccess)
- {
- onSuccess();
- }
- break;
- case C.STATUS_COMPLETED:
- break;
- }
- }
- }
+class NonInviteServerTransaction extends EventEmitter {
+ constructor(ua, transport, request) {
+ super();
+
+ this.type = C.NON_INVITE_SERVER;
+ this.id = request.via_branch;
+ this.ua = ua;
+ this.transport = transport;
+ this.request = request;
+ this.last_response = '';
+ request.server_transaction = this;
+
+ this.state = C.STATUS_TRYING;
+
+ ua.newTransaction(this);
+ }
+
+ get C() {
+ return C;
+ }
+
+ stateChanged(state) {
+ this.state = state;
+ this.emit('stateChanged');
+ }
+
+ timer_J() {
+ loggernist.debug(`Timer J expired for transaction ${this.id}`);
+ this.stateChanged(C.STATUS_TERMINATED);
+ this.ua.destroyTransaction(this);
+ }
+
+ onTransportError() {
+ if (!this.transportError) {
+ this.transportError = true;
+
+ loggernist.debug(
+ `transport error occurred, deleting transaction ${this.id}`
+ );
+
+ clearTimeout(this.J);
+ this.stateChanged(C.STATUS_TERMINATED);
+ this.ua.destroyTransaction(this);
+ }
+ }
+
+ receiveResponse(status_code, response, onSuccess, onFailure) {
+ if (status_code === 100) {
+ /* RFC 4320 4.1
+ * 'A SIP element MUST NOT
+ * send any provisional response with a
+ * Status-Code other than 100 to a non-INVITE request.'
+ */
+ switch (this.state) {
+ case C.STATUS_TRYING: {
+ this.stateChanged(C.STATUS_PROCEEDING);
+ if (!this.transport.send(response)) {
+ this.onTransportError();
+ }
+ break;
+ }
+ case C.STATUS_PROCEEDING: {
+ this.last_response = response;
+ if (!this.transport.send(response)) {
+ this.onTransportError();
+ if (onFailure) {
+ onFailure();
+ }
+ } else if (onSuccess) {
+ onSuccess();
+ }
+ break;
+ }
+ }
+ } else if (status_code >= 200 && status_code <= 699) {
+ switch (this.state) {
+ case C.STATUS_TRYING:
+ case C.STATUS_PROCEEDING: {
+ this.stateChanged(C.STATUS_COMPLETED);
+ this.last_response = response;
+ this.J = setTimeout(() => {
+ this.timer_J();
+ }, Timers.TIMER_J);
+ if (!this.transport.send(response)) {
+ this.onTransportError();
+ if (onFailure) {
+ onFailure();
+ }
+ } else if (onSuccess) {
+ onSuccess();
+ }
+ break;
+ }
+ case C.STATUS_COMPLETED: {
+ break;
+ }
+ }
+ }
+ }
}
-class InviteServerTransaction extends EventEmitter
-{
- constructor(ua, transport, request)
- {
- super();
-
- this.type = C.INVITE_SERVER;
- this.id = request.via_branch;
- this.ua = ua;
- this.transport = transport;
- this.request = request;
- this.last_response = '';
- request.server_transaction = this;
-
- this.state = C.STATUS_PROCEEDING;
-
- ua.newTransaction(this);
-
- this.resendProvisionalTimer = null;
-
- request.reply(100);
- }
-
- get C()
- {
- return C;
- }
-
- stateChanged(state)
- {
- this.state = state;
- this.emit('stateChanged');
- }
-
- timer_H()
- {
- loggerist.debug(`Timer H expired for transaction ${this.id}`);
-
- if (this.state === C.STATUS_COMPLETED)
- {
- loggerist.debug('ACK not received, dialog will be terminated');
- }
-
- this.stateChanged(C.STATUS_TERMINATED);
- this.ua.destroyTransaction(this);
- }
-
- timer_I()
- {
- this.stateChanged(C.STATUS_TERMINATED);
- this.ua.destroyTransaction(this);
- }
-
- // RFC 6026 7.1.
- timer_L()
- {
- loggerist.debug(`Timer L expired for transaction ${this.id}`);
-
- if (this.state === C.STATUS_ACCEPTED)
- {
- this.stateChanged(C.STATUS_TERMINATED);
- this.ua.destroyTransaction(this);
- }
- }
-
- onTransportError()
- {
- if (!this.transportError)
- {
- this.transportError = true;
-
- loggerist.debug(`transport error occurred, deleting transaction ${this.id}`);
-
- if (this.resendProvisionalTimer !== null)
- {
- clearInterval(this.resendProvisionalTimer);
- this.resendProvisionalTimer = null;
- }
-
- clearTimeout(this.L);
- clearTimeout(this.H);
- clearTimeout(this.I);
-
- this.stateChanged(C.STATUS_TERMINATED);
- this.ua.destroyTransaction(this);
- }
- }
-
- resend_provisional()
- {
- if (!this.transport.send(this.last_response))
- {
- this.onTransportError();
- }
- }
-
- // INVITE Server Transaction RFC 3261 17.2.1.
- receiveResponse(status_code, response, onSuccess, onFailure)
- {
- if (status_code >= 100 && status_code <= 199)
- {
- switch (this.state)
- {
- case C.STATUS_PROCEEDING:
- if (!this.transport.send(response))
- {
- this.onTransportError();
- }
- this.last_response = response;
- break;
- }
- }
-
- if (status_code > 100 && status_code <= 199 && this.state === C.STATUS_PROCEEDING)
- {
- // Trigger the resendProvisionalTimer only for the first non 100 provisional response.
- if (this.resendProvisionalTimer === null)
- {
- this.resendProvisionalTimer = setInterval(() =>
- {
- this.resend_provisional();
- }, Timers.PROVISIONAL_RESPONSE_INTERVAL);
- }
- }
- else if (status_code >= 200 && status_code <= 299)
- {
- switch (this.state)
- {
- case C.STATUS_PROCEEDING:
- this.stateChanged(C.STATUS_ACCEPTED);
- this.last_response = response;
- this.L = setTimeout(() =>
- {
- this.timer_L();
- }, Timers.TIMER_L);
-
- if (this.resendProvisionalTimer !== null)
- {
- clearInterval(this.resendProvisionalTimer);
- this.resendProvisionalTimer = null;
- }
-
- /* falls through */
- case C.STATUS_ACCEPTED:
- // Note that this point will be reached for proceeding this.state also.
- if (!this.transport.send(response))
- {
- this.onTransportError();
- if (onFailure)
- {
- onFailure();
- }
- }
- else if (onSuccess)
- {
- onSuccess();
- }
- break;
- }
- }
- else if (status_code >= 300 && status_code <= 699)
- {
- switch (this.state)
- {
- case C.STATUS_PROCEEDING:
- if (this.resendProvisionalTimer !== null)
- {
- clearInterval(this.resendProvisionalTimer);
- this.resendProvisionalTimer = null;
- }
-
- if (!this.transport.send(response))
- {
- this.onTransportError();
- if (onFailure)
- {
- onFailure();
- }
- }
- else
- {
- this.stateChanged(C.STATUS_COMPLETED);
- this.H = setTimeout(() =>
- {
- this.timer_H();
- }, Timers.TIMER_H);
- if (onSuccess)
- {
- onSuccess();
- }
- }
- break;
- }
- }
- }
+class InviteServerTransaction extends EventEmitter {
+ constructor(ua, transport, request) {
+ super();
+
+ this.type = C.INVITE_SERVER;
+ this.id = request.via_branch;
+ this.ua = ua;
+ this.transport = transport;
+ this.request = request;
+ this.last_response = '';
+ request.server_transaction = this;
+
+ this.state = C.STATUS_PROCEEDING;
+
+ ua.newTransaction(this);
+
+ this.resendProvisionalTimer = null;
+
+ request.reply(100);
+ }
+
+ get C() {
+ return C;
+ }
+
+ stateChanged(state) {
+ this.state = state;
+ this.emit('stateChanged');
+ }
+
+ timer_H() {
+ loggerist.debug(`Timer H expired for transaction ${this.id}`);
+
+ if (this.state === C.STATUS_COMPLETED) {
+ loggerist.debug('ACK not received, dialog will be terminated');
+ }
+
+ this.stateChanged(C.STATUS_TERMINATED);
+ this.ua.destroyTransaction(this);
+ }
+
+ timer_I() {
+ this.stateChanged(C.STATUS_TERMINATED);
+ this.ua.destroyTransaction(this);
+ }
+
+ // RFC 6026 7.1.
+ timer_L() {
+ loggerist.debug(`Timer L expired for transaction ${this.id}`);
+
+ if (this.state === C.STATUS_ACCEPTED) {
+ this.stateChanged(C.STATUS_TERMINATED);
+ this.ua.destroyTransaction(this);
+ }
+ }
+
+ onTransportError() {
+ if (!this.transportError) {
+ this.transportError = true;
+
+ loggerist.debug(
+ `transport error occurred, deleting transaction ${this.id}`
+ );
+
+ if (this.resendProvisionalTimer !== null) {
+ clearInterval(this.resendProvisionalTimer);
+ this.resendProvisionalTimer = null;
+ }
+
+ clearTimeout(this.L);
+ clearTimeout(this.H);
+ clearTimeout(this.I);
+
+ this.stateChanged(C.STATUS_TERMINATED);
+ this.ua.destroyTransaction(this);
+ }
+ }
+
+ resend_provisional() {
+ if (!this.transport.send(this.last_response)) {
+ this.onTransportError();
+ }
+ }
+
+ // INVITE Server Transaction RFC 3261 17.2.1.
+ receiveResponse(status_code, response, onSuccess, onFailure) {
+ if (status_code >= 100 && status_code <= 199) {
+ switch (this.state) {
+ case C.STATUS_PROCEEDING: {
+ if (!this.transport.send(response)) {
+ this.onTransportError();
+ }
+ this.last_response = response;
+ break;
+ }
+ }
+ }
+
+ if (
+ status_code > 100 &&
+ status_code <= 199 &&
+ this.state === C.STATUS_PROCEEDING
+ ) {
+ // Trigger the resendProvisionalTimer only for the first non 100 provisional response.
+ if (this.resendProvisionalTimer === null) {
+ this.resendProvisionalTimer = setInterval(() => {
+ this.resend_provisional();
+ }, Timers.PROVISIONAL_RESPONSE_INTERVAL);
+ }
+ } else if (status_code >= 200 && status_code <= 299) {
+ switch (this.state) {
+ case C.STATUS_PROCEEDING: {
+ this.stateChanged(C.STATUS_ACCEPTED);
+ this.last_response = response;
+ this.L = setTimeout(() => {
+ this.timer_L();
+ }, Timers.TIMER_L);
+
+ if (this.resendProvisionalTimer !== null) {
+ clearInterval(this.resendProvisionalTimer);
+ this.resendProvisionalTimer = null;
+ }
+ }
+ // falls through
+ case C.STATUS_ACCEPTED: {
+ // Note that this point will be reached for proceeding this.state also.
+ if (!this.transport.send(response)) {
+ this.onTransportError();
+ if (onFailure) {
+ onFailure();
+ }
+ } else if (onSuccess) {
+ onSuccess();
+ }
+ break;
+ }
+ }
+ } else if (status_code >= 300 && status_code <= 699) {
+ switch (this.state) {
+ case C.STATUS_PROCEEDING: {
+ if (this.resendProvisionalTimer !== null) {
+ clearInterval(this.resendProvisionalTimer);
+ this.resendProvisionalTimer = null;
+ }
+
+ if (!this.transport.send(response)) {
+ this.onTransportError();
+ if (onFailure) {
+ onFailure();
+ }
+ } else {
+ this.stateChanged(C.STATUS_COMPLETED);
+ this.H = setTimeout(() => {
+ this.timer_H();
+ }, Timers.TIMER_H);
+ if (onSuccess) {
+ onSuccess();
+ }
+ }
+ break;
+ }
+ }
+ }
+ }
}
/**
@@ -703,103 +630,95 @@ class InviteServerTransaction extends EventEmitter
* _true_ retransmission
* _false_ new request
*/
-function checkTransaction({ _transactions }, request)
-{
- let tr;
-
- switch (request.method)
- {
- case JsSIP_C.INVITE:
- tr = _transactions.ist[request.via_branch];
- if (tr)
- {
- switch (tr.state)
- {
- case C.STATUS_PROCEEDING:
- tr.transport.send(tr.last_response);
- break;
-
- // RFC 6026 7.1 Invite retransmission.
- // Received while in C.STATUS_ACCEPTED state. Absorb it.
- case C.STATUS_ACCEPTED:
- break;
- }
-
- return true;
- }
- break;
- case JsSIP_C.ACK:
- tr = _transactions.ist[request.via_branch];
-
- // RFC 6026 7.1.
- if (tr)
- {
- if (tr.state === C.STATUS_ACCEPTED)
- {
- return false;
- }
- else if (tr.state === C.STATUS_COMPLETED)
- {
- tr.state = C.STATUS_CONFIRMED;
- tr.I = setTimeout(() => { tr.timer_I(); }, Timers.TIMER_I);
-
- return true;
- }
- }
- // ACK to 2XX Response.
- else
- {
- return false;
- }
- break;
- case JsSIP_C.CANCEL:
- tr = _transactions.ist[request.via_branch];
- if (tr)
- {
- request.reply_sl(200);
- if (tr.state === C.STATUS_PROCEEDING)
- {
- return false;
- }
- else
- {
- return true;
- }
- }
- else
- {
- request.reply_sl(481);
-
- return true;
- }
- default:
-
- // Non-INVITE Server Transaction RFC 3261 17.2.2.
- tr = _transactions.nist[request.via_branch];
- if (tr)
- {
- switch (tr.state)
- {
- case C.STATUS_TRYING:
- break;
- case C.STATUS_PROCEEDING:
- case C.STATUS_COMPLETED:
- tr.transport.send(tr.last_response);
- break;
- }
-
- return true;
- }
- break;
- }
+function checkTransaction({ _transactions }, request) {
+ let tr;
+
+ switch (request.method) {
+ case JsSIP_C.INVITE: {
+ tr = _transactions.ist[request.via_branch];
+ if (tr) {
+ switch (tr.state) {
+ case C.STATUS_PROCEEDING: {
+ tr.transport.send(tr.last_response);
+ break;
+ }
+
+ // RFC 6026 7.1 Invite retransmission.
+ // Received while in C.STATUS_ACCEPTED state. Absorb it.
+ case C.STATUS_ACCEPTED: {
+ break;
+ }
+ }
+
+ return true;
+ }
+ break;
+ }
+ case JsSIP_C.ACK: {
+ tr = _transactions.ist[request.via_branch];
+
+ // RFC 6026 7.1.
+ if (tr) {
+ if (tr.state === C.STATUS_ACCEPTED) {
+ return false;
+ } else if (tr.state === C.STATUS_COMPLETED) {
+ tr.state = C.STATUS_CONFIRMED;
+ tr.I = setTimeout(() => {
+ tr.timer_I();
+ }, Timers.TIMER_I);
+
+ return true;
+ }
+ }
+ // ACK to 2XX Response.
+ else {
+ return false;
+ }
+ break;
+ }
+ case JsSIP_C.CANCEL: {
+ tr = _transactions.ist[request.via_branch];
+ if (tr) {
+ request.reply_sl(200);
+ if (tr.state === C.STATUS_PROCEEDING) {
+ return false;
+ } else {
+ return true;
+ }
+ } else {
+ request.reply_sl(481);
+
+ return true;
+ }
+ }
+ default: {
+ // Non-INVITE Server Transaction RFC 3261 17.2.2.
+ tr = _transactions.nist[request.via_branch];
+ if (tr) {
+ switch (tr.state) {
+ case C.STATUS_TRYING: {
+ break;
+ }
+ case C.STATUS_PROCEEDING:
+ case C.STATUS_COMPLETED: {
+ tr.transport.send(tr.last_response);
+ break;
+ }
+ }
+
+ return true;
+ }
+ break;
+ }
+ }
}
module.exports = {
- C,
- NonInviteClientTransaction,
- InviteClientTransaction,
- AckClientTransaction,
- NonInviteServerTransaction,
- InviteServerTransaction,
- checkTransaction
+ C,
+ NonInviteClientTransaction,
+ InviteClientTransaction,
+ AckClientTransaction,
+ NonInviteServerTransaction,
+ InviteServerTransaction,
+ checkTransaction,
};
diff --git a/src/Transport.d.ts b/src/Transport.d.ts
index 5d9e79a..0829a9b 100644
--- a/src/Transport.d.ts
+++ b/src/Transport.d.ts
@@ -1,10 +1,10 @@
-import {Socket} from './Socket'
+import { Socket } from './Socket';
export interface RecoveryOptions {
- min_interval: number;
- max_interval: number;
+ min_interval: number;
+ max_interval: number;
}
export class Transport extends Socket {
- constructor(sockets: Socket | Socket[], recovery_options?: RecoveryOptions)
+ constructor(sockets: Socket | Socket[], recovery_options?: RecoveryOptions);
}
diff --git a/src/Transport.js b/src/Transport.js
index ca2d715..258382c 100644
--- a/src/Transport.js
+++ b/src/Transport.js
@@ -8,22 +8,22 @@ const logger = new Logger('Transport');
* Constants
*/
const C = {
- // Transport status.
- STATUS_CONNECTED : 0,
- STATUS_CONNECTING : 1,
- STATUS_DISCONNECTED : 2,
-
- // Socket status.
- SOCKET_STATUS_READY : 0,
- SOCKET_STATUS_ERROR : 1,
-
- // Recovery options.
- recovery_options : {
- // minimum interval in seconds between recover attempts.
- min_interval : JsSIP_C.CONNECTION_RECOVERY_MIN_INTERVAL,
- // maximum interval in seconds between recover attempts.
- max_interval : JsSIP_C.CONNECTION_RECOVERY_MAX_INTERVAL
- }
+ // Transport status.
+ STATUS_CONNECTED: 0,
+ STATUS_CONNECTING: 1,
+ STATUS_DISCONNECTED: 2,
+
+ // Socket status.
+ SOCKET_STATUS_READY: 0,
+ SOCKET_STATUS_ERROR: 1,
+
+ // Recovery options.
+ recovery_options: {
+ // minimum interval in seconds between recover attempts.
+ min_interval: JsSIP_C.CONNECTION_RECOVERY_MIN_INTERVAL,
+ // maximum interval in seconds between recover attempts.
+ max_interval: JsSIP_C.CONNECTION_RECOVERY_MAX_INTERVAL,
+ },
};
/*
@@ -32,366 +32,314 @@ const C = {
*
* @socket JsSIP::Socket instance
*/
-module.exports = class Transport
-{
- constructor(sockets, recovery_options = C.recovery_options)
- {
- logger.debug('new()');
-
- this.status = C.STATUS_DISCONNECTED;
-
- // Current socket.
- this.socket = null;
-
- // Socket collection.
- this.sockets = [];
-
- this.recovery_options = recovery_options;
- this.recover_attempts = 0;
- this.recovery_timer = null;
-
- this.close_requested = false;
-
- // It seems that TextDecoder is not available in some versions of React-Native.
- // See https://github.com/versatica/JsSIP/issues/695
- try
- {
- this.textDecoder = new TextDecoder('utf8');
- }
- catch (error)
- {
- logger.warn(`cannot use TextDecoder: ${error}`);
- }
-
- if (typeof sockets === 'undefined')
- {
- throw new TypeError('Invalid argument.' +
- ' undefined \'sockets\' argument');
- }
-
- if (!(sockets instanceof Array))
- {
- sockets = [ sockets ];
- }
-
- sockets.forEach(function(socket)
- {
- if (!Socket.isSocket(socket.socket))
- {
- throw new TypeError('Invalid argument.' +
- ' invalid \'JsSIP.Socket\' instance');
- }
-
- if (socket.weight && !Number(socket.weight))
- {
- throw new TypeError('Invalid argument.' +
- ' \'weight\' attribute is not a number');
- }
-
- this.sockets.push({
- socket : socket.socket,
- weight : socket.weight || 0,
- status : C.SOCKET_STATUS_READY
- });
- }, this);
-
- // Get the socket with higher weight.
- this._getSocket();
- }
-
- /**
- * Instance Methods
- */
-
- get via_transport()
- {
- return this.socket.via_transport;
- }
-
- get url()
- {
- return this.socket.url;
- }
-
- get sip_uri()
- {
- return this.socket.sip_uri;
- }
-
- connect()
- {
- logger.debug('connect()');
-
- if (this.isConnected())
- {
- logger.debug('Transport is already connected');
-
- return;
- }
- else if (this.isConnecting())
- {
- logger.debug('Transport is connecting');
-
- return;
- }
-
- this.close_requested = false;
- this.status = C.STATUS_CONNECTING;
- this.onconnecting({ socket: this.socket, attempts: this.recover_attempts });
-
- if (!this.close_requested)
- {
- // Bind socket event callbacks.
- this.socket.onconnect = this._onConnect.bind(this);
- this.socket.ondisconnect = this._onDisconnect.bind(this);
- this.socket.ondata = this._onData.bind(this);
-
- this.socket.connect();
- }
-
- return;
- }
-
- disconnect()
- {
- logger.debug('close()');
-
- this.close_requested = true;
- this.recover_attempts = 0;
- this.status = C.STATUS_DISCONNECTED;
-
- // Clear recovery_timer.
- if (this.recovery_timer !== null)
- {
- clearTimeout(this.recovery_timer);
- this.recovery_timer = null;
- }
-
- // Unbind socket event callbacks.
- this.socket.onconnect = () => {};
- this.socket.ondisconnect = () => {};
- this.socket.ondata = () => {};
-
- this.socket.disconnect();
- this.ondisconnect({
- socket : this.socket,
- error : false
- });
- }
-
- send(data)
- {
- logger.debug('send()');
-
- if (!this.isConnected())
- {
- logger.warn('unable to send message, transport is not connected');
-
- return false;
- }
-
- const message = data.toString();
-
- logger.debug(`sending message:\n\n${message}\n`);
-
- return this.socket.send(message);
- }
-
- isConnected()
- {
- return this.status === C.STATUS_CONNECTED;
- }
-
- isConnecting()
- {
- return this.status === C.STATUS_CONNECTING;
- }
-
- /**
- * Private API.
- */
-
- _reconnect()
- {
- this.recover_attempts+=1;
-
- let k = Math.floor((Math.random() * Math.pow(2, this.recover_attempts)) +1);
-
- if (k < this.recovery_options.min_interval)
- {
- k = this.recovery_options.min_interval;
- }
-
- else if (k > this.recovery_options.max_interval)
- {
- k = this.recovery_options.max_interval;
- }
-
- logger.debug(`reconnection attempt: ${this.recover_attempts}. next connection attempt in ${k} seconds`);
-
- this.recovery_timer = setTimeout(() =>
- {
- if (!this.close_requested && !(this.isConnected() || this.isConnecting()))
- {
- // Get the next available socket with higher weight.
- this._getSocket();
-
- // Connect the socket.
- this.connect();
- }
- }, k * 1000);
- }
-
- /**
- * get the next available socket with higher weight
- */
- _getSocket()
- {
-
- let candidates = [];
-
- this.sockets.forEach((socket) =>
- {
- if (socket.status === C.SOCKET_STATUS_ERROR)
- {
- return; // continue the array iteration
- }
- else if (candidates.length === 0)
- {
- candidates.push(socket);
- }
- else if (socket.weight > candidates[0].weight)
- {
- candidates = [ socket ];
- }
- else if (socket.weight === candidates[0].weight)
- {
- candidates.push(socket);
- }
- });
-
- if (candidates.length === 0)
- {
- // All sockets have failed. reset sockets status.
- this.sockets.forEach((socket) =>
- {
- socket.status = C.SOCKET_STATUS_READY;
- });
-
- // Get next available socket.
- this._getSocket();
-
- return;
- }
-
- const idx = Math.floor((Math.random()* candidates.length));
-
- this.socket = candidates[idx].socket;
- }
-
- /**
- * Socket Event Handlers
- */
-
- _onConnect()
- {
- this.recover_attempts = 0;
- this.status = C.STATUS_CONNECTED;
-
- // Clear recovery_timer.
- if (this.recovery_timer !== null)
- {
- clearTimeout(this.recovery_timer);
- this.recovery_timer = null;
- }
-
- this.onconnect({ socket: this });
- }
-
- _onDisconnect(error, code, reason)
- {
- this.status = C.STATUS_DISCONNECTED;
- this.ondisconnect({
- socket : this.socket,
- error,
- code,
- reason
- });
-
- if (this.close_requested)
- {
- return;
- }
-
- // Update socket status.
- else
- {
- this.sockets.forEach(function(socket)
- {
- if (this.socket === socket.socket)
- {
- socket.status = C.SOCKET_STATUS_ERROR;
- }
- }, this);
- }
-
- this._reconnect(error);
- }
-
- _onData(data)
- {
- // CRLF Keep Alive request from server, reply.
- if (data === '\r\n\r\n')
- {
- logger.debug('received message with double-CRLF Keep Alive request');
-
- try
- {
- // Reply with single CRLF.
- this.socket.send('\r\n');
- }
- catch (error)
- {
- logger.warn(`error sending Keep Alive response: ${error}`);
- }
-
- return;
- }
-
- // CRLF Keep Alive response from server, ignore it.
- if (data === '\r\n')
- {
- logger.debug('received message with CRLF Keep Alive response');
-
- return;
- }
-
- // Binary message.
- else if (typeof data !== 'string')
- {
- try
- {
- if (this.textDecoder)
- data = this.textDecoder.decode(data);
- else
- data = String.fromCharCode.apply(null, new Uint8Array(data));
- }
- catch (error)
- {
- logger.debug(`received binary message failed to be converted into string: ${error}`);
-
- return;
- }
-
- logger.debug(`received binary message:\n\n${data}\n`);
- }
-
- // Text message.
- else
- {
- logger.debug(`received text message:\n\n${data}\n`);
- }
-
- this.ondata({ transport: this, message: data });
- }
+module.exports = class Transport {
+ constructor(sockets, recovery_options = C.recovery_options) {
+ logger.debug('new()');
+
+ this.status = C.STATUS_DISCONNECTED;
+
+ // Current socket.
+ this.socket = null;
+
+ // Socket collection.
+ this.sockets = [];
+
+ this.recovery_options = recovery_options;
+ this.recover_attempts = 0;
+ this.recovery_timer = null;
+
+ this.close_requested = false;
+
+ // It seems that TextDecoder is not available in some versions of React-Native.
+ // See https://github.com/versatica/JsSIP/issues/695
+ try {
+ this.textDecoder = new TextDecoder('utf8');
+ } catch (error) {
+ logger.warn(`cannot use TextDecoder: ${error}`);
+ }
+
+ if (typeof sockets === 'undefined') {
+ throw new TypeError("Invalid argument. undefined 'sockets' argument");
+ }
+
+ if (!(sockets instanceof Array)) {
+ sockets = [sockets];
+ }
+
+ sockets.forEach(function (socket) {
+ if (!Socket.isSocket(socket.socket)) {
+ throw new TypeError(
+ "Invalid argument. invalid 'JsSIP.Socket' instance"
+ );
+ }
+
+ if (socket.weight && !Number(socket.weight)) {
+ throw new TypeError(
+ "Invalid argument. 'weight' attribute is not a number"
+ );
+ }
+
+ this.sockets.push({
+ socket: socket.socket,
+ weight: socket.weight || 0,
+ status: C.SOCKET_STATUS_READY,
+ });
+ }, this);
+
+ // Get the socket with higher weight.
+ this._getSocket();
+ }
+
+ /**
+ * Instance Methods
+ */
+
+ get via_transport() {
+ return this.socket.via_transport;
+ }
+
+ get url() {
+ return this.socket.url;
+ }
+
+ get sip_uri() {
+ return this.socket.sip_uri;
+ }
+
+ connect() {
+ logger.debug('connect()');
+
+ if (this.isConnected()) {
+ logger.debug('Transport is already connected');
+
+ return;
+ } else if (this.isConnecting()) {
+ logger.debug('Transport is connecting');
+
+ return;
+ }
+
+ this.close_requested = false;
+ this.status = C.STATUS_CONNECTING;
+ this.onconnecting({ socket: this.socket, attempts: this.recover_attempts });
+
+ if (!this.close_requested) {
+ // Bind socket event callbacks.
+ this.socket.onconnect = this._onConnect.bind(this);
+ this.socket.ondisconnect = this._onDisconnect.bind(this);
+ this.socket.ondata = this._onData.bind(this);
+
+ this.socket.connect();
+ }
+
+ return;
+ }
+
+ disconnect() {
+ logger.debug('close()');
+
+ this.close_requested = true;
+ this.recover_attempts = 0;
+ this.status = C.STATUS_DISCONNECTED;
+
+ // Clear recovery_timer.
+ if (this.recovery_timer !== null) {
+ clearTimeout(this.recovery_timer);
+ this.recovery_timer = null;
+ }
+
+ // Unbind socket event callbacks.
+ this.socket.onconnect = () => {};
+ this.socket.ondisconnect = () => {};
+ this.socket.ondata = () => {};
+
+ this.socket.disconnect();
+ this.ondisconnect({
+ socket: this.socket,
+ error: false,
+ });
+ }
+
+ send(data) {
+ logger.debug('send()');
+
+ if (!this.isConnected()) {
+ logger.warn('unable to send message, transport is not connected');
+
+ return false;
+ }
+
+ const message = data.toString();
+
+ logger.debug(`sending message:\n\n${message}\n`);
+
+ return this.socket.send(message);
+ }
+
+ isConnected() {
+ return this.status === C.STATUS_CONNECTED;
+ }
+
+ isConnecting() {
+ return this.status === C.STATUS_CONNECTING;
+ }
+
+ /**
+ * Private API.
+ */
+
+ _reconnect() {
+ this.recover_attempts += 1;
+
+ let k = Math.floor(Math.random() * Math.pow(2, this.recover_attempts) + 1);
+
+ if (k < this.recovery_options.min_interval) {
+ k = this.recovery_options.min_interval;
+ } else if (k > this.recovery_options.max_interval) {
+ k = this.recovery_options.max_interval;
+ }
+
+ logger.debug(
+ `reconnection attempt: ${this.recover_attempts}. next connection attempt in ${k} seconds`
+ );
+
+ this.recovery_timer = setTimeout(() => {
+ if (
+ !this.close_requested &&
+ !(this.isConnected() || this.isConnecting())
+ ) {
+ // Get the next available socket with higher weight.
+ this._getSocket();
+
+ // Connect the socket.
+ this.connect();
+ }
+ }, k * 1000);
+ }
+
+ /**
+ * get the next available socket with higher weight
+ */
+ _getSocket() {
+ let candidates = [];
+
+ this.sockets.forEach(socket => {
+ if (socket.status === C.SOCKET_STATUS_ERROR) {
+ return; // continue the array iteration
+ } else if (candidates.length === 0) {
+ candidates.push(socket);
+ } else if (socket.weight > candidates[0].weight) {
+ candidates = [socket];
+ } else if (socket.weight === candidates[0].weight) {
+ candidates.push(socket);
+ }
+ });
+
+ if (candidates.length === 0) {
+ // All sockets have failed. reset sockets status.
+ this.sockets.forEach(socket => {
+ socket.status = C.SOCKET_STATUS_READY;
+ });
+
+ // Get next available socket.
+ this._getSocket();
+
+ return;
+ }
+
+ const idx = Math.floor(Math.random() * candidates.length);
+
+ this.socket = candidates[idx].socket;
+ }
+
+ /**
+ * Socket Event Handlers
+ */
+
+ _onConnect() {
+ this.recover_attempts = 0;
+ this.status = C.STATUS_CONNECTED;
+
+ // Clear recovery_timer.
+ if (this.recovery_timer !== null) {
+ clearTimeout(this.recovery_timer);
+ this.recovery_timer = null;
+ }
+
+ this.onconnect({ socket: this });
+ }
+
+ _onDisconnect(error, code, reason) {
+ this.status = C.STATUS_DISCONNECTED;
+ this.ondisconnect({
+ socket: this.socket,
+ error,
+ code,
+ reason,
+ });
+
+ if (this.close_requested) {
+ return;
+ }
+
+ // Update socket status.
+ else {
+ this.sockets.forEach(function (socket) {
+ if (this.socket === socket.socket) {
+ socket.status = C.SOCKET_STATUS_ERROR;
+ }
+ }, this);
+ }
+
+ this._reconnect(error);
+ }
+
+ _onData(data) {
+ // CRLF Keep Alive request from server, reply.
+ if (data === '\r\n\r\n') {
+ logger.debug('received message with double-CRLF Keep Alive request');
+
+ try {
+ // Reply with single CRLF.
+ this.socket.send('\r\n');
+ } catch (error) {
+ logger.warn(`error sending Keep Alive response: ${error}`);
+ }
+
+ return;
+ }
+
+ // CRLF Keep Alive response from server, ignore it.
+ if (data === '\r\n') {
+ logger.debug('received message with CRLF Keep Alive response');
+
+ return;
+ }
+
+ // Binary message.
+ else if (typeof data !== 'string') {
+ try {
+ if (this.textDecoder) {
+ data = this.textDecoder.decode(data);
+ } else {
+ data = String.fromCharCode.apply(null, new Uint8Array(data));
+ }
+ } catch (error) {
+ logger.debug(
+ `received binary message failed to be converted into string: ${error}`
+ );
+
+ return;
+ }
+
+ logger.debug(`received binary message:\n\n${data}\n`);
+ }
+
+ // Text message.
+ else {
+ logger.debug(`received text message:\n\n${data}\n`);
+ }
+
+ this.ondata({ transport: this, message: data });
+ }
};
diff --git a/src/UA.d.ts b/src/UA.d.ts
index 0fff768..ae562cc 100644
--- a/src/UA.d.ts
+++ b/src/UA.d.ts
@@ -1,113 +1,123 @@
-import {EventEmitter} from 'events'
-
-import {Socket, WeightedSocket} from './Socket'
-import {AnswerOptions, Originator, RTCSession, RTCSessionEventMap, TerminateOptions} from './RTCSession'
-import {IncomingRequest, IncomingResponse, OutgoingRequest} from './SIPMessage'
-import {Message, SendMessageOptions} from './Message'
-import {Registrator} from './Registrator'
-import {Notifier} from './Notifier'
-import {Subscriber} from './Subscriber'
-import {URI} from './URI'
-import {causes} from './Constants'
+import { EventEmitter } from 'events';
+
+import { Socket, WeightedSocket } from './Socket';
+import {
+ AnswerOptions,
+ Originator,
+ RTCSession,
+ RTCSessionEventMap,
+ TerminateOptions,
+} from './RTCSession';
+import {
+ IncomingRequest,
+ IncomingResponse,
+ OutgoingRequest,
+} from './SIPMessage';
+import { Message, SendMessageOptions } from './Message';
+import { Registrator } from './Registrator';
+import { Notifier } from './Notifier';
+import { Subscriber } from './Subscriber';
+import { URI } from './URI';
+import { causes } from './Constants';
export interface UnRegisterOptions {
- all?: boolean;
+ all?: boolean;
}
export interface CallOptions extends AnswerOptions {
- eventHandlers?: Partial<RTCSessionEventMap>;
- anonymous?: boolean;
- fromUserName?: string;
- fromDisplayName?: string;
+ eventHandlers?: Partial<RTCSessionEventMap>;
+ anonymous?: boolean;
+ fromUserName?: string;
+ fromDisplayName?: string;
}
export interface UAConfiguration {
- // mandatory parameters
- sockets: Socket | Socket[] | WeightedSocket[] ;
- uri: string;
- // optional parameters
- authorization_jwt?: string;
- authorization_user?: string;
- connection_recovery_max_interval?: number;
- connection_recovery_min_interval?: number;
- contact_uri?: string;
- display_name?: string;
- instance_id?: string;
- no_answer_timeout?: number;
- session_timers?: boolean;
- session_timers_refresh_method?: string;
- session_timers_force_refresher?: boolean;
- password?: string;
- realm?: string;
- ha1?: string;
- register?: boolean;
- register_expires?: number;
- register_from_tag_trail?: string | (() => string);
- registrar_server?: string;
- use_preloaded_route?: boolean;
- user_agent?: string;
- extra_headers?: string[];
+ // mandatory parameters
+ sockets: Socket | Socket[] | WeightedSocket[];
+ uri: string;
+ // optional parameters
+ authorization_jwt?: string;
+ authorization_user?: string;
+ connection_recovery_max_interval?: number;
+ connection_recovery_min_interval?: number;
+ contact_uri?: string;
+ display_name?: string;
+ instance_id?: string;
+ no_answer_timeout?: number;
+ session_timers?: boolean;
+ session_timers_refresh_method?: string;
+ session_timers_force_refresher?: boolean;
+ password?: string;
+ realm?: string;
+ ha1?: string;
+ register?: boolean;
+ register_expires?: number;
+ register_from_tag_trail?: string | (() => string);
+ registrar_server?: string;
+ use_preloaded_route?: boolean;
+ user_agent?: string;
+ extra_headers?: string[];
}
export interface IncomingRTCSessionEvent {
- originator: Originator.REMOTE;
- session: RTCSession;
- request: IncomingRequest;
+ originator: Originator.REMOTE;
+ session: RTCSession;
+ request: IncomingRequest;
}
export interface OutgoingRTCSessionEvent {
- originator: Originator.LOCAL;
- session: RTCSession;
- request: OutgoingRequest;
+ originator: Originator.LOCAL;
+ session: RTCSession;
+ request: OutgoingRequest;
}
export type RTCSessionEvent = IncomingRTCSessionEvent | OutgoingRTCSessionEvent;
export interface ConnectingEvent {
- socket: Socket;
- attempts: number
+ socket: Socket;
+ attempts: number;
}
export interface ConnectedEvent {
- socket: Socket;
+ socket: Socket;
}
export interface DisconnectEvent {
- socket: Socket;
- error: boolean;
- code?: number;
- reason?: string;
+ socket: Socket;
+ error: boolean;
+ code?: number;
+ reason?: string;
}
export interface RegisteredEvent {
- response: IncomingResponse;
+ response: IncomingResponse;
}
export interface UnRegisteredEvent {
- response: IncomingResponse;
- cause?: causes;
+ response: IncomingResponse;
+ cause?: causes;
}
export interface IncomingMessageEvent {
- originator: Originator.REMOTE;
- message: Message;
- request: IncomingRequest;
+ originator: Originator.REMOTE;
+ message: Message;
+ request: IncomingRequest;
}
export interface OutgoingMessageEvent {
- originator: Originator.LOCAL;
- message: Message;
- request: OutgoingRequest;
+ originator: Originator.LOCAL;
+ message: Message;
+ request: OutgoingRequest;
}
export interface IncomingOptionsEvent {
- originator: Originator.REMOTE;
- request: IncomingRequest;
+ originator: Originator.REMOTE;
+ request: IncomingRequest;
}
export interface OutgoingOptionsEvent {
- originator: Originator.LOCAL;
- request: OutgoingRequest;
+ originator: Originator.LOCAL;
+ request: OutgoingRequest;
}
export type ConnectingListener = (event: ConnectingEvent) => void;
@@ -117,127 +127,159 @@ export type RegisteredListener = (event: RegisteredEvent) => void;
export type UnRegisteredListener = (event: UnRegisteredEvent) => void;
export type RegistrationFailedListener = UnRegisteredListener;
export type RegistrationExpiringListener = () => void;
-export type IncomingRTCSessionListener = (event: IncomingRTCSessionEvent) => void;
-export type OutgoingRTCSessionListener = (event: OutgoingRTCSessionEvent) => void;
-export type RTCSessionListener = IncomingRTCSessionListener | OutgoingRTCSessionListener;
+export type IncomingRTCSessionListener = (
+ event: IncomingRTCSessionEvent
+) => void;
+export type OutgoingRTCSessionListener = (
+ event: OutgoingRTCSessionEvent
+) => void;
+export type RTCSessionListener =
+ | IncomingRTCSessionListener
+ | OutgoingRTCSessionListener;
export type IncomingMessageListener = (event: IncomingMessageEvent) => void;
export type OutgoingMessageListener = (event: OutgoingMessageEvent) => void;
export type MessageListener = IncomingMessageListener | OutgoingMessageListener;
export type IncomingOptionsListener = (event: IncomingOptionsEvent) => void;
export type OutgoingOptionsListener = (event: OutgoingOptionsEvent) => void;
export type OptionsListener = IncomingOptionsListener | OutgoingOptionsListener;
-export type SipEventListener = <T = any>(event: { event: T; request: IncomingRequest; }) => void
-export type SipSubscribeListener = <T = any>(event: { event: T; request: IncomingRequest; }) => void
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export type SipEventListener = <T = any>(event: {
+ event: T;
+ request: IncomingRequest;
+}) => void;
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export type SipSubscribeListener = <T = any>(event: {
+ event: T;
+ request: IncomingRequest;
+}) => void;
export interface UAEventMap {
- connecting: ConnectingListener;
- connected: ConnectedListener;
- disconnected: DisconnectedListener;
- registered: RegisteredListener;
- unregistered: UnRegisteredListener;
- registrationFailed: RegistrationFailedListener;
- registrationExpiring: RegistrationExpiringListener;
- newRTCSession: RTCSessionListener;
- newMessage: MessageListener;
- sipEvent: SipEventListener;
- newSubscribe: SipSubscribeListener;
- newOptions: OptionsListener;
+ connecting: ConnectingListener;
+ connected: ConnectedListener;
+ disconnected: DisconnectedListener;
+ registered: RegisteredListener;
+ unregistered: UnRegisteredListener;
+ registrationFailed: RegistrationFailedListener;
+ registrationExpiring: RegistrationExpiringListener;
+ newRTCSession: RTCSessionListener;
+ newMessage: MessageListener;
+ sipEvent: SipEventListener;
+ newSubscribe: SipSubscribeListener;
+ newOptions: OptionsListener;
}
export interface UAContactOptions {
- anonymous?: boolean;
- outbound?: boolean;
+ anonymous?: boolean;
+ outbound?: boolean;
}
export interface UAContact {
- pub_gruu?: string,
- temp_gruu?: string,
- uri?: string;
+ pub_gruu?: string;
+ temp_gruu?: string;
+ uri?: string;
- toString(options?: UAContactOptions): string
+ toString(options?: UAContactOptions): string;
}
export interface RequestParams {
- from_uri: URI;
- from_display_name?: string;
- from_tag: string;
- to_uri: URI;
- to_display_name?: string;
- to_tag?: string;
- call_id: string;
- cseq: number;
+ from_uri: URI;
+ from_display_name?: string;
+ from_tag: string;
+ to_uri: URI;
+ to_display_name?: string;
+ to_tag?: string;
+ call_id: string;
+ cseq: number;
}
export interface SubscriberParams {
- from_uri: URI;
- from_display_name?: string;
- to_uri: URI;
- to_display_name?: string;
+ from_uri: URI;
+ from_display_name?: string;
+ to_uri: URI;
+ to_display_name?: string;
}
export interface SubscriberOptions {
- expires?: number;
- contentType?: string;
- allowEvents?: string;
- params?: SubscriberParams;
- extraHeaders?: string[];
+ expires?: number;
+ contentType?: string;
+ allowEvents?: string;
+ params?: SubscriberParams;
+ extraHeaders?: string[];
}
export interface NotifierOptions {
- allowEvents?: string;
- extraHeaders?: string[];
- pending?: boolean;
+ allowEvents?: string;
+ extraHeaders?: string[];
+ pending?: boolean;
}
declare enum UAStatus {
- // UA status codes.
- STATUS_INIT = 0,
- STATUS_READY = 1,
- STATUS_USER_CLOSED = 2,
- STATUS_NOT_READY = 3,
- // UA error codes.
- CONFIGURATION_ERROR = 1,
- NETWORK_ERROR = 2
+ // UA status codes.
+ STATUS_INIT = 0,
+ STATUS_READY = 1,
+ STATUS_USER_CLOSED = 2,
+ STATUS_NOT_READY = 3,
+ // UA error codes.
+ // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values
+ CONFIGURATION_ERROR = 1,
+ // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values
+ NETWORK_ERROR = 2,
}
export class UA extends EventEmitter {
- static get C(): typeof UAStatus;
+ static get C(): typeof UAStatus;
- constructor(configuration: UAConfiguration);
+ constructor(configuration: UAConfiguration);
- get C(): typeof UAStatus;
+ get C(): typeof UAStatus;
- get status(): UAStatus;
+ get status(): UAStatus;
- get contact(): UAContact;
+ get contact(): UAContact;
- start(): void;
+ start(): void;
- stop(): void;
+ stop(): void;
- register(): void;
+ register(): void;
- unregister(options?: UnRegisterOptions): void;
+ unregister(options?: UnRegisterOptions): void;
- registrator(): Registrator;
+ registrator(): Registrator;
- call(target: string, options?: CallOptions): RTCSession;
+ call(target: string, options?: CallOptions): RTCSession;
- sendMessage(target: string | URI, body: string, options?: SendMessageOptions): Message;
+ sendMessage(
+ target: string | URI,
+ body: string,
+ options?: SendMessageOptions
+ ): Message;
- subscribe(target: string, eventName: string, accept: string, options?: SubscriberOptions): Subscriber;
+ subscribe(
+ target: string,
+ eventName: string,
+ accept: string,
+ options?: SubscriberOptions
+ ): Subscriber;
- notify( subscribe: IncomingRequest, contentType: string, options?: NotifierOptions): Notifier;
+ notify(
+ subscribe: IncomingRequest,
+ contentType: string,
+ options?: NotifierOptions
+ ): Notifier;
- terminateSessions(options?: TerminateOptions): void;
+ terminateSessions(options?: TerminateOptions): void;
- isRegistered(): boolean;
+ isRegistered(): boolean;
- isConnected(): boolean;
+ isConnected(): boolean;
- get<T extends keyof UAConfiguration>(parameter: T): UAConfiguration[T];
+ get<T extends keyof UAConfiguration>(parameter: T): UAConfiguration[T];
- set<T extends keyof UAConfiguration>(parameter: T, value: UAConfiguration[T]): boolean;
+ set<T extends keyof UAConfiguration>(
+ parameter: T,
+ value: UAConfiguration[T]
+ ): boolean;
- on<T extends keyof UAEventMap>(type: T, listener: UAEventMap[T]): this;
+ on<T extends keyof UAEventMap>(type: T, listener: UAEventMap[T]): this;
}
diff --git a/src/UA.js b/src/UA.js
index 066df6c..687ff37 100644
--- a/src/UA.js
+++ b/src/UA.js
@@ -1,3 +1,5 @@
+/* eslint-disable no-invalid-this */
+
const EventEmitter = require('events').EventEmitter;
const Logger = require('./Logger');
const JsSIP_C = require('./Constants');
@@ -20,15 +22,15 @@ const config = require('./Config');
const logger = new Logger('UA');
const C = {
- // UA status codes.
- STATUS_INIT : 0,
- STATUS_READY : 1,
- STATUS_USER_CLOSED : 2,
- STATUS_NOT_READY : 3,
-
- // UA error codes.
- CONFIGURATION_ERROR : 1,
- NETWORK_ERROR : 2
+ // UA status codes.
+ STATUS_INIT: 0,
+ STATUS_READY: 1,
+ STATUS_USER_CLOSED: 2,
+ STATUS_NOT_READY: 3,
+
+ // UA error codes.
+ CONFIGURATION_ERROR: 1,
+ NETWORK_ERROR: 2,
};
/**
@@ -38,1004 +40,923 @@ const C = {
* @throws {JsSIP.Exceptions.ConfigurationError} If a configuration parameter is invalid.
* @throws {TypeError} If no configuration is given.
*/
-module.exports = class UA extends EventEmitter
-{
- // Expose C object.
- static get C()
- {
- return C;
- }
-
- constructor(configuration)
- {
- // Check configuration argument.
- if (!configuration)
- {
- throw new TypeError('Not enough arguments');
- }
-
- // Hide sensitive information.
- const sensitiveKeys = [ 'password', 'ha1', 'authorization_jwt' ];
-
- logger.debug('new() [configuration:%o]',
- Object.entries(configuration).filter(([ key ]) => !sensitiveKeys.includes(key))
- );
-
- super();
-
- this._cache = {
- credentials : {}
- };
-
- this._configuration = Object.assign({}, config.settings);
- this._dynConfiguration = {};
- this._dialogs = {};
-
- // User actions outside any session/dialog (MESSAGE/OPTIONS).
- this._applicants = {};
-
- this._sessions = {};
- this._transport = null;
- this._contact = null;
- this._status = C.STATUS_INIT;
- this._error = null;
- this._transactions = {
- nist : {},
- nict : {},
- ist : {},
- ict : {}
- };
-
- // Custom UA empty object for high level use.
- this._data = {};
-
- this._closeTimer = null;
-
- // Load configuration.
- try
- {
- this._loadConfig(configuration);
- }
- catch (error)
- {
- this._status = C.STATUS_NOT_READY;
- this._error = C.CONFIGURATION_ERROR;
- throw error;
- }
-
- // Initialize registrator.
- this._registrator = new Registrator(this);
- }
-
- get C()
- {
- return C;
- }
-
- get status()
- {
- return this._status;
- }
-
- get contact()
- {
- return this._contact;
- }
-
- get configuration()
- {
- return this._configuration;
- }
-
- get transport()
- {
- return this._transport;
- }
-
- // =================
- // High Level API
- // =================
-
- /**
- * Connect to the server if status = STATUS_INIT.
- * Resume UA after being closed.
- */
- start()
- {
- logger.debug('start()');
-
- if (this._status === C.STATUS_INIT)
- {
- this._transport.connect();
- }
- else if (this._status === C.STATUS_USER_CLOSED)
- {
- logger.debug('restarting UA');
-
- // Disconnect.
- if (this._closeTimer !== null)
- {
- clearTimeout(this._closeTimer);
- this._closeTimer = null;
- this._transport.disconnect();
- }
-
- // Reconnect.
- this._status = C.STATUS_INIT;
- this._transport.connect();
- }
- else if (this._status === C.STATUS_READY)
- {
- logger.debug('UA is in READY status, not restarted');
- }
- else
- {
- logger.debug('ERROR: connection is down, Auto-Recovery system is trying to reconnect');
- }
-
- // Set dynamic configuration.
- this._dynConfiguration.register = this._configuration.register;
- }
-
- /**
- * Register.
- */
- register()
- {
- logger.debug('register()');
-
- this._dynConfiguration.register = true;
- this._registrator.register();
- }
-
- /**
- * Unregister.
- */
- unregister(options)
- {
- logger.debug('unregister()');
-
- this._dynConfiguration.register = false;
- this._registrator.unregister(options);
- }
-
- /**
- * Get the Registrator instance.
- */
- registrator()
- {
- return this._registrator;
- }
-
- /**
- * Registration state.
- */
- isRegistered()
- {
- return this._registrator.registered;
- }
-
- /**
- * Connection state.
- */
- isConnected()
- {
- return this._transport.isConnected();
- }
-
- /**
- * Make an outgoing call.
- *
- * -param {String} target
- * -param {Object} [options]
- *
- * -throws {TypeError}
- *
- */
- call(target, options)
- {
- logger.debug('call()');
-
- const session = new RTCSession(this);
-
- session.connect(target, options);
-
- return session;
- }
-
- /**
- * Send a message.
- *
- * -param {String} target
- * -param {String} body
- * -param {Object} [options]
- *
- * -throws {TypeError}
- *
- */
- sendMessage(target, body, options)
- {
- logger.debug('sendMessage()');
-
- const message = new Message(this);
-
- message.send(target, body, options);
-
- return message;
- }
-
- /**
- * Create subscriber instance
- */
- subscribe(target, eventName, accept, options)
- {
- logger.debug('subscribe()');
-
- return new Subscriber(this, target, eventName, accept, options);
- }
-
- /**
- * Create notifier instance
- */
- notify(subscribe, contentType, options)
- {
- logger.debug('notify()');
-
- return new Notifier(this, subscribe, contentType, options);
- }
-
- /**
- * Send a SIP OPTIONS.
- *
- * -param {String} target
- * -param {String} [body]
- * -param {Object} [options]
- *
- * -throws {TypeError}
- *
- */
- sendOptions(target, body, options)
- {
- logger.debug('sendOptions()');
-
- const message = new Options(this);
-
- message.send(target, body, options);
-
- return message;
- }
-
- /**
- * Terminate ongoing sessions.
- */
- terminateSessions(options)
- {
- logger.debug('terminateSessions()');
-
- for (const idx in this._sessions)
- {
- if (!this._sessions[idx].isEnded())
- {
- this._sessions[idx].terminate(options);
- }
- }
- }
-
- /**
- * Gracefully close.
- *
- */
- stop()
- {
- logger.debug('stop()');
-
- // Remove dynamic settings.
- this._dynConfiguration = {};
-
- if (this._status === C.STATUS_USER_CLOSED)
- {
- logger.debug('UA already closed');
-
- return;
- }
-
- // Close registrator.
- this._registrator.close();
-
- // If there are session wait a bit so CANCEL/BYE can be sent and their responses received.
- const num_sessions = Object.keys(this._sessions).length;
-
- // Run _terminate_ on every Session.
- for (const session in this._sessions)
- {
- if (Object.prototype.hasOwnProperty.call(this._sessions, session))
- {
- logger.debug(`closing session ${session}`);
- try { this._sessions[session].terminate(); }
- // eslint-disable-next-line no-unused-vars
- catch (error) {}
- }
- }
-
- // Run _close_ on every applicant.
- for (const applicant in this._applicants)
- {
- if (Object.prototype.hasOwnProperty.call(this._applicants, applicant))
- try { this._applicants[applicant].close(); }
- // eslint-disable-next-line no-unused-vars
- catch (error) {}
- }
-
- this._status = C.STATUS_USER_CLOSED;
-
- const num_transactions =
- Object.keys(this._transactions.nict).length +
- Object.keys(this._transactions.nist).length +
- Object.keys(this._transactions.ict).length +
- Object.keys(this._transactions.ist).length;
-
- if (num_transactions === 0 && num_sessions === 0)
- {
- this._transport.disconnect();
- }
- else
- {
- this._closeTimer = setTimeout(() =>
- {
- this._closeTimer = null;
- this._transport.disconnect();
- }, 2000);
- }
- }
-
- /**
- * Normalice a string into a valid SIP request URI
- * -param {String} target
- * -returns {JsSIP.URI|undefined}
- */
- normalizeTarget(target)
- {
- return Utils.normalizeTarget(target, this._configuration.hostport_params);
- }
-
- /**
- * Allow retrieving configuration and autogenerated fields in runtime.
- */
- get(parameter)
- {
- switch (parameter)
- {
- case 'authorization_user':
- return this._configuration.authorization_user;
-
- case 'realm':
- return this._configuration.realm;
-
- case 'ha1':
- return this._configuration.ha1;
-
- case 'authorization_jwt':
- return this._configuration.authorization_jwt;
-
- default:
- logger.warn('get() | cannot get "%s" parameter in runtime', parameter);
-
- return undefined;
- }
- }
-
- /**
- * Allow configuration changes in runtime.
- * Returns true if the parameter could be set.
- */
- set(parameter, value)
- {
- switch (parameter)
- {
- case 'authorization_user': {
- this._configuration.authorization_user = String(value);
- break;
- }
-
- case 'password': {
- this._configuration.password = String(value);
- break;
- }
-
- case 'realm': {
- this._configuration.realm = String(value);
- break;
- }
-
- case 'ha1': {
- this._configuration.ha1 = String(value);
- // Delete the plain SIP password.
- this._configuration.password = null;
- break;
- }
-
- case 'authorization_jwt': {
- this._configuration.authorization_jwt = String(value);
- break;
- }
-
- case 'display_name': {
- this._configuration.display_name = value;
- break;
- }
-
- case 'extra_headers': {
- this._configuration.extra_headers = value;
- break;
- }
-
- default:
- logger.warn('set() | cannot set "%s" parameter in runtime', parameter);
-
- return false;
- }
-
- return true;
- }
-
- // ==========================
- // Event Handlers.
- // ==========================
-
- /**
- * new Transaction
- */
- newTransaction(transaction)
- {
- this._transactions[transaction.type][transaction.id] = transaction;
- this.emit('newTransaction', {
- transaction
- });
- }
-
- /**
- * Transaction destroyed.
- */
- destroyTransaction(transaction)
- {
- delete this._transactions[transaction.type][transaction.id];
- this.emit('transactionDestroyed', {
- transaction
- });
- }
-
- /**
- * new Dialog
- */
- newDialog(dialog)
- {
- this._dialogs[dialog.id] = dialog;
- }
-
- /**
- * Dialog destroyed.
- */
- destroyDialog(dialog)
- {
- delete this._dialogs[dialog.id];
- }
-
- /**
- * new Message
- */
- newMessage(message, data)
- {
- this._applicants[message] = message;
- this.emit('newMessage', data);
- }
-
- /**
- * new Options
- */
- newOptions(message, data)
- {
- this._applicants[message] = message;
- this.emit('newOptions', data);
- }
-
- /**
- * Message destroyed.
- */
- destroyMessage(message)
- {
- delete this._applicants[message];
- }
-
- /**
- * new RTCSession
- */
- newRTCSession(session, data)
- {
- this._sessions[session.id] = session;
- this.emit('newRTCSession', data);
- }
-
- /**
- * RTCSession destroyed.
- */
- destroyRTCSession(session)
- {
- delete this._sessions[session.id];
- }
-
- /**
- * Registered
- */
- registered(data)
- {
- this.emit('registered', data);
- }
-
- /**
- * Unregistered
- */
- unregistered(data)
- {
- this.emit('unregistered', data);
- }
-
- /**
- * Registration Failed
- */
- registrationFailed(data)
- {
- this.emit('registrationFailed', data);
- }
-
- // =========================
- // ReceiveRequest.
- // =========================
-
- /**
- * Request reception
- */
- receiveRequest(request)
- {
- const method = request.method;
-
- // Check that request URI points to us.
- if (request.ruri.user !== this._configuration.uri.user &&
- request.ruri.user !== this._contact.uri.user)
- {
- logger.debug('Request-URI does not point to us');
- if (request.method !== JsSIP_C.ACK)
- {
- request.reply_sl(404);
- }
-
- return;
- }
-
- // Check request URI scheme.
- if (request.ruri.scheme === JsSIP_C.SIPS)
- {
- request.reply_sl(416);
-
- return;
- }
-
- // Check transaction.
- if (Transactions.checkTransaction(this, request))
- {
- return;
- }
-
- // Create the server transaction.
- if (method === JsSIP_C.INVITE)
- {
- /* eslint-disable no-new */
- new Transactions.InviteServerTransaction(this, this._transport, request);
- /* eslint-enable no-new */
- }
- else if (method !== JsSIP_C.ACK && method !== JsSIP_C.CANCEL)
- {
- /* eslint-disable no-new */
- new Transactions.NonInviteServerTransaction(this, this._transport, request);
- /* eslint-enable no-new */
- }
-
- /* RFC3261 12.2.2
- * Requests that do not change in any way the state of a dialog may be
- * received within a dialog (for example, an OPTIONS request).
- * They are processed as if they had been received outside the dialog.
- */
- if (method === JsSIP_C.OPTIONS)
- {
- if (this.listeners('newOptions').length === 0)
- {
- request.reply(200);
-
- return;
- }
-
- const message = new Options(this);
-
- message.init_incoming(request);
- }
- else if (method === JsSIP_C.MESSAGE)
- {
- if (this.listeners('newMessage').length === 0)
- {
- request.reply(405);
-
- return;
- }
- const message = new Message(this);
-
- message.init_incoming(request);
- }
- else if (method === JsSIP_C.SUBSCRIBE)
- {
- if (this.listeners('newSubscribe').length === 0)
- {
- request.reply(405);
-
- return;
- }
- }
- else if (method === JsSIP_C.INVITE)
- {
- // Initial INVITE.
- if (!request.to_tag && this.listeners('newRTCSession').length === 0)
- {
- request.reply(405);
-
- return;
- }
- }
-
- let dialog;
- let session;
-
- // Initial Request.
- if (!request.to_tag)
- {
- switch (method)
- {
- case JsSIP_C.INVITE:
- if (window.RTCPeerConnection)
- { // TODO
- if (request.hasHeader('replaces'))
- {
- const replaces = request.replaces;
-
- dialog = this._findDialog(
- replaces.call_id, replaces.from_tag, replaces.to_tag);
- if (dialog)
- {
- session = dialog.owner;
- if (!session.isEnded())
- {
- session.receiveRequest(request);
- }
- else
- {
- request.reply(603);
- }
- }
- else
- {
- request.reply(481);
- }
- }
- else
- {
- session = new RTCSession(this);
- session.init_incoming(request);
- }
- }
- else
- {
- logger.warn('INVITE received but WebRTC is not supported');
- request.reply(488);
- }
- break;
- case JsSIP_C.BYE:
- // Out of dialog BYE received.
- request.reply(481);
- break;
- case JsSIP_C.CANCEL:
- session = this._findSession(request);
- if (session)
- {
- session.receiveRequest(request);
- }
- else
- {
- logger.debug('received CANCEL request for a non existent session');
- }
- break;
- case JsSIP_C.ACK:
- /* Absorb it.
- * ACK request without a corresponding Invite Transaction
- * and without To tag.
- */
- break;
- case JsSIP_C.NOTIFY:
- // Receive new sip event.
- this.emit('sipEvent', {
- event : request.event,
- request
- });
- request.reply(200);
- break;
- case JsSIP_C.SUBSCRIBE:
- Notifier.init_incoming(request, () =>
- {
- this.emit('newSubscribe', {
- event : request.event,
- request
- });
- });
- break;
- default:
- request.reply(405);
- break;
- }
- }
- // In-dialog request.
- else
- {
- dialog = this._findDialog(request.call_id, request.from_tag, request.to_tag);
-
- if (dialog)
- {
- dialog.receiveRequest(request);
- }
- else if (method === JsSIP_C.NOTIFY)
- {
- session = this._findSession(request);
- if (session)
- {
- session.receiveRequest(request);
- }
- else
- {
- logger.debug('received NOTIFY request for a non existent subscription');
- request.reply(481, 'Subscription does not exist');
- }
- }
-
- /* RFC3261 12.2.2
- * Request with to tag, but no matching dialog found.
- * Exception: ACK for an Invite request for which a dialog has not
- * been created.
- */
- else if (method !== JsSIP_C.ACK)
- {
- request.reply(481);
- }
- }
- }
-
- // =================
- // Utils.
- // =================
-
- /**
- * Get the session to which the request belongs to, if any.
- */
- _findSession({ call_id, from_tag, to_tag })
- {
- const sessionIDa = call_id + from_tag;
- const sessionA = this._sessions[sessionIDa];
- const sessionIDb = call_id + to_tag;
- const sessionB = this._sessions[sessionIDb];
-
- if (sessionA)
- {
- return sessionA;
- }
- else if (sessionB)
- {
- return sessionB;
- }
- else
- {
- return null;
- }
- }
-
- /**
- * Get the dialog to which the request belongs to, if any.
- */
- _findDialog(call_id, from_tag, to_tag)
- {
- let id = call_id + from_tag + to_tag;
- let dialog = this._dialogs[id];
-
- if (dialog)
- {
- return dialog;
- }
- else
- {
- id = call_id + to_tag + from_tag;
- dialog = this._dialogs[id];
- if (dialog)
- {
- return dialog;
- }
- else
- {
- return null;
- }
- }
- }
-
- _loadConfig(configuration)
- {
- // Check and load the given configuration.
- // This can throw.
- config.load(this._configuration, configuration);
-
- // Post Configuration Process.
-
- // Allow passing 0 number as display_name.
- if (this._configuration.display_name === 0)
- {
- this._configuration.display_name = '0';
- }
-
- // Instance-id for GRUU.
- if (!this._configuration.instance_id)
- {
- this._configuration.instance_id = Utils.newUUID();
- }
-
- // Jssip_id instance parameter. Static random tag of length 5.
- this._configuration.jssip_id = Utils.createRandomToken(5);
-
- // String containing this._configuration.uri without scheme and user.
- const hostport_params = this._configuration.uri.clone();
-
- hostport_params.user = null;
- this._configuration.hostport_params = hostport_params.toString().replace(/^sip:/i, '');
-
- // Transport.
- try
- {
- this._transport = new Transport(this._configuration.sockets, {
- // Recovery options.
- max_interval : this._configuration.connection_recovery_max_interval,
- min_interval : this._configuration.connection_recovery_min_interval
- });
-
- // Transport event callbacks.
- this._transport.onconnecting = onTransportConnecting.bind(this);
- this._transport.onconnect = onTransportConnect.bind(this);
- this._transport.ondisconnect = onTransportDisconnect.bind(this);
- this._transport.ondata = onTransportData.bind(this);
- }
- catch (error)
- {
- logger.warn(error);
- throw new Exceptions.ConfigurationError('sockets', this._configuration.sockets);
- }
-
- // Remove sockets instance from configuration object.
- delete this._configuration.sockets;
-
- // Check whether authorization_user is explicitly defined.
- // Take 'this._configuration.uri.user' value if not.
- if (!this._configuration.authorization_user)
- {
- this._configuration.authorization_user = this._configuration.uri.user;
- }
-
- // If no 'registrar_server' is set use the 'uri' value without user portion and
- // without URI params/headers.
- if (!this._configuration.registrar_server)
- {
- const registrar_server = this._configuration.uri.clone();
-
- registrar_server.user = null;
- registrar_server.clearParams();
- registrar_server.clearHeaders();
- this._configuration.registrar_server = registrar_server;
- }
-
- // User no_answer_timeout.
- this._configuration.no_answer_timeout *= 1000;
-
- // Via Host.
- if (this._configuration.contact_uri)
- {
- this._configuration.via_host = this._configuration.contact_uri.host;
- }
-
- // Contact URI.
- else
- {
- this._configuration.contact_uri = new URI('sip', Utils.createRandomToken(8), this._configuration.via_host, null, { transport: 'ws' });
- }
-
- this._contact = {
- pub_gruu : null,
- temp_gruu : null,
- uri : this._configuration.contact_uri,
- toString(options = {})
- {
- const anonymous = options.anonymous || null;
- const outbound = options.outbound || null;
- let contact = '<';
-
- if (anonymous)
- {
- contact += this.temp_gruu || 'sip:anonymous@anonymous.invalid;transport=ws';
- }
- else
- {
- contact += this.pub_gruu || this.uri.toString();
- }
-
- if (outbound && (anonymous ? !this.temp_gruu : !this.pub_gruu))
- {
- contact += ';ob';
- }
-
- contact += '>';
-
- return contact;
- }
- };
-
- // Seal the configuration.
- const writable_parameters = [
- 'authorization_user', 'password', 'realm', 'ha1', 'authorization_jwt', 'display_name', 'register', 'extra_headers'
- ];
-
- for (const parameter in this._configuration)
- {
- if (Object.prototype.hasOwnProperty.call(this._configuration, parameter))
- {
- if (writable_parameters.indexOf(parameter) !== -1)
- {
- Object.defineProperty(this._configuration, parameter, {
- writable : true,
- configurable : false
- });
- }
- else
- {
- Object.defineProperty(this._configuration, parameter, {
- writable : false,
- configurable : false
- });
- }
- }
- }
-
- logger.debug('configuration parameters after validation:');
- for (const parameter in this._configuration)
- {
- // Only show the user user configurable parameters.
- if (Object.prototype.hasOwnProperty.call(config.settings, parameter))
- {
- switch (parameter)
- {
- case 'uri':
- case 'registrar_server':
- logger.debug(`- ${parameter}: ${this._configuration[parameter]}`);
- break;
- case 'password':
- case 'ha1':
- case 'authorization_jwt':
- logger.debug(`- ${parameter}: NOT SHOWN`);
- break;
- default:
- logger.debug(`- ${parameter}: ${JSON.stringify(this._configuration[parameter])}`);
- }
- }
- }
-
- return;
- }
+module.exports = class UA extends EventEmitter {
+ // Expose C object.
+ static get C() {
+ return C;
+ }
+
+ constructor(configuration) {
+ // Check configuration argument.
+ if (!configuration) {
+ throw new TypeError('Not enough arguments');
+ }
+
+ // Hide sensitive information.
+ const sensitiveKeys = ['password', 'ha1', 'authorization_jwt'];
+
+ logger.debug(
+ 'new() [configuration:%o]',
+ Object.entries(configuration).filter(
+ ([key]) => !sensitiveKeys.includes(key)
+ )
+ );
+
+ super();
+
+ this._cache = {
+ credentials: {},
+ };
+
+ this._configuration = Object.assign({}, config.settings);
+ this._dynConfiguration = {};
+ this._dialogs = {};
+
+ // User actions outside any session/dialog (MESSAGE/OPTIONS).
+ this._applicants = {};
+
+ this._sessions = {};
+ this._transport = null;
+ this._contact = null;
+ this._status = C.STATUS_INIT;
+ this._error = null;
+ this._transactions = {
+ nist: {},
+ nict: {},
+ ist: {},
+ ict: {},
+ };
+
+ // Custom UA empty object for high level use.
+ this._data = {};
+
+ this._closeTimer = null;
+
+ // Load configuration.
+ try {
+ this._loadConfig(configuration);
+ } catch (error) {
+ this._status = C.STATUS_NOT_READY;
+ this._error = C.CONFIGURATION_ERROR;
+ throw error;
+ }
+
+ // Initialize registrator.
+ this._registrator = new Registrator(this);
+ }
+
+ get C() {
+ return C;
+ }
+
+ get status() {
+ return this._status;
+ }
+
+ get contact() {
+ return this._contact;
+ }
+
+ get configuration() {
+ return this._configuration;
+ }
+
+ get transport() {
+ return this._transport;
+ }
+
+ // =================
+ // High Level API
+ // =================
+
+ /**
+ * Connect to the server if status = STATUS_INIT.
+ * Resume UA after being closed.
+ */
+ start() {
+ logger.debug('start()');
+
+ if (this._status === C.STATUS_INIT) {
+ this._transport.connect();
+ } else if (this._status === C.STATUS_USER_CLOSED) {
+ logger.debug('restarting UA');
+
+ // Disconnect.
+ if (this._closeTimer !== null) {
+ clearTimeout(this._closeTimer);
+ this._closeTimer = null;
+ this._transport.disconnect();
+ }
+
+ // Reconnect.
+ this._status = C.STATUS_INIT;
+ this._transport.connect();
+ } else if (this._status === C.STATUS_READY) {
+ logger.debug('UA is in READY status, not restarted');
+ } else {
+ logger.debug(
+ 'ERROR: connection is down, Auto-Recovery system is trying to reconnect'
+ );
+ }
+
+ // Set dynamic configuration.
+ this._dynConfiguration.register = this._configuration.register;
+ }
+
+ /**
+ * Register.
+ */
+ register() {
+ logger.debug('register()');
+
+ this._dynConfiguration.register = true;
+ this._registrator.register();
+ }
+
+ /**
+ * Unregister.
+ */
+ unregister(options) {
+ logger.debug('unregister()');
+
+ this._dynConfiguration.register = false;
+ this._registrator.unregister(options);
+ }
+
+ /**
+ * Get the Registrator instance.
+ */
+ registrator() {
+ return this._registrator;
+ }
+
+ /**
+ * Registration state.
+ */
+ isRegistered() {
+ return this._registrator.registered;
+ }
+
+ /**
+ * Connection state.
+ */
+ isConnected() {
+ return this._transport.isConnected();
+ }
+
+ /**
+ * Make an outgoing call.
+ *
+ * -param {String} target
+ * -param {Object} [options]
+ *
+ * -throws {TypeError}
+ *
+ */
+ call(target, options) {
+ logger.debug('call()');
+
+ const session = new RTCSession(this);
+
+ session.connect(target, options);
+
+ return session;
+ }
+
+ /**
+ * Send a message.
+ *
+ * -param {String} target
+ * -param {String} body
+ * -param {Object} [options]
+ *
+ * -throws {TypeError}
+ *
+ */
+ sendMessage(target, body, options) {
+ logger.debug('sendMessage()');
+
+ const message = new Message(this);
+
+ message.send(target, body, options);
+
+ return message;
+ }
+
+ /**
+ * Create subscriber instance
+ */
+ subscribe(target, eventName, accept, options) {
+ logger.debug('subscribe()');
+
+ return new Subscriber(this, target, eventName, accept, options);
+ }
+
+ /**
+ * Create notifier instance
+ */
+ notify(subscribe, contentType, options) {
+ logger.debug('notify()');
+
+ return new Notifier(this, subscribe, contentType, options);
+ }
+
+ /**
+ * Send a SIP OPTIONS.
+ *
+ * -param {String} target
+ * -param {String} [body]
+ * -param {Object} [options]
+ *
+ * -throws {TypeError}
+ *
+ */
+ sendOptions(target, body, options) {
+ logger.debug('sendOptions()');
+
+ const message = new Options(this);
+
+ message.send(target, body, options);
+
+ return message;
+ }
+
+ /**
+ * Terminate ongoing sessions.
+ */
+ terminateSessions(options) {
+ logger.debug('terminateSessions()');
+
+ for (const idx in this._sessions) {
+ if (!this._sessions[idx].isEnded()) {
+ this._sessions[idx].terminate(options);
+ }
+ }
+ }
+
+ /**
+ * Gracefully close.
+ *
+ */
+ stop() {
+ logger.debug('stop()');
+
+ // Remove dynamic settings.
+ this._dynConfiguration = {};
+
+ if (this._status === C.STATUS_USER_CLOSED) {
+ logger.debug('UA already closed');
+
+ return;
+ }
+
+ // Close registrator.
+ this._registrator.close();
+
+ // If there are session wait a bit so CANCEL/BYE can be sent and their responses received.
+ const num_sessions = Object.keys(this._sessions).length;
+
+ // Run _terminate_ on every Session.
+ for (const session in this._sessions) {
+ if (Object.prototype.hasOwnProperty.call(this._sessions, session)) {
+ logger.debug(`closing session ${session}`);
+ try {
+ this._sessions[session].terminate();
+ } catch (error) {}
+ }
+ }
+
+ // Run _close_ on every applicant.
+ for (const applicant in this._applicants) {
+ if (Object.prototype.hasOwnProperty.call(this._applicants, applicant)) {
+ try {
+ this._applicants[applicant].close();
+ } catch (error) {}
+ }
+ }
+
+ this._status = C.STATUS_USER_CLOSED;
+
+ const num_transactions =
+ Object.keys(this._transactions.nict).length +
+ Object.keys(this._transactions.nist).length +
+ Object.keys(this._transactions.ict).length +
+ Object.keys(this._transactions.ist).length;
+
+ if (num_transactions === 0 && num_sessions === 0) {
+ this._transport.disconnect();
+ } else {
+ this._closeTimer = setTimeout(() => {
+ this._closeTimer = null;
+ this._transport.disconnect();
+ }, 2000);
+ }
+ }
+
+ /**
+ * Normalice a string into a valid SIP request URI
+ * -param {String} target
+ * -returns {JsSIP.URI|undefined}
+ */
+ normalizeTarget(target) {
+ return Utils.normalizeTarget(target, this._configuration.hostport_params);
+ }
+
+ /**
+ * Allow retrieving configuration and autogenerated fields in runtime.
+ */
+ get(parameter) {
+ switch (parameter) {
+ case 'authorization_user': {
+ return this._configuration.authorization_user;
+ }
+
+ case 'realm': {
+ return this._configuration.realm;
+ }
+
+ case 'ha1': {
+ return this._configuration.ha1;
+ }
+
+ case 'authorization_jwt': {
+ return this._configuration.authorization_jwt;
+ }
+
+ default: {
+ logger.warn('get() | cannot get "%s" parameter in runtime', parameter);
+
+ return undefined;
+ }
+ }
+ }
+
+ /**
+ * Allow configuration changes in runtime.
+ * Returns true if the parameter could be set.
+ */
+ set(parameter, value) {
+ switch (parameter) {
+ case 'authorization_user': {
+ this._configuration.authorization_user = String(value);
+ break;
+ }
+
+ case 'password': {
+ this._configuration.password = String(value);
+ break;
+ }
+
+ case 'realm': {
+ this._configuration.realm = String(value);
+ break;
+ }
+
+ case 'ha1': {
+ this._configuration.ha1 = String(value);
+ // Delete the plain SIP password.
+ this._configuration.password = null;
+ break;
+ }
+
+ case 'authorization_jwt': {
+ this._configuration.authorization_jwt = String(value);
+ break;
+ }
+
+ case 'display_name': {
+ this._configuration.display_name = value;
+ break;
+ }
+
+ case 'extra_headers': {
+ this._configuration.extra_headers = value;
+ break;
+ }
+
+ default: {
+ logger.warn('set() | cannot set "%s" parameter in runtime', parameter);
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ // ==========================
+ // Event Handlers.
+ // ==========================
+
+ /**
+ * new Transaction
+ */
+ newTransaction(transaction) {
+ this._transactions[transaction.type][transaction.id] = transaction;
+ this.emit('newTransaction', {
+ transaction,
+ });
+ }
+
+ /**
+ * Transaction destroyed.
+ */
+ destroyTransaction(transaction) {
+ delete this._transactions[transaction.type][transaction.id];
+ this.emit('transactionDestroyed', {
+ transaction,
+ });
+ }
+
+ /**
+ * new Dialog
+ */
+ newDialog(dialog) {
+ this._dialogs[dialog.id] = dialog;
+ }
+
+ /**
+ * Dialog destroyed.
+ */
+ destroyDialog(dialog) {
+ delete this._dialogs[dialog.id];
+ }
+
+ /**
+ * new Message
+ */
+ newMessage(message, data) {
+ this._applicants[message] = message;
+ this.emit('newMessage', data);
+ }
+
+ /**
+ * new Options
+ */
+ newOptions(message, data) {
+ this._applicants[message] = message;
+ this.emit('newOptions', data);
+ }
+
+ /**
+ * Message destroyed.
+ */
+ destroyMessage(message) {
+ delete this._applicants[message];
+ }
+
+ /**
+ * new RTCSession
+ */
+ newRTCSession(session, data) {
+ this._sessions[session.id] = session;
+ this.emit('newRTCSession', data);
+ }
+
+ /**
+ * RTCSession destroyed.
+ */
+ destroyRTCSession(session) {
+ delete this._sessions[session.id];
+ }
+
+ /**
+ * Registered
+ */
+ registered(data) {
+ this.emit('registered', data);
+ }
+
+ /**
+ * Unregistered
+ */
+ unregistered(data) {
+ this.emit('unregistered', data);
+ }
+
+ /**
+ * Registration Failed
+ */
+ registrationFailed(data) {
+ this.emit('registrationFailed', data);
+ }
+
+ // =========================
+ // ReceiveRequest.
+ // =========================
+
+ /**
+ * Request reception
+ */
+ receiveRequest(request) {
+ const method = request.method;
+
+ // Check that request URI points to us.
+ if (
+ request.ruri.user !== this._configuration.uri.user &&
+ request.ruri.user !== this._contact.uri.user
+ ) {
+ logger.debug('Request-URI does not point to us');
+ if (request.method !== JsSIP_C.ACK) {
+ request.reply_sl(404);
+ }
+
+ return;
+ }
+
+ // Check request URI scheme.
+ if (request.ruri.scheme === JsSIP_C.SIPS) {
+ request.reply_sl(416);
+
+ return;
+ }
+
+ // Check transaction.
+ if (Transactions.checkTransaction(this, request)) {
+ return;
+ }
+
+ // Create the server transaction.
+ if (method === JsSIP_C.INVITE) {
+ /* eslint-disable no-new */
+ new Transactions.InviteServerTransaction(this, this._transport, request);
+ /* eslint-enable no-new */
+ } else if (method !== JsSIP_C.ACK && method !== JsSIP_C.CANCEL) {
+ /* eslint-disable no-new */
+ new Transactions.NonInviteServerTransaction(
+ this,
+ this._transport,
+ request
+ );
+ /* eslint-enable no-new */
+ }
+
+ /* RFC3261 12.2.2
+ * Requests that do not change in any way the state of a dialog may be
+ * received within a dialog (for example, an OPTIONS request).
+ * They are processed as if they had been received outside the dialog.
+ */
+ if (method === JsSIP_C.OPTIONS) {
+ if (this.listeners('newOptions').length === 0) {
+ request.reply(200);
+
+ return;
+ }
+
+ const message = new Options(this);
+
+ message.init_incoming(request);
+ } else if (method === JsSIP_C.MESSAGE) {
+ if (this.listeners('newMessage').length === 0) {
+ request.reply(405);
+
+ return;
+ }
+ const message = new Message(this);
+
+ message.init_incoming(request);
+ } else if (method === JsSIP_C.SUBSCRIBE) {
+ if (this.listeners('newSubscribe').length === 0) {
+ request.reply(405);
+
+ return;
+ }
+ } else if (method === JsSIP_C.INVITE) {
+ // Initial INVITE.
+ if (!request.to_tag && this.listeners('newRTCSession').length === 0) {
+ request.reply(405);
+
+ return;
+ }
+ }
+
+ let dialog;
+ let session;
+
+ // Initial Request.
+ if (!request.to_tag) {
+ switch (method) {
+ case JsSIP_C.INVITE: {
+ // eslint-disable-next-line no-undef
+ if (window.RTCPeerConnection) {
+ // TODO
+ if (request.hasHeader('replaces')) {
+ const replaces = request.replaces;
+
+ dialog = this._findDialog(
+ replaces.call_id,
+ replaces.from_tag,
+ replaces.to_tag
+ );
+ if (dialog) {
+ session = dialog.owner;
+ if (!session.isEnded()) {
+ session.receiveRequest(request);
+ } else {
+ request.reply(603);
+ }
+ } else {
+ request.reply(481);
+ }
+ } else {
+ session = new RTCSession(this);
+ session.init_incoming(request);
+ }
+ } else {
+ logger.warn('INVITE received but WebRTC is not supported');
+ request.reply(488);
+ }
+ break;
+ }
+ case JsSIP_C.BYE: {
+ // Out of dialog BYE received.
+ request.reply(481);
+ break;
+ }
+ case JsSIP_C.CANCEL: {
+ session = this._findSession(request);
+ if (session) {
+ session.receiveRequest(request);
+ } else {
+ logger.debug('received CANCEL request for a non existent session');
+ }
+ break;
+ }
+ case JsSIP_C.ACK: {
+ /* Absorb it.
+ * ACK request without a corresponding Invite Transaction
+ * and without To tag.
+ */
+ break;
+ }
+ case JsSIP_C.NOTIFY: {
+ // Receive new sip event.
+ this.emit('sipEvent', {
+ event: request.event,
+ request,
+ });
+ request.reply(200);
+ break;
+ }
+ case JsSIP_C.SUBSCRIBE: {
+ Notifier.init_incoming(request, () => {
+ this.emit('newSubscribe', {
+ event: request.event,
+ request,
+ });
+ });
+ break;
+ }
+ default: {
+ request.reply(405);
+ break;
+ }
+ }
+ }
+ // In-dialog request.
+ else {
+ dialog = this._findDialog(
+ request.call_id,
+ request.from_tag,
+ request.to_tag
+ );
+
+ if (dialog) {
+ dialog.receiveRequest(request);
+ } else if (method === JsSIP_C.NOTIFY) {
+ session = this._findSession(request);
+ if (session) {
+ session.receiveRequest(request);
+ } else {
+ logger.debug(
+ 'received NOTIFY request for a non existent subscription'
+ );
+ request.reply(481, 'Subscription does not exist');
+ }
+ } else if (method !== JsSIP_C.ACK) {
+ /* RFC3261 12.2.2
+ * Request with to tag, but no matching dialog found.
+ * Exception: ACK for an Invite request for which a dialog has not
+ * been created.
+ */
+ request.reply(481);
+ }
+ }
+ }
+
+ // =================
+ // Utils.
+ // =================
+
+ /**
+ * Get the session to which the request belongs to, if any.
+ */
+ _findSession({ call_id, from_tag, to_tag }) {
+ const sessionIDa = call_id + from_tag;
+ const sessionA = this._sessions[sessionIDa];
+ const sessionIDb = call_id + to_tag;
+ const sessionB = this._sessions[sessionIDb];
+
+ if (sessionA) {
+ return sessionA;
+ } else if (sessionB) {
+ return sessionB;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Get the dialog to which the request belongs to, if any.
+ */
+ _findDialog(call_id, from_tag, to_tag) {
+ let id = call_id + from_tag + to_tag;
+ let dialog = this._dialogs[id];
+
+ if (dialog) {
+ return dialog;
+ } else {
+ id = call_id + to_tag + from_tag;
+ dialog = this._dialogs[id];
+ if (dialog) {
+ return dialog;
+ } else {
+ return null;
+ }
+ }
+ }
+
+ _loadConfig(configuration) {
+ // Check and load the given configuration.
+ // This can throw.
+ config.load(this._configuration, configuration);
+
+ // Post Configuration Process.
+
+ // Allow passing 0 number as display_name.
+ if (this._configuration.display_name === 0) {
+ this._configuration.display_name = '0';
+ }
+
+ // Instance-id for GRUU.
+ if (!this._configuration.instance_id) {
+ this._configuration.instance_id = Utils.newUUID();
+ }
+
+ // Jssip_id instance parameter. Static random tag of length 5.
+ this._configuration.jssip_id = Utils.createRandomToken(5);
+
+ // String containing this._configuration.uri without scheme and user.
+ const hostport_params = this._configuration.uri.clone();
+
+ hostport_params.user = null;
+ this._configuration.hostport_params = hostport_params
+ .toString()
+ .replace(/^sip:/i, '');
+
+ // Transport.
+ try {
+ this._transport = new Transport(this._configuration.sockets, {
+ // Recovery options.
+ max_interval: this._configuration.connection_recovery_max_interval,
+ min_interval: this._configuration.connection_recovery_min_interval,
+ });
+
+ // Transport event callbacks.
+ this._transport.onconnecting = onTransportConnecting.bind(this);
+ this._transport.onconnect = onTransportConnect.bind(this);
+ this._transport.ondisconnect = onTransportDisconnect.bind(this);
+ this._transport.ondata = onTransportData.bind(this);
+ } catch (error) {
+ logger.warn(error);
+ throw new Exceptions.ConfigurationError(
+ 'sockets',
+ this._configuration.sockets
+ );
+ }
+
+ // Remove sockets instance from configuration object.
+ delete this._configuration.sockets;
+
+ // Check whether authorization_user is explicitly defined.
+ // Take 'this._configuration.uri.user' value if not.
+ if (!this._configuration.authorization_user) {
+ this._configuration.authorization_user = this._configuration.uri.user;
+ }
+
+ // If no 'registrar_server' is set use the 'uri' value without user portion and
+ // without URI params/headers.
+ if (!this._configuration.registrar_server) {
+ const registrar_server = this._configuration.uri.clone();
+
+ registrar_server.user = null;
+ registrar_server.clearParams();
+ registrar_server.clearHeaders();
+ this._configuration.registrar_server = registrar_server;
+ }
+
+ // User no_answer_timeout.
+ this._configuration.no_answer_timeout *= 1000;
+
+ // Via Host.
+ if (this._configuration.contact_uri) {
+ this._configuration.via_host = this._configuration.contact_uri.host;
+ }
+
+ // Contact URI.
+ else {
+ this._configuration.contact_uri = new URI(
+ 'sip',
+ Utils.createRandomToken(8),
+ this._configuration.via_host,
+ null,
+ { transport: 'ws' }
+ );
+ }
+
+ this._contact = {
+ pub_gruu: null,
+ temp_gruu: null,
+ uri: this._configuration.contact_uri,
+ toString(options = {}) {
+ const anonymous = options.anonymous || null;
+ const outbound = options.outbound || null;
+ let contact = '<';
+
+ if (anonymous) {
+ contact +=
+ this.temp_gruu || 'sip:anonymous@anonymous.invalid;transport=ws';
+ } else {
+ contact += this.pub_gruu || this.uri.toString();
+ }
+
+ if (outbound && (anonymous ? !this.temp_gruu : !this.pub_gruu)) {
+ contact += ';ob';
+ }
+
+ contact += '>';
+
+ return contact;
+ },
+ };
+
+ // Seal the configuration.
+ const writable_parameters = [
+ 'authorization_user',
+ 'password',
+ 'realm',
+ 'ha1',
+ 'authorization_jwt',
+ 'display_name',
+ 'register',
+ 'extra_headers',
+ ];
+
+ for (const parameter in this._configuration) {
+ if (
+ Object.prototype.hasOwnProperty.call(this._configuration, parameter)
+ ) {
+ if (writable_parameters.indexOf(parameter) !== -1) {
+ Object.defineProperty(this._configuration, parameter, {
+ writable: true,
+ configurable: false,
+ });
+ } else {
+ Object.defineProperty(this._configuration, parameter, {
+ writable: false,
+ configurable: false,
+ });
+ }
+ }
+ }
+
+ logger.debug('configuration parameters after validation:');
+ for (const parameter in this._configuration) {
+ // Only show the user user configurable parameters.
+ if (Object.prototype.hasOwnProperty.call(config.settings, parameter)) {
+ switch (parameter) {
+ case 'uri':
+ case 'registrar_server': {
+ logger.debug(`- ${parameter}: ${this._configuration[parameter]}`);
+ break;
+ }
+ case 'password':
+ case 'ha1':
+ case 'authorization_jwt': {
+ logger.debug(`- ${parameter}: NOT SHOWN`);
+ break;
+ }
+ default: {
+ logger.debug(
+ `- ${parameter}: ${JSON.stringify(this._configuration[parameter])}`
+ );
+ }
+ }
+ }
+ }
+
+ return;
+ }
};
/**
@@ -1043,117 +964,103 @@ module.exports = class UA extends EventEmitter
*/
// Transport connecting event.
-function onTransportConnecting(data)
-{
- this.emit('connecting', data);
+function onTransportConnecting(data) {
+ this.emit('connecting', data);
}
// Transport connected event.
-function onTransportConnect(data)
-{
- if (this._status === C.STATUS_USER_CLOSED)
- {
- return;
- }
-
- this._status = C.STATUS_READY;
- this._error = null;
-
- this.emit('connected', data);
-
- if (this._dynConfiguration.register)
- {
- this._registrator.register();
- }
+function onTransportConnect(data) {
+ if (this._status === C.STATUS_USER_CLOSED) {
+ return;
+ }
+
+ this._status = C.STATUS_READY;
+ this._error = null;
+
+ this.emit('connected', data);
+
+ if (this._dynConfiguration.register) {
+ this._registrator.register();
+ }
}
// Transport disconnected event.
-function onTransportDisconnect(data)
-{
- // Run _onTransportError_ callback on every client transaction using _transport_.
- const client_transactions = [ 'nict', 'ict', 'nist', 'ist' ];
-
- for (const type of client_transactions)
- {
- for (const id in this._transactions[type])
- {
- if (Object.prototype.hasOwnProperty.call(this._transactions[type], id))
- {
- this._transactions[type][id].onTransportError();
- }
- }
- }
-
- this.emit('disconnected', data);
-
- // Call registrator _onTransportClosed_.
- this._registrator.onTransportClosed();
-
- if (this._status !== C.STATUS_USER_CLOSED)
- {
- this._status = C.STATUS_NOT_READY;
- this._error = C.NETWORK_ERROR;
- }
+function onTransportDisconnect(data) {
+ // Run _onTransportError_ callback on every client transaction using _transport_.
+ const client_transactions = ['nict', 'ict', 'nist', 'ist'];
+
+ for (const type of client_transactions) {
+ for (const id in this._transactions[type]) {
+ if (Object.prototype.hasOwnProperty.call(this._transactions[type], id)) {
+ this._transactions[type][id].onTransportError();
+ }
+ }
+ }
+
+ this.emit('disconnected', data);
+
+ // Call registrator _onTransportClosed_.
+ this._registrator.onTransportClosed();
+
+ if (this._status !== C.STATUS_USER_CLOSED) {
+ this._status = C.STATUS_NOT_READY;
+ this._error = C.NETWORK_ERROR;
+ }
}
// Transport data event.
-function onTransportData(data)
-{
- const transport = data.transport;
- let message = data.message;
-
- message = Parser.parseMessage(message, this);
-
- if (!message)
- {
- return;
- }
-
- if (this._status === C.STATUS_USER_CLOSED &&
- message instanceof SIPMessage.IncomingRequest)
- {
- return;
- }
-
- // Do some sanity check.
- if (!sanityCheck(message, this, transport))
- {
- return;
- }
-
- if (message instanceof SIPMessage.IncomingRequest)
- {
- message.transport = transport;
- this.receiveRequest(message);
- }
- else if (message instanceof SIPMessage.IncomingResponse)
- {
- /* Unike stated in 18.1.2, if a response does not match
- * any transaction, it is discarded here and no passed to the core
- * in order to be discarded there.
- */
-
- let transaction;
-
- switch (message.method)
- {
- case JsSIP_C.INVITE:
- transaction = this._transactions.ict[message.via_branch];
- if (transaction)
- {
- transaction.receiveResponse(message);
- }
- break;
- case JsSIP_C.ACK:
- // Just in case ;-).
- break;
- default:
- transaction = this._transactions.nict[message.via_branch];
- if (transaction)
- {
- transaction.receiveResponse(message);
- }
- break;
- }
- }
+function onTransportData(data) {
+ const transport = data.transport;
+ let message = data.message;
+
+ message = Parser.parseMessage(message, this);
+
+ if (!message) {
+ return;
+ }
+
+ if (
+ this._status === C.STATUS_USER_CLOSED &&
+ message instanceof SIPMessage.IncomingRequest
+ ) {
+ return;
+ }
+
+ // Do some sanity check.
+ if (!sanityCheck(message, this, transport)) {
+ return;
+ }
+
+ if (message instanceof SIPMessage.IncomingRequest) {
+ message.transport = transport;
+ this.receiveRequest(message);
+ } else if (message instanceof SIPMessage.IncomingResponse) {
+ /* Unike stated in 18.1.2, if a response does not match
+ * any transaction, it is discarded here and no passed to the core
+ * in order to be discarded there.
+ */
+
+ let transaction;
+
+ switch (message.method) {
+ case JsSIP_C.INVITE: {
+ transaction = this._transactions.ict[message.via_branch];
+ if (transaction) {
+ transaction.receiveResponse(message);
+ }
+ break;
+ }
+ case JsSIP_C.ACK: {
+ // Just in case ;-).
+ break;
+ }
+ default: {
+ transaction = this._transactions.nict[message.via_branch];
+ if (transaction) {
+ transaction.receiveResponse(message);
+ }
+ break;
+ }
+ }
+ }
}
diff --git a/src/URI.d.ts b/src/URI.d.ts
index 66d3a09..05fcd0d 100644
--- a/src/URI.d.ts
+++ b/src/URI.d.ts
@@ -1,4 +1,4 @@
-import {Grammar} from './Grammar'
+import { Grammar } from './Grammar';
export type URIScheme = 'sip' | string;
@@ -7,38 +7,45 @@ export type Parameters = Record<string, string | null>;
export type Headers = Record<string, string | string[]>;
export class URI {
- scheme: URIScheme
- user: string
- host: string
- port: number
+ scheme: URIScheme;
+ user: string;
+ host: string;
+ port: number;
- constructor(scheme: URIScheme, user: string, host: string, port?: number, parameters?: Parameters, headers?: Headers);
+ constructor(
+ scheme: URIScheme,
+ user: string,
+ host: string,
+ port?: number,
+ parameters?: Parameters,
+ headers?: Headers
+ );
- setParam(key: string, value?: string): void;
+ setParam(key: string, value?: string): void;
- getParam<T = unknown>(key: string): T;
+ getParam<T = unknown>(key: string): T;
- hasParam(key: string): boolean;
+ hasParam(key: string): boolean;
- deleteParam(key: string): void;
+ deleteParam(key: string): void;
- clearParams(): void;
+ clearParams(): void;
- setHeader(key: string, value: string | string[]): void;
+ setHeader(key: string, value: string | string[]): void;
- getHeader(key: string): string[];
+ getHeader(key: string): string[];
- hasHeader(key: string): boolean;
+ hasHeader(key: string): boolean;
- deleteHeader(key: string): void;
+ deleteHeader(key: string): void;
- clearHeaders(): void;
+ clearHeaders(): void;
- clone(): this;
+ clone(): this;
- toString(): string;
+ toString(): string;
- toAor(): string;
+ toAor(): string;
- static parse(uri: string): Grammar | undefined;
+ static parse(uri: string): Grammar | undefined;
}
diff --git a/src/URI.js b/src/URI.js
index b8ac6fc..8e629a9 100644
--- a/src/URI.js
+++ b/src/URI.js
@@ -11,254 +11,214 @@ const Grammar = require('./Grammar');
* -param {Object} [headers]
*
*/
-module.exports = class URI
-{
- /**
- * Parse the given string and returns a JsSIP.URI instance or undefined if
- * it is an invalid URI.
- */
- static parse(uri)
- {
- uri = Grammar.parse(uri, 'SIP_URI');
-
- if (uri !== -1)
- {
- return uri;
- }
- else
- {
- return undefined;
- }
- }
-
- constructor(scheme, user, host, port, parameters = {}, headers = {})
- {
- // Checks.
- if (!host)
- {
- throw new TypeError('missing or invalid "host" parameter');
- }
-
- // Initialize parameters.
- this._parameters = {};
- this._headers = {};
-
- this._scheme = scheme || JsSIP_C.SIP;
- this._user = user;
- this._host = host;
- this._port = port;
-
- for (const param in parameters)
- {
- if (Object.prototype.hasOwnProperty.call(parameters, param))
- {
- this.setParam(param, parameters[param]);
- }
- }
-
- for (const header in headers)
- {
- if (Object.prototype.hasOwnProperty.call(headers, header))
- {
- this.setHeader(header, headers[header]);
- }
- }
- }
-
- get scheme()
- {
- return this._scheme;
- }
-
- set scheme(value)
- {
- this._scheme = value.toLowerCase();
- }
-
- get user()
- {
- return this._user;
- }
-
- set user(value)
- {
- this._user = value;
- }
-
- get host()
- {
- return this._host;
- }
-
- set host(value)
- {
- this._host = value.toLowerCase();
- }
-
- get port()
- {
- return this._port;
- }
-
- set port(value)
- {
- this._port = value === 0 ? value : (parseInt(value, 10) || null);
- }
-
- setParam(key, value)
- {
- if (key)
- {
- this._parameters[key.toLowerCase()] = (typeof value === 'undefined' || value === null) ? null : value.toString();
- }
- }
-
- getParam(key)
- {
- if (key)
- {
- return this._parameters[key.toLowerCase()];
- }
- }
-
- hasParam(key)
- {
- if (key)
- {
- return (this._parameters.hasOwnProperty(key.toLowerCase()) && true) || false;
- }
- }
-
- deleteParam(parameter)
- {
- parameter = parameter.toLowerCase();
- if (this._parameters.hasOwnProperty(parameter))
- {
- const value = this._parameters[parameter];
-
- delete this._parameters[parameter];
-
- return value;
- }
- }
-
- clearParams()
- {
- this._parameters = {};
- }
-
- setHeader(name, value)
- {
- this._headers[Utils.headerize(name)] = (Array.isArray(value)) ? value : [ value ];
- }
-
- getHeader(name)
- {
- if (name)
- {
- return this._headers[Utils.headerize(name)];
- }
- }
-
- hasHeader(name)
- {
- if (name)
- {
- return (this._headers.hasOwnProperty(Utils.headerize(name)) && true) || false;
- }
- }
-
- deleteHeader(header)
- {
- header = Utils.headerize(header);
- if (this._headers.hasOwnProperty(header))
- {
- const value = this._headers[header];
-
- delete this._headers[header];
-
- return value;
- }
- }
-
- clearHeaders()
- {
- this._headers = {};
- }
-
- clone()
- {
- return new URI(
- this._scheme,
- this._user,
- this._host,
- this._port,
- JSON.parse(JSON.stringify(this._parameters)),
- JSON.parse(JSON.stringify(this._headers)));
- }
-
- toString()
- {
- const headers = [];
-
- let uri = `${this._scheme}:`;
-
- if (this._user)
- {
- uri += `${Utils.escapeUser(this._user)}@`;
- }
- uri += this._host;
- if (this._port || this._port === 0)
- {
- uri += `:${this._port}`;
- }
-
- for (const parameter in this._parameters)
- {
- if (Object.prototype.hasOwnProperty.call(this._parameters, parameter))
- {
- uri += `;${parameter}`;
-
- if (this._parameters[parameter] !== null)
- {
- uri += `=${this._parameters[parameter]}`;
- }
- }
- }
-
- for (const header in this._headers)
- {
- if (Object.prototype.hasOwnProperty.call(this._headers, header))
- {
- for (const item of this._headers[header])
- {
- headers.push(`${header}=${item}`);
- }
- }
- }
-
- if (headers.length > 0)
- {
- uri += `?${headers.join('&')}`;
- }
-
- return uri;
- }
-
- toAor(show_port)
- {
- let aor = `${this._scheme}:`;
-
- if (this._user)
- {
- aor += `${Utils.escapeUser(this._user)}@`;
- }
- aor += this._host;
- if (show_port && (this._port || this._port === 0))
- {
- aor += `:${this._port}`;
- }
-
- return aor;
- }
+module.exports = class URI {
+ /**
+ * Parse the given string and returns a JsSIP.URI instance or undefined if
+ * it is an invalid URI.
+ */
+ static parse(uri) {
+ uri = Grammar.parse(uri, 'SIP_URI');
+
+ if (uri !== -1) {
+ return uri;
+ } else {
+ return undefined;
+ }
+ }
+
+ constructor(scheme, user, host, port, parameters = {}, headers = {}) {
+ // Checks.
+ if (!host) {
+ throw new TypeError('missing or invalid "host" parameter');
+ }
+
+ // Initialize parameters.
+ this._parameters = {};
+ this._headers = {};
+
+ this._scheme = scheme || JsSIP_C.SIP;
+ this._user = user;
+ this._host = host;
+ this._port = port;
+
+ for (const param in parameters) {
+ if (Object.prototype.hasOwnProperty.call(parameters, param)) {
+ this.setParam(param, parameters[param]);
+ }
+ }
+
+ for (const header in headers) {
+ if (Object.prototype.hasOwnProperty.call(headers, header)) {
+ this.setHeader(header, headers[header]);
+ }
+ }
+ }
+
+ get scheme() {
+ return this._scheme;
+ }
+
+ set scheme(value) {
+ this._scheme = value.toLowerCase();
+ }
+
+ get user() {
+ return this._user;
+ }
+
+ set user(value) {
+ this._user = value;
+ }
+
+ get host() {
+ return this._host;
+ }
+
+ set host(value) {
+ this._host = value.toLowerCase();
+ }
+
+ get port() {
+ return this._port;
+ }
+
+ set port(value) {
+ this._port = value === 0 ? value : parseInt(value, 10) || null;
+ }
+
+ setParam(key, value) {
+ if (key) {
+ this._parameters[key.toLowerCase()] =
+ typeof value === 'undefined' || value === null
+ ? null
+ : value.toString();
+ }
+ }
+
+ getParam(key) {
+ if (key) {
+ return this._parameters[key.toLowerCase()];
+ }
+ }
+
+ hasParam(key) {
+ if (key) {
+ return (
+ (this._parameters.hasOwnProperty(key.toLowerCase()) && true) || false
+ );
+ }
+ }
+
+ deleteParam(parameter) {
+ parameter = parameter.toLowerCase();
+ if (this._parameters.hasOwnProperty(parameter)) {
+ const value = this._parameters[parameter];
+
+ delete this._parameters[parameter];
+
+ return value;
+ }
+ }
+
+ clearParams() {
+ this._parameters = {};
+ }
+
+ setHeader(name, value) {
+ this._headers[Utils.headerize(name)] = Array.isArray(value)
+ ? value
+ : [value];
+ }
+
+ getHeader(name) {
+ if (name) {
+ return this._headers[Utils.headerize(name)];
+ }
+ }
+
+ hasHeader(name) {
+ if (name) {
+ return (
+ (this._headers.hasOwnProperty(Utils.headerize(name)) && true) || false
+ );
+ }
+ }
+
+ deleteHeader(header) {
+ header = Utils.headerize(header);
+ if (this._headers.hasOwnProperty(header)) {
+ const value = this._headers[header];
+
+ delete this._headers[header];
+
+ return value;
+ }
+ }
+
+ clearHeaders() {
+ this._headers = {};
+ }
+
+ clone() {
+ return new URI(
+ this._scheme,
+ this._user,
+ this._host,
+ this._port,
+ JSON.parse(JSON.stringify(this._parameters)),
+ JSON.parse(JSON.stringify(this._headers))
+ );
+ }
+
+ toString() {
+ const headers = [];
+
+ let uri = `${this._scheme}:`;
+
+ if (this._user) {
+ uri += `${Utils.escapeUser(this._user)}@`;
+ }
+ uri += this._host;
+ if (this._port || this._port === 0) {
+ uri += `:${this._port}`;
+ }
+
+ for (const parameter in this._parameters) {
+ if (Object.prototype.hasOwnProperty.call(this._parameters, parameter)) {
+ uri += `;${parameter}`;
+
+ if (this._parameters[parameter] !== null) {
+ uri += `=${this._parameters[parameter]}`;
+ }
+ }
+ }
+
+ for (const header in this._headers) {
+ if (Object.prototype.hasOwnProperty.call(this._headers, header)) {
+ for (const item of this._headers[header]) {
+ headers.push(`${header}=${item}`);
+ }
+ }
+ }
+
+ if (headers.length > 0) {
+ uri += `?${headers.join('&')}`;
+ }
+
+ return uri;
+ }
+
+ toAor(show_port) {
+ let aor = `${this._scheme}:`;
+
+ if (this._user) {
+ aor += `${Utils.escapeUser(this._user)}@`;
+ }
+ aor += this._host;
+ if (show_port && (this._port || this._port === 0)) {
+ aor += `:${this._port}`;
+ }
+
+ return aor;
+ }
};
diff --git a/src/Utils.d.ts b/src/Utils.d.ts
index 3e73f88..051a04c 100644
--- a/src/Utils.d.ts
+++ b/src/Utils.d.ts
@@ -1,5 +1,5 @@
-import {URI} from './URI'
-import {causes} from './Constants'
+import { URI } from './URI';
+import { causes } from './Constants';
export function str_utf8_length(str: string): number;
@@ -9,6 +9,7 @@ export function isDecimal(num: unknown): num is number;
export function isEmpty(value: unknown): boolean;
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function hasMethods(obj: any, ...methodNames: string[]): boolean;
export function newTag(): string;
@@ -19,7 +20,10 @@ export function hostType(host: string): string;
export function escapeUser(user: string): string;
-export function normalizeTarget(target: URI | string, domain?: string): URI | undefined;
+export function normalizeTarget(
+ target: URI | string,
+ domain?: string
+): URI | undefined;
export function headerize(str: string): string;
diff --git a/src/Utils.js b/src/Utils.js
index 065873e..1ed5aac 100644
--- a/src/Utils.js
+++ b/src/Utils.js
@@ -2,551 +2,494 @@ const JsSIP_C = require('./Constants');
const URI = require('./URI');
const Grammar = require('./Grammar');
-exports.str_utf8_length = (string) => unescape(encodeURIComponent(string)).length;
+exports.str_utf8_length = string => unescape(encodeURIComponent(string)).length;
// Used by 'hasMethods'.
-const isFunction = exports.isFunction = (fn) =>
-{
- if (fn !== undefined)
- {
- return (Object.prototype.toString.call(fn) === '[object Function]')? true : false;
- }
- else
- {
- return false;
- }
+const isFunction = (exports.isFunction = fn => {
+ if (fn !== undefined) {
+ return Object.prototype.toString.call(fn) === '[object Function]'
+ ? true
+ : false;
+ } else {
+ return false;
+ }
+});
+
+exports.isString = str => {
+ if (str !== undefined) {
+ return Object.prototype.toString.call(str) === '[object String]'
+ ? true
+ : false;
+ } else {
+ return false;
+ }
};
-exports.isString = (str) =>
-{
- if (str !== undefined)
- {
- return (Object.prototype.toString.call(str) === '[object String]')? true : false;
- }
- else
- {
- return false;
- }
-};
-
-exports.isDecimal = (num) => !isNaN(num) && (parseFloat(num) === parseInt(num, 10));
+exports.isDecimal = num => !isNaN(num) && parseFloat(num) === parseInt(num, 10);
-exports.isEmpty = (value) =>
-{
- return (value === null ||
- value === '' ||
- value === undefined ||
- (Array.isArray(value) && value.length === 0) ||
- (typeof(value) === 'number' && isNaN(value)));
+exports.isEmpty = value => {
+ return (
+ value === null ||
+ value === '' ||
+ value === undefined ||
+ (Array.isArray(value) && value.length === 0) ||
+ (typeof value === 'number' && isNaN(value))
+ );
};
-exports.hasMethods = function(obj, ...methodNames)
-{
- for (const methodName of methodNames)
- {
- if (isFunction(obj[methodName]))
- {
- return false;
- }
- }
-
- return true;
+exports.hasMethods = function (obj, ...methodNames) {
+ for (const methodName of methodNames) {
+ if (isFunction(obj[methodName])) {
+ return false;
+ }
+ }
+
+ return true;
};
// Used by 'newTag'.
-const createRandomToken = exports.createRandomToken = (size, base = 32) =>
-{
- let i, r, token = '';
+const createRandomToken = (exports.createRandomToken = (size, base = 32) => {
+ let i,
+ r,
+ token = '';
- for (i=0; i < size; i++)
- {
- r = (Math.random() * base) | 0;
- token += r.toString(base);
- }
+ for (i = 0; i < size; i++) {
+ r = (Math.random() * base) | 0;
+ token += r.toString(base);
+ }
- return token;
-};
+ return token;
+});
exports.newTag = () => createRandomToken(10);
// https://stackoverflow.com/users/109538/broofa.
-exports.newUUID = () =>
-{
- const UUID = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) =>
- {
- const r = Math.random()*16|0, v = c === 'x' ? r : ((r&0x3)|0x8);
-
+exports.newUUID = () => {
+ const UUID = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
+ const r = (Math.random() * 16) | 0,
+ v = c === 'x' ? r : (r & 0x3) | 0x8;
- return v.toString(16);
- });
+ return v.toString(16);
+ });
- return UUID;
+ return UUID;
};
-exports.hostType = (host) =>
-{
- if (!host)
- {
- return;
- }
- else
- {
- host = Grammar.parse(host, 'host');
- if (host !== -1)
- {
- return host.host_type;
- }
- }
+exports.hostType = host => {
+ if (!host) {
+ return;
+ } else {
+ host = Grammar.parse(host, 'host');
+ if (host !== -1) {
+ return host.host_type;
+ }
+ }
};
/**
-* Hex-escape a SIP URI user.
-* Don't hex-escape ':' (%3A), '+' (%2B), '?' (%3F"), '/' (%2F).
-*
-* Used by 'normalizeTarget'.
-*/
-const escapeUser = exports.escapeUser = (user) =>
- encodeURIComponent(decodeURIComponent(user))
- .replace(/%3A/ig, ':')
- .replace(/%2B/ig, '+')
- .replace(/%3F/ig, '?')
- .replace(/%2F/ig, '/');
+ * Hex-escape a SIP URI user.
+ * Don't hex-escape ':' (%3A), '+' (%2B), '?' (%3F"), '/' (%2F).
+ *
+ * Used by 'normalizeTarget'.
+ */
+const escapeUser = (exports.escapeUser = user =>
+ encodeURIComponent(decodeURIComponent(user))
+ .replace(/%3A/gi, ':')
+ .replace(/%2B/gi, '+')
+ .replace(/%3F/gi, '?')
+ .replace(/%2F/gi, '/'));
/**
-* Normalize SIP URI.
-* NOTE: It does not allow a SIP URI without username.
-* Accepts 'sip', 'sips' and 'tel' URIs and convert them into 'sip'.
-* Detects the domain part (if given) and properly hex-escapes the user portion.
-* If the user portion has only 'tel' number symbols the user portion is clean of 'tel' visual separators.
-*/
-exports.normalizeTarget = (target, domain) =>
-{
- // If no target is given then raise an error.
- if (!target)
- {
- return;
- // If a URI instance is given then return it.
- }
- else if (target instanceof URI)
- {
- return target;
-
- // If a string is given split it by '@':
- // - Last fragment is the desired domain.
- // - Otherwise append the given domain argument.
- }
- else if (typeof target === 'string')
- {
- const target_array = target.split('@');
- let target_user;
- let target_domain;
-
- switch (target_array.length)
- {
- case 1:
- if (!domain)
- {
- return;
- }
- target_user = target;
- target_domain = domain;
- break;
- case 2:
- target_user = target_array[0];
- target_domain = target_array[1];
- break;
- default:
- target_user = target_array.slice(0, target_array.length-1).join('@');
- target_domain = target_array[target_array.length-1];
- }
-
- // Remove the URI scheme (if present).
- target_user = target_user.replace(/^(sips?|tel):/i, '');
-
- // Remove 'tel' visual separators if the user portion just contains 'tel' number symbols.
- if (/^[-.()]*\+?[0-9\-.()]+$/.test(target_user))
- {
- target_user = target_user.replace(/[-.()]/g, '');
- }
-
- // Build the complete SIP URI.
- target = `${JsSIP_C.SIP}:${escapeUser(target_user)}@${target_domain}`;
-
- // Finally parse the resulting URI.
- let uri;
-
- if ((uri = URI.parse(target)))
- {
- return uri;
- }
- else
- {
- return;
- }
- }
- else
- {
- return;
- }
+ * Normalize SIP URI.
+ * NOTE: It does not allow a SIP URI without username.
+ * Accepts 'sip', 'sips' and 'tel' URIs and convert them into 'sip'.
+ * Detects the domain part (if given) and properly hex-escapes the user portion.
+ * If the user portion has only 'tel' number symbols the user portion is clean of 'tel' visual separators.
+ */
+exports.normalizeTarget = (target, domain) => {
+ // If no target is given then raise an error.
+ if (!target) {
+ return;
+ // If a URI instance is given then return it.
+ } else if (target instanceof URI) {
+ return target;
+
+ // If a string is given split it by '@':
+ // - Last fragment is the desired domain.
+ // - Otherwise append the given domain argument.
+ } else if (typeof target === 'string') {
+ const target_array = target.split('@');
+ let target_user;
+ let target_domain;
+
+ switch (target_array.length) {
+ case 1: {
+ if (!domain) {
+ return;
+ }
+ target_user = target;
+ target_domain = domain;
+ break;
+ }
+ case 2: {
+ target_user = target_array[0];
+ target_domain = target_array[1];
+ break;
+ }
+ default: {
+ target_user = target_array.slice(0, target_array.length - 1).join('@');
+ target_domain = target_array[target_array.length - 1];
+ }
+ }
+
+ // Remove the URI scheme (if present).
+ target_user = target_user.replace(/^(sips?|tel):/i, '');
+
+ // Remove 'tel' visual separators if the user portion just contains 'tel' number symbols.
+ if (/^[-.()]*\+?[0-9\-.()]+$/.test(target_user)) {
+ target_user = target_user.replace(/[-.()]/g, '');
+ }
+
+ // Build the complete SIP URI.
+ target = `${JsSIP_C.SIP}:${escapeUser(target_user)}@${target_domain}`;
+
+ // Finally parse the resulting URI.
+ let uri;
+
+ if ((uri = URI.parse(target))) {
+ return uri;
+ } else {
+ return;
+ }
+ } else {
+ return;
+ }
};
-exports.headerize = (string) =>
-{
- const exceptions = {
- 'Call-Id' : 'Call-ID',
- 'Cseq' : 'CSeq',
- 'Www-Authenticate' : 'WWW-Authenticate'
- };
-
- const name = string.toLowerCase()
- .replace(/_/g, '-')
- .split('-');
- let hname = '';
- const parts = name.length;
- let part;
-
- for (part = 0; part < parts; part++)
- {
- if (part !== 0)
- {
- hname +='-';
- }
- hname += name[part].charAt(0).toUpperCase()+name[part].substring(1);
- }
- if (exceptions[hname])
- {
- hname = exceptions[hname];
- }
-
- return hname;
+exports.headerize = string => {
+ const exceptions = {
+ 'Call-Id': 'Call-ID',
+ Cseq: 'CSeq',
+ 'Www-Authenticate': 'WWW-Authenticate',
+ };
+
+ const name = string.toLowerCase().replace(/_/g, '-').split('-');
+ let hname = '';
+ const parts = name.length;
+ let part;
+
+ for (part = 0; part < parts; part++) {
+ if (part !== 0) {
+ hname += '-';
+ }
+ hname += name[part].charAt(0).toUpperCase() + name[part].substring(1);
+ }
+ if (exceptions[hname]) {
+ hname = exceptions[hname];
+ }
+
+ return hname;
};
-exports.sipErrorCause = (status_code) =>
-{
- for (const cause in JsSIP_C.SIP_ERROR_CAUSES)
- {
- if (JsSIP_C.SIP_ERROR_CAUSES[cause].indexOf(status_code) !== -1)
- {
- return JsSIP_C.causes[cause];
- }
- }
-
- return JsSIP_C.causes.SIP_FAILURE_CODE;
+exports.sipErrorCause = status_code => {
+ for (const cause in JsSIP_C.SIP_ERROR_CAUSES) {
+ if (JsSIP_C.SIP_ERROR_CAUSES[cause].indexOf(status_code) !== -1) {
+ return JsSIP_C.causes[cause];
+ }
+ }
+
+ return JsSIP_C.causes.SIP_FAILURE_CODE;
};
/**
-* Generate a random Test-Net IP (https://tools.ietf.org/html/rfc5735)
-*/
-exports.getRandomTestNetIP = () =>
-{
- function getOctet(from, to)
- {
- return Math.floor((Math.random() * (to-from+1)) + from);
- }
-
- return `192.0.2.${getOctet(1, 254)}`;
+ * Generate a random Test-Net IP (https://tools.ietf.org/html/rfc5735)
+ */
+exports.getRandomTestNetIP = () => {
+ function getOctet(from, to) {
+ return Math.floor(Math.random() * (to - from + 1) + from);
+ }
+
+ return `192.0.2.${getOctet(1, 254)}`;
};
// MD5 (Message-Digest Algorithm) https://www.webtoolkit.info.
-exports.calculateMD5 = (string) =>
-{
- function rotateLeft(lValue, iShiftBits)
- {
- return (lValue<<iShiftBits) | (lValue>>>(32-iShiftBits));
- }
-
- function addUnsigned(lX, lY)
- {
- const lX8 = (lX & 0x80000000);
- const lY8 = (lY & 0x80000000);
- const lX4 = (lX & 0x40000000);
- const lY4 = (lY & 0x40000000);
- const lResult = (lX & 0x3FFFFFFF)+(lY & 0x3FFFFFFF);
-
- if (lX4 & lY4)
- {
- return (lResult ^ 0x80000000 ^ lX8 ^ lY8);
- }
- if (lX4 | lY4)
- {
- if (lResult & 0x40000000)
- {
- return (lResult ^ 0xC0000000 ^ lX8 ^ lY8);
- }
- else
- {
- return (lResult ^ 0x40000000 ^ lX8 ^ lY8);
- }
- }
- else
- {
- return (lResult ^ lX8 ^ lY8);
- }
- }
-
- function doF(x, y, z)
- {
- return (x & y) | ((~x) & z);
- }
-
- function doG(x, y, z)
- {
- return (x & z) | (y & (~z));
- }
-
- function doH(x, y, z)
- {
- return (x ^ y ^ z);
- }
-
- function doI(x, y, z)
- {
- return (y ^ (x | (~z)));
- }
-
- function doFF(a, b, c, d, x, s, ac)
- {
- a = addUnsigned(a, addUnsigned(addUnsigned(doF(b, c, d), x), ac));
-
- return addUnsigned(rotateLeft(a, s), b);
- }
-
- function doGG(a, b, c, d, x, s, ac)
- {
- a = addUnsigned(a, addUnsigned(addUnsigned(doG(b, c, d), x), ac));
-
- return addUnsigned(rotateLeft(a, s), b);
- }
-
- function doHH(a, b, c, d, x, s, ac)
- {
- a = addUnsigned(a, addUnsigned(addUnsigned(doH(b, c, d), x), ac));
-
- return addUnsigned(rotateLeft(a, s), b);
- }
-
- function doII(a, b, c, d, x, s, ac)
- {
- a = addUnsigned(a, addUnsigned(addUnsigned(doI(b, c, d), x), ac));
-
- return addUnsigned(rotateLeft(a, s), b);
- }
-
- function convertToWordArray(str)
- {
- let lWordCount;
- const lMessageLength = str.length;
- const lNumberOfWords_temp1=lMessageLength + 8;
- const lNumberOfWords_temp2=(lNumberOfWords_temp1-(lNumberOfWords_temp1 % 64))/64;
- const lNumberOfWords = (lNumberOfWords_temp2+1)*16;
- const lWordArray = new Array(lNumberOfWords-1);
- let lBytePosition = 0;
- let lByteCount = 0;
-
- while (lByteCount < lMessageLength)
- {
- lWordCount = (lByteCount-(lByteCount % 4))/4;
- lBytePosition = (lByteCount % 4)*8;
- lWordArray[lWordCount] = (lWordArray[lWordCount] |
- (str.charCodeAt(lByteCount)<<lBytePosition));
- lByteCount++;
- }
- lWordCount = (lByteCount-(lByteCount % 4))/4;
- lBytePosition = (lByteCount % 4)*8;
- lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80<<lBytePosition);
- lWordArray[lNumberOfWords-2] = lMessageLength<<3;
- lWordArray[lNumberOfWords-1] = lMessageLength>>>29;
-
- return lWordArray;
- }
-
- function wordToHex(lValue)
- {
- let wordToHexValue='', wordToHexValue_temp='', lByte, lCount;
-
- for (lCount = 0; lCount<=3; lCount++)
- {
- lByte = (lValue>>>(lCount*8)) & 255;
- wordToHexValue_temp = `0${lByte.toString(16)}`;
- wordToHexValue = wordToHexValue +
- wordToHexValue_temp.substr(wordToHexValue_temp.length-2, 2);
- }
-
- return wordToHexValue;
- }
-
- function utf8Encode(str)
- {
- let utftext = '';
-
- for (let n = 0; n < str.length; n++)
- {
- const c = str.charCodeAt(n);
-
- if (c < 128)
- {
- utftext += String.fromCharCode(c);
- }
- else if ((c > 127) && (c < 2048))
- {
- utftext += String.fromCharCode((c >> 6) | 192);
- utftext += String.fromCharCode((c & 63) | 128);
- }
- else
- {
- utftext += String.fromCharCode((c >> 12) | 224);
- utftext += String.fromCharCode(((c >> 6) & 63) | 128);
- utftext += String.fromCharCode((c & 63) | 128);
- }
- }
-
- return utftext;
- }
-
- let x=[];
- let k, AA, BB, CC, DD, a, b, c, d;
- const S11=7, S12=12, S13=17, S14=22;
- const S21=5, S22=9, S23=14, S24=20;
- const S31=4, S32=11, S33=16, S34=23;
- const S41=6, S42=10, S43=15, S44=21;
-
- string = utf8Encode(string);
-
- x = convertToWordArray(string);
-
- a = 0x67452301; b = 0xEFCDAB89; c = 0x98BADCFE; d = 0x10325476;
-
- for (k=0; k<x.length; k+=16)
- {
- AA=a; BB=b; CC=c; DD=d;
- a=doFF(a, b, c, d, x[k+0], S11, 0xD76AA478);
- d=doFF(d, a, b, c, x[k+1], S12, 0xE8C7B756);
- c=doFF(c, d, a, b, x[k+2], S13, 0x242070DB);
- b=doFF(b, c, d, a, x[k+3], S14, 0xC1BDCEEE);
- a=doFF(a, b, c, d, x[k+4], S11, 0xF57C0FAF);
- d=doFF(d, a, b, c, x[k+5], S12, 0x4787C62A);
- c=doFF(c, d, a, b, x[k+6], S13, 0xA8304613);
- b=doFF(b, c, d, a, x[k+7], S14, 0xFD469501);
- a=doFF(a, b, c, d, x[k+8], S11, 0x698098D8);
- d=doFF(d, a, b, c, x[k+9], S12, 0x8B44F7AF);
- c=doFF(c, d, a, b, x[k+10], S13, 0xFFFF5BB1);
- b=doFF(b, c, d, a, x[k+11], S14, 0x895CD7BE);
- a=doFF(a, b, c, d, x[k+12], S11, 0x6B901122);
- d=doFF(d, a, b, c, x[k+13], S12, 0xFD987193);
- c=doFF(c, d, a, b, x[k+14], S13, 0xA679438E);
- b=doFF(b, c, d, a, x[k+15], S14, 0x49B40821);
- a=doGG(a, b, c, d, x[k+1], S21, 0xF61E2562);
- d=doGG(d, a, b, c, x[k+6], S22, 0xC040B340);
- c=doGG(c, d, a, b, x[k+11], S23, 0x265E5A51);
- b=doGG(b, c, d, a, x[k+0], S24, 0xE9B6C7AA);
- a=doGG(a, b, c, d, x[k+5], S21, 0xD62F105D);
- d=doGG(d, a, b, c, x[k+10], S22, 0x2441453);
- c=doGG(c, d, a, b, x[k+15], S23, 0xD8A1E681);
- b=doGG(b, c, d, a, x[k+4], S24, 0xE7D3FBC8);
- a=doGG(a, b, c, d, x[k+9], S21, 0x21E1CDE6);
- d=doGG(d, a, b, c, x[k+14], S22, 0xC33707D6);
- c=doGG(c, d, a, b, x[k+3], S23, 0xF4D50D87);
- b=doGG(b, c, d, a, x[k+8], S24, 0x455A14ED);
- a=doGG(a, b, c, d, x[k+13], S21, 0xA9E3E905);
- d=doGG(d, a, b, c, x[k+2], S22, 0xFCEFA3F8);
- c=doGG(c, d, a, b, x[k+7], S23, 0x676F02D9);
- b=doGG(b, c, d, a, x[k+12], S24, 0x8D2A4C8A);
- a=doHH(a, b, c, d, x[k+5], S31, 0xFFFA3942);
- d=doHH(d, a, b, c, x[k+8], S32, 0x8771F681);
- c=doHH(c, d, a, b, x[k+11], S33, 0x6D9D6122);
- b=doHH(b, c, d, a, x[k+14], S34, 0xFDE5380C);
- a=doHH(a, b, c, d, x[k+1], S31, 0xA4BEEA44);
- d=doHH(d, a, b, c, x[k+4], S32, 0x4BDECFA9);
- c=doHH(c, d, a, b, x[k+7], S33, 0xF6BB4B60);
- b=doHH(b, c, d, a, x[k+10], S34, 0xBEBFBC70);
- a=doHH(a, b, c, d, x[k+13], S31, 0x289B7EC6);
- d=doHH(d, a, b, c, x[k+0], S32, 0xEAA127FA);
- c=doHH(c, d, a, b, x[k+3], S33, 0xD4EF3085);
- b=doHH(b, c, d, a, x[k+6], S34, 0x4881D05);
- a=doHH(a, b, c, d, x[k+9], S31, 0xD9D4D039);
- d=doHH(d, a, b, c, x[k+12], S32, 0xE6DB99E5);
- c=doHH(c, d, a, b, x[k+15], S33, 0x1FA27CF8);
- b=doHH(b, c, d, a, x[k+2], S34, 0xC4AC5665);
- a=doII(a, b, c, d, x[k+0], S41, 0xF4292244);
- d=doII(d, a, b, c, x[k+7], S42, 0x432AFF97);
- c=doII(c, d, a, b, x[k+14], S43, 0xAB9423A7);
- b=doII(b, c, d, a, x[k+5], S44, 0xFC93A039);
- a=doII(a, b, c, d, x[k+12], S41, 0x655B59C3);
- d=doII(d, a, b, c, x[k+3], S42, 0x8F0CCC92);
- c=doII(c, d, a, b, x[k+10], S43, 0xFFEFF47D);
- b=doII(b, c, d, a, x[k+1], S44, 0x85845DD1);
- a=doII(a, b, c, d, x[k+8], S41, 0x6FA87E4F);
- d=doII(d, a, b, c, x[k+15], S42, 0xFE2CE6E0);
- c=doII(c, d, a, b, x[k+6], S43, 0xA3014314);
- b=doII(b, c, d, a, x[k+13], S44, 0x4E0811A1);
- a=doII(a, b, c, d, x[k+4], S41, 0xF7537E82);
- d=doII(d, a, b, c, x[k+11], S42, 0xBD3AF235);
- c=doII(c, d, a, b, x[k+2], S43, 0x2AD7D2BB);
- b=doII(b, c, d, a, x[k+9], S44, 0xEB86D391);
- a=addUnsigned(a, AA);
- b=addUnsigned(b, BB);
- c=addUnsigned(c, CC);
- d=addUnsigned(d, DD);
- }
-
- const temp = wordToHex(a)+wordToHex(b)+wordToHex(c)+wordToHex(d);
-
- return temp.toLowerCase();
+exports.calculateMD5 = string => {
+ function rotateLeft(lValue, iShiftBits) {
+ return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits));
+ }
+
+ function addUnsigned(lX, lY) {
+ const lX8 = lX & 0x80000000;
+ const lY8 = lY & 0x80000000;
+ const lX4 = lX & 0x40000000;
+ const lY4 = lY & 0x40000000;
+ const lResult = (lX & 0x3fffffff) + (lY & 0x3fffffff);
+
+ if (lX4 & lY4) {
+ return lResult ^ 0x80000000 ^ lX8 ^ lY8;
+ }
+ if (lX4 | lY4) {
+ if (lResult & 0x40000000) {
+ return lResult ^ 0xc0000000 ^ lX8 ^ lY8;
+ } else {
+ return lResult ^ 0x40000000 ^ lX8 ^ lY8;
+ }
+ } else {
+ return lResult ^ lX8 ^ lY8;
+ }
+ }
+
+ function doF(x, y, z) {
+ return (x & y) | (~x & z);
+ }
+
+ function doG(x, y, z) {
+ return (x & z) | (y & ~z);
+ }
+
+ function doH(x, y, z) {
+ return x ^ y ^ z;
+ }
+
+ function doI(x, y, z) {
+ return y ^ (x | ~z);
+ }
+
+ function doFF(a, b, c, d, x, s, ac) {
+ a = addUnsigned(a, addUnsigned(addUnsigned(doF(b, c, d), x), ac));
+
+ return addUnsigned(rotateLeft(a, s), b);
+ }
+
+ function doGG(a, b, c, d, x, s, ac) {
+ a = addUnsigned(a, addUnsigned(addUnsigned(doG(b, c, d), x), ac));
+
+ return addUnsigned(rotateLeft(a, s), b);
+ }
+
+ function doHH(a, b, c, d, x, s, ac) {
+ a = addUnsigned(a, addUnsigned(addUnsigned(doH(b, c, d), x), ac));
+
+ return addUnsigned(rotateLeft(a, s), b);
+ }
+
+ function doII(a, b, c, d, x, s, ac) {
+ a = addUnsigned(a, addUnsigned(addUnsigned(doI(b, c, d), x), ac));
+
+ return addUnsigned(rotateLeft(a, s), b);
+ }
+
+ function convertToWordArray(str) {
+ let lWordCount;
+ const lMessageLength = str.length;
+ const lNumberOfWords_temp1 = lMessageLength + 8;
+ const lNumberOfWords_temp2 =
+ (lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64;
+ const lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16;
+ const lWordArray = new Array(lNumberOfWords - 1);
+ let lBytePosition = 0;
+ let lByteCount = 0;
+
+ while (lByteCount < lMessageLength) {
+ lWordCount = (lByteCount - (lByteCount % 4)) / 4;
+ lBytePosition = (lByteCount % 4) * 8;
+ lWordArray[lWordCount] =
+ lWordArray[lWordCount] | (str.charCodeAt(lByteCount) << lBytePosition);
+ lByteCount++;
+ }
+ lWordCount = (lByteCount - (lByteCount % 4)) / 4;
+ lBytePosition = (lByteCount % 4) * 8;
+ lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition);
+ lWordArray[lNumberOfWords - 2] = lMessageLength << 3;
+ lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29;
+
+ return lWordArray;
+ }
+
+ function wordToHex(lValue) {
+ let wordToHexValue = '',
+ wordToHexValue_temp = '',
+ lByte,
+ lCount;
+
+ for (lCount = 0; lCount <= 3; lCount++) {
+ lByte = (lValue >>> (lCount * 8)) & 255;
+ wordToHexValue_temp = `0${lByte.toString(16)}`;
+ wordToHexValue =
+ wordToHexValue +
+ wordToHexValue_temp.substr(wordToHexValue_temp.length - 2, 2);
+ }
+
+ return wordToHexValue;
+ }
+
+ function utf8Encode(str) {
+ let utftext = '';
+
+ for (let n = 0; n < str.length; n++) {
+ const c = str.charCodeAt(n);
+
+ if (c < 128) {
+ utftext += String.fromCharCode(c);
+ } else if (c > 127 && c < 2048) {
+ utftext += String.fromCharCode((c >> 6) | 192);
+ utftext += String.fromCharCode((c & 63) | 128);
+ } else {
+ utftext += String.fromCharCode((c >> 12) | 224);
+ utftext += String.fromCharCode(((c >> 6) & 63) | 128);
+ utftext += String.fromCharCode((c & 63) | 128);
+ }
+ }
+
+ return utftext;
+ }
+
+ let x = [];
+ let k, AA, BB, CC, DD, a, b, c, d;
+ const S11 = 7,
+ S12 = 12,
+ S13 = 17,
+ S14 = 22;
+ const S21 = 5,
+ S22 = 9,
+ S23 = 14,
+ S24 = 20;
+ const S31 = 4,
+ S32 = 11,
+ S33 = 16,
+ S34 = 23;
+ const S41 = 6,
+ S42 = 10,
+ S43 = 15,
+ S44 = 21;
+
+ string = utf8Encode(string);
+
+ x = convertToWordArray(string);
+
+ a = 0x67452301;
+ b = 0xefcdab89;
+ c = 0x98badcfe;
+ d = 0x10325476;
+
+ for (k = 0; k < x.length; k += 16) {
+ AA = a;
+ BB = b;
+ CC = c;
+ DD = d;
+ a = doFF(a, b, c, d, x[k + 0], S11, 0xd76aa478);
+ d = doFF(d, a, b, c, x[k + 1], S12, 0xe8c7b756);
+ c = doFF(c, d, a, b, x[k + 2], S13, 0x242070db);
+ b = doFF(b, c, d, a, x[k + 3], S14, 0xc1bdceee);
+ a = doFF(a, b, c, d, x[k + 4], S11, 0xf57c0faf);
+ d = doFF(d, a, b, c, x[k + 5], S12, 0x4787c62a);
+ c = doFF(c, d, a, b, x[k + 6], S13, 0xa8304613);
+ b = doFF(b, c, d, a, x[k + 7], S14, 0xfd469501);
+ a = doFF(a, b, c, d, x[k + 8], S11, 0x698098d8);
+ d = doFF(d, a, b, c, x[k + 9], S12, 0x8b44f7af);
+ c = doFF(c, d, a, b, x[k + 10], S13, 0xffff5bb1);
+ b = doFF(b, c, d, a, x[k + 11], S14, 0x895cd7be);
+ a = doFF(a, b, c, d, x[k + 12], S11, 0x6b901122);
+ d = doFF(d, a, b, c, x[k + 13], S12, 0xfd987193);
+ c = doFF(c, d, a, b, x[k + 14], S13, 0xa679438e);
+ b = doFF(b, c, d, a, x[k + 15], S14, 0x49b40821);
+ a = doGG(a, b, c, d, x[k + 1], S21, 0xf61e2562);
+ d = doGG(d, a, b, c, x[k + 6], S22, 0xc040b340);
+ c = doGG(c, d, a, b, x[k + 11], S23, 0x265e5a51);
+ b = doGG(b, c, d, a, x[k + 0], S24, 0xe9b6c7aa);
+ a = doGG(a, b, c, d, x[k + 5], S21, 0xd62f105d);
+ d = doGG(d, a, b, c, x[k + 10], S22, 0x2441453);
+ c = doGG(c, d, a, b, x[k + 15], S23, 0xd8a1e681);
+ b = doGG(b, c, d, a, x[k + 4], S24, 0xe7d3fbc8);
+ a = doGG(a, b, c, d, x[k + 9], S21, 0x21e1cde6);
+ d = doGG(d, a, b, c, x[k + 14], S22, 0xc33707d6);
+ c = doGG(c, d, a, b, x[k + 3], S23, 0xf4d50d87);
+ b = doGG(b, c, d, a, x[k + 8], S24, 0x455a14ed);
+ a = doGG(a, b, c, d, x[k + 13], S21, 0xa9e3e905);
+ d = doGG(d, a, b, c, x[k + 2], S22, 0xfcefa3f8);
+ c = doGG(c, d, a, b, x[k + 7], S23, 0x676f02d9);
+ b = doGG(b, c, d, a, x[k + 12], S24, 0x8d2a4c8a);
+ a = doHH(a, b, c, d, x[k + 5], S31, 0xfffa3942);
+ d = doHH(d, a, b, c, x[k + 8], S32, 0x8771f681);
+ c = doHH(c, d, a, b, x[k + 11], S33, 0x6d9d6122);
+ b = doHH(b, c, d, a, x[k + 14], S34, 0xfde5380c);
+ a = doHH(a, b, c, d, x[k + 1], S31, 0xa4beea44);
+ d = doHH(d, a, b, c, x[k + 4], S32, 0x4bdecfa9);
+ c = doHH(c, d, a, b, x[k + 7], S33, 0xf6bb4b60);
+ b = doHH(b, c, d, a, x[k + 10], S34, 0xbebfbc70);
+ a = doHH(a, b, c, d, x[k + 13], S31, 0x289b7ec6);
+ d = doHH(d, a, b, c, x[k + 0], S32, 0xeaa127fa);
+ c = doHH(c, d, a, b, x[k + 3], S33, 0xd4ef3085);
+ b = doHH(b, c, d, a, x[k + 6], S34, 0x4881d05);
+ a = doHH(a, b, c, d, x[k + 9], S31, 0xd9d4d039);
+ d = doHH(d, a, b, c, x[k + 12], S32, 0xe6db99e5);
+ c = doHH(c, d, a, b, x[k + 15], S33, 0x1fa27cf8);
+ b = doHH(b, c, d, a, x[k + 2], S34, 0xc4ac5665);
+ a = doII(a, b, c, d, x[k + 0], S41, 0xf4292244);
+ d = doII(d, a, b, c, x[k + 7], S42, 0x432aff97);
+ c = doII(c, d, a, b, x[k + 14], S43, 0xab9423a7);
+ b = doII(b, c, d, a, x[k + 5], S44, 0xfc93a039);
+ a = doII(a, b, c, d, x[k + 12], S41, 0x655b59c3);
+ d = doII(d, a, b, c, x[k + 3], S42, 0x8f0ccc92);
+ c = doII(c, d, a, b, x[k + 10], S43, 0xffeff47d);
+ b = doII(b, c, d, a, x[k + 1], S44, 0x85845dd1);
+ a = doII(a, b, c, d, x[k + 8], S41, 0x6fa87e4f);
+ d = doII(d, a, b, c, x[k + 15], S42, 0xfe2ce6e0);
+ c = doII(c, d, a, b, x[k + 6], S43, 0xa3014314);
+ b = doII(b, c, d, a, x[k + 13], S44, 0x4e0811a1);
+ a = doII(a, b, c, d, x[k + 4], S41, 0xf7537e82);
+ d = doII(d, a, b, c, x[k + 11], S42, 0xbd3af235);
+ c = doII(c, d, a, b, x[k + 2], S43, 0x2ad7d2bb);
+ b = doII(b, c, d, a, x[k + 9], S44, 0xeb86d391);
+ a = addUnsigned(a, AA);
+ b = addUnsigned(b, BB);
+ c = addUnsigned(c, CC);
+ d = addUnsigned(d, DD);
+ }
+
+ const temp = wordToHex(a) + wordToHex(b) + wordToHex(c) + wordToHex(d);
+
+ return temp.toLowerCase();
};
-exports.closeMediaStream = (stream) =>
-{
- if (!stream)
- {
- return;
- }
-
- // Latest spec states that MediaStream has no stop() method and instead must
- // call stop() on every MediaStreamTrack.
- try
- {
- let tracks;
-
- if (stream.getTracks)
- {
- tracks = stream.getTracks();
- for (const track of tracks)
- {
- track.stop();
- }
- }
- else
- {
- tracks = stream.getAudioTracks();
- for (const track of tracks)
- {
- track.stop();
- }
- tracks = stream.getVideoTracks();
- for (const track of tracks)
- {
- track.stop();
- }
- }
- }
- // eslint-disable-next-line no-unused-vars
- catch (error)
- {
- // Deprecated by the spec, but still in use.
- // NOTE: In Temasys IE plugin stream.stop is a callable 'object'.
- if (typeof stream.stop === 'function' || typeof stream.stop === 'object')
- {
- stream.stop();
- }
- }
+exports.closeMediaStream = stream => {
+ if (!stream) {
+ return;
+ }
+
+ // Latest spec states that MediaStream has no stop() method and instead must
+ // call stop() on every MediaStreamTrack.
+ try {
+ let tracks;
+
+ if (stream.getTracks) {
+ tracks = stream.getTracks();
+ for (const track of tracks) {
+ track.stop();
+ }
+ } else {
+ tracks = stream.getAudioTracks();
+ for (const track of tracks) {
+ track.stop();
+ }
+ tracks = stream.getVideoTracks();
+ for (const track of tracks) {
+ track.stop();
+ }
+ }
+ } catch (error) {
+ // Deprecated by the spec, but still in use.
+ // NOTE: In Temasys IE plugin stream.stop is a callable 'object'.
+ if (typeof stream.stop === 'function' || typeof stream.stop === 'object') {
+ stream.stop();
+ }
+ }
};
-exports.cloneArray = (array) =>
-{
- return (array && array.slice()) || [];
+exports.cloneArray = array => {
+ return (array && array.slice()) || [];
};
-exports.cloneObject = (obj, fallback = {}) =>
-{
- return (obj && Object.assign({}, obj)) || fallback;
+exports.cloneObject = (obj, fallback = {}) => {
+ return (obj && Object.assign({}, obj)) || fallback;
};
diff --git a/src/WebSocketInterface.d.ts b/src/WebSocketInterface.d.ts
index 581b712..4e37a16 100644
--- a/src/WebSocketInterface.d.ts
+++ b/src/WebSocketInterface.d.ts
@@ -1,5 +1,5 @@
import { Socket } from './Socket';
export class WebSocketInterface extends Socket {
- constructor(url: string)
+ constructor(url: string);
}
diff --git a/src/WebSocketInterface.js b/src/WebSocketInterface.js
index 8d9de81..9dc582e 100644
--- a/src/WebSocketInterface.js
+++ b/src/WebSocketInterface.js
@@ -3,175 +3,142 @@ const Grammar = require('./Grammar');
const logger = new Logger('WebSocketInterface');
-module.exports = class WebSocketInterface
-{
- constructor(url)
- {
- logger.debug('new() [url:"%s"]', url);
-
- this._url = url;
- this._sip_uri = null;
- this._via_transport = null;
- this._ws = null;
-
- const parsed_url = Grammar.parse(url, 'absoluteURI');
-
- if (parsed_url === -1)
- {
- logger.warn(`invalid WebSocket URI: ${url}`);
- throw new TypeError(`Invalid argument: ${url}`);
- }
- else if (parsed_url.scheme !== 'wss' && parsed_url.scheme !== 'ws')
- {
- logger.warn(`invalid WebSocket URI scheme: ${parsed_url.scheme}`);
- throw new TypeError(`Invalid argument: ${url}`);
- }
- else
- {
- this._sip_uri = `sip:${parsed_url.host}${parsed_url.port ? `:${parsed_url.port}` : ''};transport=ws`;
- this._via_transport = parsed_url.scheme.toUpperCase();
- }
- }
-
- get via_transport()
- {
- return this._via_transport;
- }
-
- set via_transport(value)
- {
- this._via_transport = value.toUpperCase();
- }
-
- get sip_uri()
- {
- return this._sip_uri;
- }
-
- get url()
- {
- return this._url;
- }
-
- connect()
- {
- logger.debug('connect()');
-
- if (this.isConnected())
- {
- logger.debug(`WebSocket ${this._url} is already connected`);
-
- return;
- }
- else if (this.isConnecting())
- {
- logger.debug(`WebSocket ${this._url} is connecting`);
-
- return;
- }
-
- if (this._ws)
- {
- this.disconnect();
- }
-
- logger.debug(`connecting to WebSocket ${this._url}`);
-
- try
- {
- this._ws = new WebSocket(this._url, 'sip');
-
- this._ws.binaryType = 'arraybuffer';
-
- this._ws.onopen = this._onOpen.bind(this);
- this._ws.onclose = this._onClose.bind(this);
- this._ws.onmessage = this._onMessage.bind(this);
- this._ws.onerror = this._onError.bind(this);
- }
- catch (error)
- {
- this._onError(error);
- }
- }
-
- disconnect()
- {
- logger.debug('disconnect()');
-
- if (this._ws)
- {
- // Unbind websocket event callbacks.
- this._ws.onopen = () => {};
- this._ws.onclose = () => {};
- this._ws.onmessage = () => {};
- this._ws.onerror = () => {};
-
- this._ws.close();
- this._ws = null;
- }
- }
-
- send(message)
- {
- logger.debug('send()');
-
- if (this.isConnected())
- {
- this._ws.send(message);
-
- return true;
- }
- else
- {
- logger.warn('unable to send message, WebSocket is not open');
-
- return false;
- }
- }
-
- isConnected()
- {
- return this._ws && this._ws.readyState === this._ws.OPEN;
- }
-
- isConnecting()
- {
- return this._ws && this._ws.readyState === this._ws.CONNECTING;
- }
-
-
- /**
- * WebSocket Event Handlers
- */
-
- _onOpen()
- {
- logger.debug(`WebSocket ${this._url} connected`);
-
- this.onconnect();
- }
-
- _onClose({ wasClean, code, reason })
- {
- logger.debug(`WebSocket ${this._url} closed`);
-
- if (wasClean === false)
- {
- logger.debug('WebSocket abrupt disconnection');
- }
-
- this.ondisconnect(!wasClean, code, reason);
- }
-
- _onMessage({ data })
- {
- logger.debug('received WebSocket message');
-
- this.ondata(data);
- }
-
- _onError(e)
- {
- logger.warn(`WebSocket ${this._url} error: `, e);
- }
+module.exports = class WebSocketInterface {
+ constructor(url) {
+ logger.debug('new() [url:"%s"]', url);
+
+ this._url = url;
+ this._sip_uri = null;
+ this._via_transport = null;
+ this._ws = null;
+
+ const parsed_url = Grammar.parse(url, 'absoluteURI');
+
+ if (parsed_url === -1) {
+ logger.warn(`invalid WebSocket URI: ${url}`);
+ throw new TypeError(`Invalid argument: ${url}`);
+ } else if (parsed_url.scheme !== 'wss' && parsed_url.scheme !== 'ws') {
+ logger.warn(`invalid WebSocket URI scheme: ${parsed_url.scheme}`);
+ throw new TypeError(`Invalid argument: ${url}`);
+ } else {
+ this._sip_uri = `sip:${parsed_url.host}${parsed_url.port ? `:${parsed_url.port}` : ''};transport=ws`;
+ this._via_transport = parsed_url.scheme.toUpperCase();
+ }
+ }
+
+ get via_transport() {
+ return this._via_transport;
+ }
+
+ set via_transport(value) {
+ this._via_transport = value.toUpperCase();
+ }
+
+ get sip_uri() {
+ return this._sip_uri;
+ }
+
+ get url() {
+ return this._url;
+ }
+
+ connect() {
+ logger.debug('connect()');
+
+ if (this.isConnected()) {
+ logger.debug(`WebSocket ${this._url} is already connected`);
+
+ return;
+ } else if (this.isConnecting()) {
+ logger.debug(`WebSocket ${this._url} is connecting`);
+
+ return;
+ }
+
+ if (this._ws) {
+ this.disconnect();
+ }
+
+ logger.debug(`connecting to WebSocket ${this._url}`);
+
+ try {
+ this._ws = new WebSocket(this._url, 'sip');
+
+ this._ws.binaryType = 'arraybuffer';
+
+ this._ws.onopen = this._onOpen.bind(this);
+ this._ws.onclose = this._onClose.bind(this);
+ this._ws.onmessage = this._onMessage.bind(this);
+ this._ws.onerror = this._onError.bind(this);
+ } catch (error) {
+ this._onError(error);
+ }
+ }
+
+ disconnect() {
+ logger.debug('disconnect()');
+
+ if (this._ws) {
+ // Unbind websocket event callbacks.
+ this._ws.onopen = () => {};
+ this._ws.onclose = () => {};
+ this._ws.onmessage = () => {};
+ this._ws.onerror = () => {};
+
+ this._ws.close();
+ this._ws = null;
+ }
+ }
+
+ send(message) {
+ logger.debug('send()');
+
+ if (this.isConnected()) {
+ this._ws.send(message);
+
+ return true;
+ } else {
+ logger.warn('unable to send message, WebSocket is not open');
+
+ return false;
+ }
+ }
+
+ isConnected() {
+ return this._ws && this._ws.readyState === this._ws.OPEN;
+ }
+
+ isConnecting() {
+ return this._ws && this._ws.readyState === this._ws.CONNECTING;
+ }
+
+ /**
+ * WebSocket Event Handlers
+ */
+
+ _onOpen() {
+ logger.debug(`WebSocket ${this._url} connected`);
+
+ this.onconnect();
+ }
+
+ _onClose({ wasClean, code, reason }) {
+ logger.debug(`WebSocket ${this._url} closed`);
+
+ if (wasClean === false) {
+ logger.debug('WebSocket abrupt disconnection');
+ }
+
+ this.ondisconnect(!wasClean, code, reason);
+ }
+
+ _onMessage({ data }) {
+ logger.debug('received WebSocket message');
+
+ this.ondata(data);
+ }
+
+ _onError(e) {
+ logger.warn(`WebSocket ${this._url} error: `, e);
+ }
};
diff --git a/src/sanityCheck.js b/src/sanityCheck.js
index b843048..ef3d661 100644
--- a/src/sanityCheck.js
+++ b/src/sanityCheck.js
@@ -6,68 +6,53 @@ const Utils = require('./Utils');
const logger = new Logger('sanityCheck');
// Checks for requests and responses.
-const all = [ minimumHeaders ];
+const all = [minimumHeaders];
// Checks for requests.
const requests = [
- rfc3261_8_2_2_1,
- rfc3261_16_3_4,
- rfc3261_18_3_request,
- rfc3261_8_2_2_2
+ rfc3261_8_2_2_1,
+ rfc3261_16_3_4,
+ rfc3261_18_3_request,
+ rfc3261_8_2_2_2,
];
// Checks for responses.
-const responses = [
- rfc3261_8_1_3_3,
- rfc3261_18_3_response
-];
+const responses = [rfc3261_8_1_3_3, rfc3261_18_3_response];
// local variables.
let message;
let ua;
let transport;
-module.exports = (m, u, t) =>
-{
- message = m;
- ua = u;
- transport = t;
-
- for (const check of all)
- {
- if (check() === false)
- {
- return false;
- }
- }
-
- if (message instanceof SIPMessage.IncomingRequest)
- {
- for (const check of requests)
- {
- if (check() === false)
- {
- return false;
- }
- }
- }
-
- else if (message instanceof SIPMessage.IncomingResponse)
- {
- for (const check of responses)
- {
- if (check() === false)
- {
- return false;
- }
- }
- }
-
- // Everything is OK.
- return true;
+module.exports = (m, u, t) => {
+ message = m;
+ ua = u;
+ transport = t;
+
+ for (const check of all) {
+ if (check() === false) {
+ return false;
+ }
+ }
+
+ if (message instanceof SIPMessage.IncomingRequest) {
+ for (const check of requests) {
+ if (check() === false) {
+ return false;
+ }
+ }
+ } else if (message instanceof SIPMessage.IncomingResponse) {
+ for (const check of responses) {
+ if (check() === false) {
+ return false;
+ }
+ }
+ }
+
+ // Everything is OK.
+ return true;
};
-
/*
* Sanity Check for incoming Messages
*
@@ -88,181 +73,169 @@ module.exports = (m, u, t) =>
*/
// Sanity Check functions for requests.
-function rfc3261_8_2_2_1()
-{
- if (message.s('to').uri.scheme !== 'sip')
- {
- reply(416);
-
- return false;
- }
+function rfc3261_8_2_2_1() {
+ if (message.s('to').uri.scheme !== 'sip') {
+ reply(416);
+
+ return false;
+ }
}
-function rfc3261_16_3_4()
-{
- if (!message.to_tag)
- {
- if (message.call_id.substr(0, 5) === ua.configuration.jssip_id)
- {
- reply(482);
-
- return false;
- }
- }
+function rfc3261_16_3_4() {
+ if (!message.to_tag) {
+ if (message.call_id.substr(0, 5) === ua.configuration.jssip_id) {
+ reply(482);
+
+ return false;
+ }
+ }
}
-function rfc3261_18_3_request()
-{
- const len = Utils.str_utf8_length(message.body);
- const contentLength = message.getHeader('content-length');
+function rfc3261_18_3_request() {
+ const len = Utils.str_utf8_length(message.body);
+ const contentLength = message.getHeader('content-length');
- if (len < contentLength)
- {
- reply(400);
+ if (len < contentLength) {
+ reply(400);
- return false;
- }
+ return false;
+ }
}
-function rfc3261_8_2_2_2()
-{
- const fromTag = message.from_tag;
- const call_id = message.call_id;
- const cseq = message.cseq;
- let tr;
-
- // Accept any in-dialog request.
- if (message.to_tag)
- {
- return;
- }
-
- // INVITE request.
- if (message.method === JsSIP_C.INVITE)
- {
- // If the branch matches the key of any IST then assume it is a retransmission
- // and ignore the INVITE.
- // TODO: we should reply the last response.
- if (ua._transactions.ist[message.via_branch])
- {
- return false;
- }
- // Otherwise check whether it is a merged request.
- else
- {
- for (const transaction in ua._transactions.ist)
- {
- if (Object.prototype.hasOwnProperty.call(ua._transactions.ist, transaction))
- {
- tr = ua._transactions.ist[transaction];
- if (tr.request.from_tag === fromTag &&
- tr.request.call_id === call_id &&
- tr.request.cseq === cseq)
- {
- reply(482);
-
- return false;
- }
- }
- }
- }
- }
-
- // Non INVITE request.
-
- // If the branch matches the key of any NIST then assume it is a retransmission
- // and ignore the request.
- // TODO: we should reply the last response.
- else if (ua._transactions.nist[message.via_branch])
- {
- return false;
- }
-
- // Otherwise check whether it is a merged request.
- else
- {
- for (const transaction in ua._transactions.nist)
- {
- if (Object.prototype.hasOwnProperty.call(ua._transactions.nist, transaction))
- {
- tr = ua._transactions.nist[transaction];
- if (tr.request.from_tag === fromTag &&
- tr.request.call_id === call_id &&
- tr.request.cseq === cseq)
- {
- reply(482);
-
- return false;
- }
- }
- }
- }
+function rfc3261_8_2_2_2() {
+ const fromTag = message.from_tag;
+ const call_id = message.call_id;
+ const cseq = message.cseq;
+ let tr;
+
+ // Accept any in-dialog request.
+ if (message.to_tag) {
+ return;
+ }
+
+ // INVITE request.
+ if (message.method === JsSIP_C.INVITE) {
+ // If the branch matches the key of any IST then assume it is a retransmission
+ // and ignore the INVITE.
+ // TODO: we should reply the last response.
+ if (ua._transactions.ist[message.via_branch]) {
+ return false;
+ }
+ // Otherwise check whether it is a merged request.
+ else {
+ for (const transaction in ua._transactions.ist) {
+ if (
+ Object.prototype.hasOwnProperty.call(
+ ua._transactions.ist,
+ transaction
+ )
+ ) {
+ tr = ua._transactions.ist[transaction];
+ if (
+ tr.request.from_tag === fromTag &&
+ tr.request.call_id === call_id &&
+ tr.request.cseq === cseq
+ ) {
+ reply(482);
+
+ return false;
+ }
+ }
+ }
+ }
+ }
+
+ // Non INVITE request.
+
+ // If the branch matches the key of any NIST then assume it is a retransmission
+ // and ignore the request.
+ // TODO: we should reply the last response.
+ else if (ua._transactions.nist[message.via_branch]) {
+ return false;
+ }
+
+ // Otherwise check whether it is a merged request.
+ else {
+ for (const transaction in ua._transactions.nist) {
+ if (
+ Object.prototype.hasOwnProperty.call(ua._transactions.nist, transaction)
+ ) {
+ tr = ua._transactions.nist[transaction];
+ if (
+ tr.request.from_tag === fromTag &&
+ tr.request.call_id === call_id &&
+ tr.request.cseq === cseq
+ ) {
+ reply(482);
+
+ return false;
+ }
+ }
+ }
+ }
}
// Sanity Check functions for responses.
-function rfc3261_8_1_3_3()
-{
- if (message.getHeaders('via').length > 1)
- {
- logger.debug('more than one Via header field present in the response, dropping the response');
-
- return false;
- }
+function rfc3261_8_1_3_3() {
+ if (message.getHeaders('via').length > 1) {
+ logger.debug(
+ 'more than one Via header field present in the response, dropping the response'
+ );
+
+ return false;
+ }
}
-function rfc3261_18_3_response()
-{
- const len = Utils.str_utf8_length(message.body), contentLength = message.getHeader('content-length');
+function rfc3261_18_3_response() {
+ const len = Utils.str_utf8_length(message.body),
+ contentLength = message.getHeader('content-length');
- if (len < contentLength)
- {
- logger.debug('message body length is lower than the value in Content-Length header field, dropping the response');
+ if (len < contentLength) {
+ logger.debug(
+ 'message body length is lower than the value in Content-Length header field, dropping the response'
+ );
- return false;
- }
+ return false;
+ }
}
// Sanity Check functions for requests and responses.
-function minimumHeaders()
-{
- const mandatoryHeaders = [ 'from', 'to', 'call_id', 'cseq', 'via' ];
-
- for (const header of mandatoryHeaders)
- {
- if (!message.hasHeader(header))
- {
- logger.debug(`missing mandatory header field : ${header}, dropping the response`);
-
- return false;
- }
- }
+function minimumHeaders() {
+ const mandatoryHeaders = ['from', 'to', 'call_id', 'cseq', 'via'];
+
+ for (const header of mandatoryHeaders) {
+ if (!message.hasHeader(header)) {
+ logger.debug(
+ `missing mandatory header field : ${header}, dropping the response`
+ );
+
+ return false;
+ }
+ }
}
// Reply.
-function reply(status_code)
-{
- const vias = message.getHeaders('via');
+function reply(status_code) {
+ const vias = message.getHeaders('via');
- let to;
- let response = `SIP/2.0 ${status_code} ${JsSIP_C.REASON_PHRASE[status_code]}\r\n`;
+ let to;
+ let response = `SIP/2.0 ${status_code} ${JsSIP_C.REASON_PHRASE[status_code]}\r\n`;
- for (const via of vias)
- {
- response += `Via: ${via}\r\n`;
- }
+ for (const via of vias) {
+ response += `Via: ${via}\r\n`;
+ }
- to = message.getHeader('To');
+ to = message.getHeader('To');
- if (!message.to_tag)
- {
- to += `;tag=${Utils.newTag()}`;
- }
+ if (!message.to_tag) {
+ to += `;tag=${Utils.newTag()}`;
+ }
- response += `To: ${to}\r\n`;
- response += `From: ${message.getHeader('From')}\r\n`;
- response += `Call-ID: ${message.call_id}\r\n`;
- response += `CSeq: ${message.cseq} ${message.method}\r\n`;
- response += '\r\n';
+ response += `To: ${to}\r\n`;
+ response += `From: ${message.getHeader('From')}\r\n`;
+ response += `Call-ID: ${message.call_id}\r\n`;
+ response += `CSeq: ${message.cseq} ${message.method}\r\n`;
+ response += '\r\n';
- transport.send(response);
+ transport.send(response);
}
diff --git a/test/include/common.js b/test/include/common.js
index 1033686..85a9094 100644
--- a/test/include/common.js
+++ b/test/include/common.js
@@ -1,20 +1,18 @@
/* eslint no-console: 0*/
// Show uncaught errors.
-process.on('uncaughtException', function(error)
-{
- console.error('uncaught exception:');
- console.error(error.stack);
- process.exit(1);
+process.on('uncaughtException', function (error) {
+ console.error('uncaught exception:');
+ console.error(error.stack);
+ process.exit(1);
});
// Define global.WebSocket.
-global.WebSocket = function()
-{
- this.close = function() {};
+global.WebSocket = function () {
+ this.close = function () {};
};
// Define global.navigator for bowser module.
global.navigator = {
- userAgent : ''
+ userAgent: '',
};
diff --git a/test/include/loopSocket.js b/test/include/loopSocket.js
index 9a92d9a..c2a68dc 100644
--- a/test/include/loopSocket.js
+++ b/test/include/loopSocket.js
@@ -1,48 +1,42 @@
// LoopSocket send message itself.
// Used P2P logic: message call-id is modified in each leg.
-module.exports = class LoopSocket
-{
- constructor()
- {
- this.url = 'ws://localhost:12345';
- this.via_transport = 'WS';
- this.sip_uri = 'sip:localhost:12345;transport=ws';
- }
-
- connect()
- {
- setTimeout(() => { this.onconnect(); }, 0);
- }
-
- disconnect()
- {
- }
-
- send(message)
- {
- const message2 = this._modifyCallId(message);
-
- setTimeout(() => { this.ondata(message2); }, 0);
-
- return true;
- }
-
- // Call-ID: add or drop word '_second'.
- _modifyCallId(message)
- {
- const ixBegin = message.indexOf('Call-ID');
- const ixEnd = message.indexOf('\r', ixBegin);
- let callId = message.substring(ixBegin+9, ixEnd);
-
- if (callId.endsWith('_second'))
- {
- callId = callId.substring(0, callId.length - 7);
- }
- else
- {
- callId += '_second';
- }
-
- return `${message.substring(0, ixBegin)}Call-ID: ${callId}${message.substring(ixEnd)}`;
- }
+module.exports = class LoopSocket {
+ constructor() {
+ this.url = 'ws://localhost:12345';
+ this.via_transport = 'WS';
+ this.sip_uri = 'sip:localhost:12345;transport=ws';
+ }
+
+ connect() {
+ setTimeout(() => {
+ this.onconnect();
+ }, 0);
+ }
+
+ disconnect() {}
+
+ send(message) {
+ const message2 = this._modifyCallId(message);
+
+ setTimeout(() => {
+ this.ondata(message2);
+ }, 0);
+
+ return true;
+ }
+
+ // Call-ID: add or drop word '_second'.
+ _modifyCallId(message) {
+ const ixBegin = message.indexOf('Call-ID');
+ const ixEnd = message.indexOf('\r', ixBegin);
+ let callId = message.substring(ixBegin + 9, ixEnd);
+
+ if (callId.endsWith('_second')) {
+ callId = callId.substring(0, callId.length - 7);
+ } else {
+ callId += '_second';
+ }
+
+ return `${message.substring(0, ixBegin)}Call-ID: ${callId}${message.substring(ixEnd)}`;
+ }
};
diff --git a/test/include/testUA.js b/test/include/testUA.js
index 71caf67..2e426a5 100644
--- a/test/include/testUA.js
+++ b/test/include/testUA.js
@@ -1,54 +1,54 @@
module.exports = {
- SOCKET_DESCRIPTION : {
- 'via_transport' : 'WS',
- 'sip_uri' : 'sip:localhost:12345;transport=ws',
- 'url' : 'ws://localhost:12345'
- },
+ SOCKET_DESCRIPTION: {
+ via_transport: 'WS',
+ sip_uri: 'sip:localhost:12345;transport=ws',
+ url: 'ws://localhost:12345',
+ },
- UA_CONFIGURATION : {
- uri : 'sip:f%61keUA@jssip.net',
- password : '1234ññññ',
- display_name : 'Fake UA ð→€ł !!!',
- authorization_user : 'fakeUA',
- instance_id : 'uuid:8f1fa16a-1165-4a96-8341-785b1ef24f12',
- registrar_server : 'registrar.jssip.NET:6060;TRansport=TCP',
- register_expires : 600,
- register : false,
- connection_recovery_min_interval : 2,
- connection_recovery_max_interval : 30,
- use_preloaded_route : true,
- no_answer_timeout : 60000,
- session_timers : true
- },
+ UA_CONFIGURATION: {
+ uri: 'sip:f%61keUA@jssip.net',
+ password: '1234ññññ',
+ display_name: 'Fake UA ð→€ł !!!',
+ authorization_user: 'fakeUA',
+ instance_id: 'uuid:8f1fa16a-1165-4a96-8341-785b1ef24f12',
+ registrar_server: 'registrar.jssip.NET:6060;TRansport=TCP',
+ register_expires: 600,
+ register: false,
+ connection_recovery_min_interval: 2,
+ connection_recovery_max_interval: 30,
+ use_preloaded_route: true,
+ no_answer_timeout: 60000,
+ session_timers: true,
+ },
- UA_CONFIGURATION_AFTER_START : {
- uri : 'sip:fakeUA@jssip.net',
- password : '1234ññññ',
- display_name : 'Fake UA ð→€ł !!!',
- authorization_user : 'fakeUA',
- instance_id : '8f1fa16a-1165-4a96-8341-785b1ef24f12', // Without 'uuid:'.
- registrar_server : 'sip:registrar.jssip.net:6060;transport=tcp',
- register_expires : 600,
- register : false,
- use_preloaded_route : true,
- no_answer_timeout : 60000 * 1000, // Internally converted to miliseconds.
- session_timers : true
- },
+ UA_CONFIGURATION_AFTER_START: {
+ uri: 'sip:fakeUA@jssip.net',
+ password: '1234ññññ',
+ display_name: 'Fake UA ð→€ł !!!',
+ authorization_user: 'fakeUA',
+ instance_id: '8f1fa16a-1165-4a96-8341-785b1ef24f12', // Without 'uuid:'.
+ registrar_server: 'sip:registrar.jssip.net:6060;transport=tcp',
+ register_expires: 600,
+ register: false,
+ use_preloaded_route: true,
+ no_answer_timeout: 60000 * 1000, // Internally converted to miliseconds.
+ session_timers: true,
+ },
- UA_TRANSPORT_AFTER_START : {
- 'sockets' : [ {
- 'socket' : {
- 'via_transport' : 'WS',
- 'sip_uri' : 'sip:localhost:12345;transport=ws',
- 'url' : 'ws://localhost:12345'
- },
- 'weight' : 0
- } ],
- 'recovery_options' : {
- 'min_interval' : 2,
- 'max_interval' : 30
- }
- }
+ UA_TRANSPORT_AFTER_START: {
+ sockets: [
+ {
+ socket: {
+ via_transport: 'WS',
+ sip_uri: 'sip:localhost:12345;transport=ws',
+ url: 'ws://localhost:12345',
+ },
+ weight: 0,
+ },
+ ],
+ recovery_options: {
+ min_interval: 2,
+ max_interval: 30,
+ },
+ },
};
-
-
diff --git a/test/test-UA-no-WebRTC.js b/test/test-UA-no-WebRTC.js
index 840d5eb..9ef14fa 100644
--- a/test/test-UA-no-WebRTC.js
+++ b/test/test-UA-no-WebRTC.js
@@ -4,81 +4,99 @@ require('./include/common');
const testUA = require('./include/testUA');
const JsSIP = require('../');
-
-describe('UA No WebRTC', () =>
-{
-
- test('UA wrong configuration', () =>
- {
- expect(() => new JsSIP.UA({ 'lalala': 'lololo' })).toThrow(JsSIP.Exceptions.ConfigurationError);
- });
-
- test('UA no WS connection', () =>
- {
- const config = testUA.UA_CONFIGURATION;
- const wsSocket = new JsSIP.WebSocketInterface(testUA.SOCKET_DESCRIPTION.url);
-
- config.sockets = wsSocket;
-
- const ua = new JsSIP.UA(config);
-
- expect(ua instanceof (JsSIP.UA)).toBeTruthy();
-
- ua.start();
-
- expect(ua.contact.toString()).toBe(`<sip:${ua.contact.uri.user}@${ua.configuration.via_host};transport=ws>`);
- expect(ua.contact.toString({ outbound: false, anonymous: false, foo: true })).toBe(`<sip:${ua.contact.uri.user}@${ua.configuration.via_host};transport=ws>`);
- expect(ua.contact.toString({ outbound: true })).toBe(`<sip:${ua.contact.uri.user}@${ua.configuration.via_host};transport=ws;ob>`);
- expect(ua.contact.toString({ anonymous: true })).toBe('<sip:anonymous@anonymous.invalid;transport=ws>');
- expect(ua.contact.toString({ anonymous: true, outbound: true })).toBe('<sip:anonymous@anonymous.invalid;transport=ws;ob>');
-
- for (const parameter in testUA.UA_CONFIGURATION_AFTER_START)
- {
- if (Object.prototype.hasOwnProperty.call(
- testUA.UA_CONFIGURATION_AFTER_START, parameter))
- {
- switch (parameter)
- {
- case 'uri':
- case 'registrar_server':
- // eslint-disable-next-line jest/no-conditional-expect
- expect(ua.configuration[parameter].toString()).toBe(testUA.UA_CONFIGURATION_AFTER_START[parameter], `testing parameter ${parameter}`);
- break;
- case 'sockets':
- console.warn('IGNORE SOCKETS');
- break;
- default:
- // eslint-disable-next-line jest/no-conditional-expect
- expect(ua.configuration[parameter]).toBe(testUA.UA_CONFIGURATION_AFTER_START[parameter], `testing parameter ${parameter}`);
- }
- }
- }
-
- const transport = testUA.UA_TRANSPORT_AFTER_START;
- const sockets = transport.sockets;
- const socket = sockets[0].socket;
-
- expect(sockets.length).toEqual(ua.transport.sockets.length);
- expect(sockets[0].weight).toEqual(ua.transport.sockets[0].weight);
- expect(socket.via_transport).toEqual(ua.transport.via_transport);
- expect(socket.sip_uri).toEqual(ua.transport.sip_uri);
- expect(socket.url).toEqual(ua.transport.url);
-
- expect(transport.recovery_options).toEqual(ua.transport.recovery_options);
-
- ua.sendMessage('test', 'FAIL WITH CONNECTION_ERROR PLEASE', {
- eventHandlers : {
- failed : function(e)
- {
- expect(e.cause).toEqual(JsSIP.C.causes.CONNECTION_ERROR);
- }
- }
- });
-
- expect(
- () => ua.sendMessage('sip:ibc@iñaki.ðđß', 'FAIL WITH INVALID_TARGET PLEASE')
- ).toThrow();
-
- ua.stop();
- });
+describe('UA No WebRTC', () => {
+ test('UA wrong configuration', () => {
+ expect(() => new JsSIP.UA({ lalala: 'lololo' })).toThrow(
+ JsSIP.Exceptions.ConfigurationError
+ );
+ });
+
+ test('UA no WS connection', () => {
+ const config = testUA.UA_CONFIGURATION;
+ const wsSocket = new JsSIP.WebSocketInterface(
+ testUA.SOCKET_DESCRIPTION.url
+ );
+
+ config.sockets = wsSocket;
+
+ const ua = new JsSIP.UA(config);
+
+ expect(ua instanceof JsSIP.UA).toBeTruthy();
+
+ ua.start();
+
+ expect(ua.contact.toString()).toBe(
+ `<sip:${ua.contact.uri.user}@${ua.configuration.via_host};transport=ws>`
+ );
+ expect(
+ ua.contact.toString({ outbound: false, anonymous: false, foo: true })
+ ).toBe(
+ `<sip:${ua.contact.uri.user}@${ua.configuration.via_host};transport=ws>`
+ );
+ expect(ua.contact.toString({ outbound: true })).toBe(
+ `<sip:${ua.contact.uri.user}@${ua.configuration.via_host};transport=ws;ob>`
+ );
+ expect(ua.contact.toString({ anonymous: true })).toBe(
+ '<sip:anonymous@anonymous.invalid;transport=ws>'
+ );
+ expect(ua.contact.toString({ anonymous: true, outbound: true })).toBe(
+ '<sip:anonymous@anonymous.invalid;transport=ws;ob>'
+ );
+
+ for (const parameter in testUA.UA_CONFIGURATION_AFTER_START) {
+ if (
+ Object.prototype.hasOwnProperty.call(
+ testUA.UA_CONFIGURATION_AFTER_START,
+ parameter
+ )
+ ) {
+ switch (parameter) {
+ case 'uri':
+ case 'registrar_server': {
+ expect(ua.configuration[parameter].toString()).toBe(
+ testUA.UA_CONFIGURATION_AFTER_START[parameter],
+ `testing parameter ${parameter}`
+ );
+ break;
+ }
+ case 'sockets': {
+ console.warn('IGNORE SOCKETS');
+ break;
+ }
+ default: {
+ expect(ua.configuration[parameter]).toBe(
+ testUA.UA_CONFIGURATION_AFTER_START[parameter],
+ `testing parameter ${parameter}`
+ );
+ }
+ }
+ }
+ }
+
+ const transport = testUA.UA_TRANSPORT_AFTER_START;
+ const sockets = transport.sockets;
+ const socket = sockets[0].socket;
+
+ expect(sockets.length).toEqual(ua.transport.sockets.length);
+ expect(sockets[0].weight).toEqual(ua.transport.sockets[0].weight);
+ expect(socket.via_transport).toEqual(ua.transport.via_transport);
+ expect(socket.sip_uri).toEqual(ua.transport.sip_uri);
+ expect(socket.url).toEqual(ua.transport.url);
+
+ expect(transport.recovery_options).toEqual(ua.transport.recovery_options);
+
+ ua.sendMessage('test', 'FAIL WITH CONNECTION_ERROR PLEASE', {
+ eventHandlers: {
+ failed: function (e) {
+ expect(e.cause).toEqual(JsSIP.C.causes.CONNECTION_ERROR);
+ },
+ },
+ });
+
+ expect(() =>
+ ua.sendMessage('sip:ibc@iñaki.ðđß', 'FAIL WITH INVALID_TARGET PLEASE')
+ ).toThrow();
+
+ ua.stop();
+ });
});
diff --git a/test/test-UA-subscriber-notifier.js b/test/test-UA-subscriber-notifier.js
index 62b5979..96729db 100644
--- a/test/test-UA-subscriber-notifier.js
+++ b/test/test-UA-subscriber-notifier.js
@@ -2,185 +2,174 @@ require('./include/common');
const JsSIP = require('../');
const LoopSocket = require('./include/loopSocket');
-describe('subscriber/notifier communication', () =>
-{
- test('should handle subscriber/notifier communication', () => new Promise((resolve) =>
- {
- let eventSequence = 0;
-
- const TARGET = 'ikq';
- const REQUEST_URI = 'sip:ikq@example.com';
- const CONTACT_URI = 'sip:ikq@abcdefabcdef.invalid;transport=ws';
- const SUBSCRIBE_ACCEPT = 'application/text, text/plain';
- const EVENT_NAME = 'weather';
- const CONTENT_TYPE = 'text/plain';
- const WEATHER_REQUEST = 'Please report the weather condition';
- const WEATHER_REPORT = '+20..+24°C, no precipitation, light wind';
-
- /**
- * @param {JsSIP.UA} ua
- */
- function createSubscriber(ua)
- {
- const options = {
- expires : 3600,
- contentType : CONTENT_TYPE,
- params : null
- };
-
- const subscriber = ua.subscribe(TARGET, EVENT_NAME, SUBSCRIBE_ACCEPT, options);
-
- subscriber.on('active', () =>
- {
- // 'receive notify with subscription-state: active'
- expect(++eventSequence).toBe(6);
- });
-
- subscriber.on('notify', (isFinal, notify, body, contType) =>
- {
- eventSequence++;
- // 'receive notify'
- expect(eventSequence === 7 || eventSequence === 11).toBe(true);
-
- expect(notify.method).toBe('NOTIFY');
- expect(notify.getHeader('contact')).toBe(`<${CONTACT_URI}>`); // 'notify contact'
- expect(body).toBe(WEATHER_REPORT); // 'notify body'
- expect(contType).toBe(CONTENT_TYPE); // 'notify content-type'
-
- const subsState = notify.parseHeader('subscription-state').state;
-
- expect(subsState === 'pending' || subsState === 'active' || subsState === 'terminated').toBe(true); // 'notify subscription-state'
-
- // After receiving the first notify, send un-subscribe.
- if (eventSequence === 7)
- {
- ++eventSequence; // 'send un-subscribe'
-
- subscriber.terminate(WEATHER_REQUEST);
- }
- });
-
- subscriber.on('terminated', (terminationCode, reason, retryAfter) =>
- {
- expect(++eventSequence).toBe(12); // 'subscriber terminated'
- expect(terminationCode).toBe(subscriber.C.FINAL_NOTIFY_RECEIVED);
- expect(reason).toBeUndefined();
- expect(retryAfter).toBeUndefined();
-
- ua.stop();
- });
-
- subscriber.on('accepted', () =>
- {
- expect(++eventSequence).toBe(5); // 'initial subscribe accepted'
- });
-
- expect(++eventSequence).toBe(2); // 'send subscribe'
-
- subscriber.subscribe(WEATHER_REQUEST);
- }
-
- /**
- * @param {JsSIP.UA} ua
- */
- function createNotifier(ua, subscribe)
- {
- const notifier = ua.notify(subscribe, CONTENT_TYPE, { pending: false });
-
- // Receive subscribe (includes initial)
- notifier.on('subscribe', (isUnsubscribe, subs, body, contType) =>
- {
- expect(subscribe.method).toBe('SUBSCRIBE');
- expect(subscribe.getHeader('contact')).toBe(`<${CONTACT_URI}>`); // 'subscribe contact'
- expect(subscribe.getHeader('accept')).toBe(SUBSCRIBE_ACCEPT); // 'subscribe accept'
- expect(body).toBe(WEATHER_REQUEST); // 'subscribe body'
- expect(contType).toBe(CONTENT_TYPE); // 'subscribe content-type'
-
- expect(++eventSequence).toBe(isUnsubscribe ? 9 : 4);
- if (isUnsubscribe)
- {
- // 'send final notify'
- notifier.terminate(WEATHER_REPORT);
- }
- else
- {
- // 'send notify'
- notifier.notify(WEATHER_REPORT);
- }
- });
-
- // Example only. Never reached.
- notifier.on('expired', () =>
- {
- notifier.terminate(WEATHER_REPORT, 'timeout');
- });
-
- notifier.on('terminated', () =>
- {
- expect(++eventSequence).toBe(10); // 'notifier terminated'
- });
-
- notifier.start();
- }
-
- // Start JsSIP UA with loop socket.
- const config =
- {
- sockets : new LoopSocket(), // message sending itself, with modified Call-ID
- uri : REQUEST_URI,
- contact_uri : CONTACT_URI,
- register : false
- };
-
- const ua = new JsSIP.UA(config);
-
- // Uncomment to see SIP communication
- // JsSIP.debug.enable('JsSIP:*');
-
- ua.on('newSubscribe', (e) =>
- {
- expect(++eventSequence).toBe(3); // 'receive initial subscribe'
-
- const subs = e.request;
- const ev = subs.parseHeader('event');
-
- expect(subs.ruri.toString()).toBe(REQUEST_URI); // 'initial subscribe uri'
- expect(ev.event).toBe(EVENT_NAME); // 'subscribe event'
-
- if (ev.event !== EVENT_NAME)
- {
- subs.reply(489); // "Bad Event"
-
- return;
- }
-
- const accepts = subs.getHeaders('accept');
- const canUse = accepts && accepts.some((v) => v.includes(CONTENT_TYPE));
-
- expect(canUse).toBe(true); // 'notifier can use subscribe accept header'
-
- if (!canUse)
- {
- subs.reply(406); // "Not Acceptable"
-
- return;
- }
-
- createNotifier(ua, subs);
- });
-
- ua.on('connected', () =>
- {
- expect(++eventSequence).toBe(1); // 'socket connected'
-
- createSubscriber(ua);
- });
-
- ua.on('disconnected', () =>
- {
- resolve();
- });
-
- ua.start();
- }));
+describe('subscriber/notifier communication', () => {
+ test('should handle subscriber/notifier communication', () =>
+ new Promise(resolve => {
+ let eventSequence = 0;
+
+ const TARGET = 'ikq';
+ const REQUEST_URI = 'sip:ikq@example.com';
+ const CONTACT_URI = 'sip:ikq@abcdefabcdef.invalid;transport=ws';
+ const SUBSCRIBE_ACCEPT = 'application/text, text/plain';
+ const EVENT_NAME = 'weather';
+ const CONTENT_TYPE = 'text/plain';
+ const WEATHER_REQUEST = 'Please report the weather condition';
+ const WEATHER_REPORT = '+20..+24°C, no precipitation, light wind';
+
+ /**
+ * @param {JsSIP.UA} ua
+ */
+ function createSubscriber(ua) {
+ const options = {
+ expires: 3600,
+ contentType: CONTENT_TYPE,
+ params: null,
+ };
+
+ const subscriber = ua.subscribe(
+ TARGET,
+ EVENT_NAME,
+ SUBSCRIBE_ACCEPT,
+ options
+ );
+
+ subscriber.on('active', () => {
+ // 'receive notify with subscription-state: active'
+ expect(++eventSequence).toBe(6);
+ });
+
+ subscriber.on('notify', (isFinal, notify, body, contType) => {
+ eventSequence++;
+ // 'receive notify'
+ expect(eventSequence === 7 || eventSequence === 11).toBe(true);
+
+ expect(notify.method).toBe('NOTIFY');
+ expect(notify.getHeader('contact')).toBe(`<${CONTACT_URI}>`); // 'notify contact'
+ expect(body).toBe(WEATHER_REPORT); // 'notify body'
+ expect(contType).toBe(CONTENT_TYPE); // 'notify content-type'
+
+ const subsState = notify.parseHeader('subscription-state').state;
+
+ expect(
+ subsState === 'pending' ||
+ subsState === 'active' ||
+ subsState === 'terminated'
+ ).toBe(true); // 'notify subscription-state'
+
+ // After receiving the first notify, send un-subscribe.
+ if (eventSequence === 7) {
+ ++eventSequence; // 'send un-subscribe'
+
+ subscriber.terminate(WEATHER_REQUEST);
+ }
+ });
+
+ subscriber.on('terminated', (terminationCode, reason, retryAfter) => {
+ expect(++eventSequence).toBe(12); // 'subscriber terminated'
+ expect(terminationCode).toBe(subscriber.C.FINAL_NOTIFY_RECEIVED);
+ expect(reason).toBeUndefined();
+ expect(retryAfter).toBeUndefined();
+
+ ua.stop();
+ });
+
+ subscriber.on('accepted', () => {
+ expect(++eventSequence).toBe(5); // 'initial subscribe accepted'
+ });
+
+ expect(++eventSequence).toBe(2); // 'send subscribe'
+
+ subscriber.subscribe(WEATHER_REQUEST);
+ }
+
+ /**
+ * @param {JsSIP.UA} ua
+ */
+ function createNotifier(ua, subscribe) {
+ const notifier = ua.notify(subscribe, CONTENT_TYPE, { pending: false });
+
+ // Receive subscribe (includes initial)
+ notifier.on('subscribe', (isUnsubscribe, subs, body, contType) => {
+ expect(subscribe.method).toBe('SUBSCRIBE');
+ expect(subscribe.getHeader('contact')).toBe(`<${CONTACT_URI}>`); // 'subscribe contact'
+ expect(subscribe.getHeader('accept')).toBe(SUBSCRIBE_ACCEPT); // 'subscribe accept'
+ expect(body).toBe(WEATHER_REQUEST); // 'subscribe body'
+ expect(contType).toBe(CONTENT_TYPE); // 'subscribe content-type'
+
+ expect(++eventSequence).toBe(isUnsubscribe ? 9 : 4);
+ if (isUnsubscribe) {
+ // 'send final notify'
+ notifier.terminate(WEATHER_REPORT);
+ } else {
+ // 'send notify'
+ notifier.notify(WEATHER_REPORT);
+ }
+ });
+
+ // Example only. Never reached.
+ notifier.on('expired', () => {
+ notifier.terminate(WEATHER_REPORT, 'timeout');
+ });
+
+ notifier.on('terminated', () => {
+ expect(++eventSequence).toBe(10); // 'notifier terminated'
+ });
+
+ notifier.start();
+ }
+
+ // Start JsSIP UA with loop socket.
+ const config = {
+ sockets: new LoopSocket(), // message sending itself, with modified Call-ID
+ uri: REQUEST_URI,
+ contact_uri: CONTACT_URI,
+ register: false,
+ };
+
+ const ua = new JsSIP.UA(config);
+
+ // Uncomment to see SIP communication
+ // JsSIP.debug.enable('JsSIP:*');
+
+ ua.on('newSubscribe', e => {
+ expect(++eventSequence).toBe(3); // 'receive initial subscribe'
+
+ const subs = e.request;
+ const ev = subs.parseHeader('event');
+
+ expect(subs.ruri.toString()).toBe(REQUEST_URI); // 'initial subscribe uri'
+ expect(ev.event).toBe(EVENT_NAME); // 'subscribe event'
+
+ if (ev.event !== EVENT_NAME) {
+ subs.reply(489); // "Bad Event"
+
+ return;
+ }
+
+ const accepts = subs.getHeaders('accept');
+ const canUse = accepts && accepts.some(v => v.includes(CONTENT_TYPE));
+
+ expect(canUse).toBe(true); // 'notifier can use subscribe accept header'
+
+ if (!canUse) {
+ subs.reply(406); // "Not Acceptable"
+
+ return;
+ }
+
+ createNotifier(ua, subs);
+ });
+
+ ua.on('connected', () => {
+ expect(++eventSequence).toBe(1); // 'socket connected'
+
+ createSubscriber(ua);
+ });
+
+ ua.on('disconnected', () => {
+ resolve();
+ });
+
+ ua.start();
+ }));
});
diff --git a/test/test-classes.js b/test/test-classes.js
index 02d9002..71f0f8e 100644
--- a/test/test-classes.js
+++ b/test/test-classes.js
@@ -1,156 +1,152 @@
require('./include/common');
const JsSIP = require('../');
-describe('URI Tests', () =>
-{
- test('new URI', () =>
- {
- const uri = new JsSIP.URI(null, 'alice', 'jssip.net', 6060);
+describe('URI Tests', () => {
+ test('new URI', () => {
+ const uri = new JsSIP.URI(null, 'alice', 'jssip.net', 6060);
- expect(uri.scheme).toBe('sip');
- expect(uri.user).toBe('alice');
- expect(uri.host).toBe('jssip.net');
- expect(uri.port).toBe(6060);
- expect(uri.toString()).toBe('sip:alice@jssip.net:6060');
- expect(uri.toAor()).toBe('sip:alice@jssip.net');
- expect(uri.toAor(false)).toBe('sip:alice@jssip.net');
- expect(uri.toAor(true)).toBe('sip:alice@jssip.net:6060');
-
- uri.scheme = 'SIPS';
- expect(uri.scheme).toBe('sips');
- expect(uri.toAor()).toBe('sips:alice@jssip.net');
- uri.scheme = 'sip';
-
- uri.user = 'Iñaki ðđ';
- expect(uri.user).toBe('Iñaki ðđ');
- expect(uri.toString()).toBe('sip:I%C3%B1aki%20%C3%B0%C4%91@jssip.net:6060');
- expect(uri.toAor()).toBe('sip:I%C3%B1aki%20%C3%B0%C4%91@jssip.net');
-
- uri.user = '%61lice';
- expect(uri.toAor()).toBe('sip:alice@jssip.net');
-
- uri.user = null;
- expect(uri.user).toBeNull();
- expect(uri.toAor()).toBe('sip:jssip.net');
- uri.user = 'alice';
-
- expect(() =>
- {
- uri.host = null;
- }).toThrow(TypeError);
-
- expect(() =>
- {
- uri.host = { bar: 'foo' };
- }).toThrow(TypeError);
-
- expect(uri.host).toBe('jssip.net');
-
- uri.host = 'VERSATICA.com';
- expect(uri.host).toBe('versatica.com');
- uri.host = 'jssip.net';
-
- uri.port = null;
- expect(uri.port).toBeNull();
-
- uri.port = undefined;
- expect(uri.port).toBeNull();
-
- uri.port = 'ABCD'; // Should become null.
- expect(uri.toString()).toBe('sip:alice@jssip.net');
-
- uri.port = '123ABCD'; // Should become 123.
- expect(uri.toString()).toBe('sip:alice@jssip.net:123');
-
- uri.port = 0;
- expect(uri.port).toBe(0);
- expect(uri.toString()).toBe('sip:alice@jssip.net:0');
- uri.port = null;
-
- expect(uri.hasParam('foo')).toBe(false);
-
- uri.setParam('Foo', null);
- expect(uri.hasParam('FOO')).toBe(true);
-
- uri.setParam('Baz', 123);
- expect(uri.getParam('baz')).toBe('123');
- expect(uri.toString()).toBe('sip:alice@jssip.net;foo;baz=123');
-
- uri.setParam('zero', 0);
- expect(uri.hasParam('ZERO')).toBe(true);
- expect(uri.getParam('ZERO')).toBe('0');
- expect(uri.toString()).toBe('sip:alice@jssip.net;foo;baz=123;zero=0');
- expect(uri.deleteParam('ZERO')).toBe('0');
-
- expect(uri.deleteParam('baZ')).toBe('123');
- expect(uri.deleteParam('NOO')).toBeUndefined();
- expect(uri.toString()).toBe('sip:alice@jssip.net;foo');
-
- uri.clearParams();
- expect(uri.toString()).toBe('sip:alice@jssip.net');
-
- expect(uri.hasHeader('foo')).toBe(false);
-
- uri.setHeader('Foo', 'LALALA');
- expect(uri.hasHeader('FOO')).toBe(true);
- expect(uri.getHeader('FOO')).toEqual([ 'LALALA' ]);
- expect(uri.toString()).toBe('sip:alice@jssip.net?Foo=LALALA');
-
- uri.setHeader('bAz', [ 'ABC-1', 'ABC-2' ]);
- expect(uri.getHeader('baz')).toEqual([ 'ABC-1', 'ABC-2' ]);
- expect(uri.toString()).toBe('sip:alice@jssip.net?Foo=LALALA&Baz=ABC-1&Baz=ABC-2');
-
- expect(uri.deleteHeader('baZ')).toEqual([ 'ABC-1', 'ABC-2' ]);
- expect(uri.deleteHeader('NOO')).toBeUndefined();
+ expect(uri.scheme).toBe('sip');
+ expect(uri.user).toBe('alice');
+ expect(uri.host).toBe('jssip.net');
+ expect(uri.port).toBe(6060);
+ expect(uri.toString()).toBe('sip:alice@jssip.net:6060');
+ expect(uri.toAor()).toBe('sip:alice@jssip.net');
+ expect(uri.toAor(false)).toBe('sip:alice@jssip.net');
+ expect(uri.toAor(true)).toBe('sip:alice@jssip.net:6060');
- uri.clearHeaders();
- expect(uri.toString()).toBe('sip:alice@jssip.net');
+ uri.scheme = 'SIPS';
+ expect(uri.scheme).toBe('sips');
+ expect(uri.toAor()).toBe('sips:alice@jssip.net');
+ uri.scheme = 'sip';
- const uri2 = uri.clone();
+ uri.user = 'Iñaki ðđ';
+ expect(uri.user).toBe('Iñaki ðđ');
+ expect(uri.toString()).toBe('sip:I%C3%B1aki%20%C3%B0%C4%91@jssip.net:6060');
+ expect(uri.toAor()).toBe('sip:I%C3%B1aki%20%C3%B0%C4%91@jssip.net');
- expect(uri2.toString()).toBe(uri.toString());
- uri2.user = 'popo';
- expect(uri2.user).toBe('popo');
- expect(uri.user).toBe('alice');
- });
+ uri.user = '%61lice';
+ expect(uri.toAor()).toBe('sip:alice@jssip.net');
+
+ uri.user = null;
+ expect(uri.user).toBeNull();
+ expect(uri.toAor()).toBe('sip:jssip.net');
+ uri.user = 'alice';
+
+ expect(() => {
+ uri.host = null;
+ }).toThrow(TypeError);
+
+ expect(() => {
+ uri.host = { bar: 'foo' };
+ }).toThrow(TypeError);
+
+ expect(uri.host).toBe('jssip.net');
+
+ uri.host = 'VERSATICA.com';
+ expect(uri.host).toBe('versatica.com');
+ uri.host = 'jssip.net';
+
+ uri.port = null;
+ expect(uri.port).toBeNull();
+
+ uri.port = undefined;
+ expect(uri.port).toBeNull();
+
+ uri.port = 'ABCD'; // Should become null.
+ expect(uri.toString()).toBe('sip:alice@jssip.net');
+
+ uri.port = '123ABCD'; // Should become 123.
+ expect(uri.toString()).toBe('sip:alice@jssip.net:123');
+
+ uri.port = 0;
+ expect(uri.port).toBe(0);
+ expect(uri.toString()).toBe('sip:alice@jssip.net:0');
+ uri.port = null;
+
+ expect(uri.hasParam('foo')).toBe(false);
+
+ uri.setParam('Foo', null);
+ expect(uri.hasParam('FOO')).toBe(true);
+
+ uri.setParam('Baz', 123);
+ expect(uri.getParam('baz')).toBe('123');
+ expect(uri.toString()).toBe('sip:alice@jssip.net;foo;baz=123');
+
+ uri.setParam('zero', 0);
+ expect(uri.hasParam('ZERO')).toBe(true);
+ expect(uri.getParam('ZERO')).toBe('0');
+ expect(uri.toString()).toBe('sip:alice@jssip.net;foo;baz=123;zero=0');
+ expect(uri.deleteParam('ZERO')).toBe('0');
+
+ expect(uri.deleteParam('baZ')).toBe('123');
+ expect(uri.deleteParam('NOO')).toBeUndefined();
+ expect(uri.toString()).toBe('sip:alice@jssip.net;foo');
+
+ uri.clearParams();
+ expect(uri.toString()).toBe('sip:alice@jssip.net');
+
+ expect(uri.hasHeader('foo')).toBe(false);
+
+ uri.setHeader('Foo', 'LALALA');
+ expect(uri.hasHeader('FOO')).toBe(true);
+ expect(uri.getHeader('FOO')).toEqual(['LALALA']);
+ expect(uri.toString()).toBe('sip:alice@jssip.net?Foo=LALALA');
+
+ uri.setHeader('bAz', ['ABC-1', 'ABC-2']);
+ expect(uri.getHeader('baz')).toEqual(['ABC-1', 'ABC-2']);
+ expect(uri.toString()).toBe(
+ 'sip:alice@jssip.net?Foo=LALALA&Baz=ABC-1&Baz=ABC-2'
+ );
+
+ expect(uri.deleteHeader('baZ')).toEqual(['ABC-1', 'ABC-2']);
+ expect(uri.deleteHeader('NOO')).toBeUndefined();
+
+ uri.clearHeaders();
+ expect(uri.toString()).toBe('sip:alice@jssip.net');
+
+ const uri2 = uri.clone();
+
+ expect(uri2.toString()).toBe(uri.toString());
+ uri2.user = 'popo';
+ expect(uri2.user).toBe('popo');
+ expect(uri.user).toBe('alice');
+ });
});
-describe('NameAddr Tests', () =>
-{
- test('new NameAddr', () =>
- {
- const uri = new JsSIP.URI('sip', 'alice', 'jssip.net');
- const name = new JsSIP.NameAddrHeader(uri, 'Alice æßð');
+describe('NameAddr Tests', () => {
+ test('new NameAddr', () => {
+ const uri = new JsSIP.URI('sip', 'alice', 'jssip.net');
+ const name = new JsSIP.NameAddrHeader(uri, 'Alice æßð');
- expect(name.display_name).toBe('Alice æßð');
- expect(name.toString()).toBe('"Alice æßð" <sip:alice@jssip.net>');
+ expect(name.display_name).toBe('Alice æßð');
+ expect(name.toString()).toBe('"Alice æßð" <sip:alice@jssip.net>');
- name.display_name = null;
- expect(name.toString()).toBe('<sip:alice@jssip.net>');
+ name.display_name = null;
+ expect(name.toString()).toBe('<sip:alice@jssip.net>');
- name.display_name = 0;
- expect(name.toString()).toBe('"0" <sip:alice@jssip.net>');
+ name.display_name = 0;
+ expect(name.toString()).toBe('"0" <sip:alice@jssip.net>');
- name.display_name = '';
- expect(name.toString()).toBe('<sip:alice@jssip.net>');
+ name.display_name = '';
+ expect(name.toString()).toBe('<sip:alice@jssip.net>');
- name.setParam('Foo', null);
- expect(name.hasParam('FOO')).toBe(true);
+ name.setParam('Foo', null);
+ expect(name.hasParam('FOO')).toBe(true);
- name.setParam('Baz', 123);
- expect(name.getParam('baz')).toBe('123');
- expect(name.toString()).toBe('<sip:alice@jssip.net>;foo;baz=123');
+ name.setParam('Baz', 123);
+ expect(name.getParam('baz')).toBe('123');
+ expect(name.toString()).toBe('<sip:alice@jssip.net>;foo;baz=123');
- expect(name.deleteParam('bAz')).toBe('123');
+ expect(name.deleteParam('bAz')).toBe('123');
- name.clearParams();
- expect(name.toString()).toBe('<sip:alice@jssip.net>');
+ name.clearParams();
+ expect(name.toString()).toBe('<sip:alice@jssip.net>');
- const name2 = name.clone();
+ const name2 = name.clone();
- expect(name2.toString()).toBe(name.toString());
- name2.display_name = '@ł€';
- expect(name2.display_name).toBe('@ł€');
- expect(name.user).toBeUndefined();
- });
+ expect(name2.toString()).toBe(name.toString());
+ name2.display_name = '@ł€';
+ expect(name2.display_name).toBe('@ł€');
+ expect(name.user).toBeUndefined();
+ });
});
diff --git a/test/test-digestAuthentication.js b/test/test-digestAuthentication.js
index 83d4a46..7acf27c 100644
--- a/test/test-digestAuthentication.js
+++ b/test/test-digestAuthentication.js
@@ -4,149 +4,134 @@ const DigestAuthentication = require('../src/DigestAuthentication.js');
// Results of this tests originally obtained from RFC 2617 and:
// 'https://pernau.at/kd/sipdigest.php'
-describe('DigestAuthentication', () =>
-{
- test('parse no auth testrealm@host.com -RFC 2617-', () =>
- {
- const method = 'GET';
- const ruri = '/dir/index.html';
- const cnonce = '0a4f113b';
- const credentials =
- {
- username : 'Mufasa',
- password : 'Circle Of Life',
- realm : 'testrealm@host.com',
- ha1 : null
- };
- const challenge =
- {
- algorithm : 'MD5',
- realm : 'testrealm@host.com',
- nonce : 'dcd98b7102dd2f0e8b11d0f600bfb0c093',
- opaque : '5ccc069c403ebaf9f0171e9517f40e41',
- stale : null,
- qop : 'auth'
- };
-
- const digest = new DigestAuthentication(credentials);
-
- digest.authenticate({ method, ruri }, challenge, cnonce);
-
- expect(digest._response).toBe('6629fae49393a05397450978507c4ef1');
- });
-
- test('digest authenticate qop = null', () =>
- {
- const method = 'REGISTER';
- const ruri = 'sip:testrealm@host.com';
- const credentials = {
- username : 'testuser',
- password : 'testpassword',
- realm : 'testrealm@host.com',
- ha1 : null
- };
- const challenge =
- {
- algorithm : 'MD5',
- realm : 'testrealm@host.com',
- nonce : '5a071f75353f667787615249c62dcc7b15a4828f',
- opaque : null,
- stale : null,
- qop : null
- };
-
- const digest = new DigestAuthentication(credentials);
-
- digest.authenticate({ method, ruri }, challenge);
-
- expect(digest._response).toBe('f99e05f591f147facbc94ff23b4b1dee');
- });
-
- test('digest authenticate qop = auth', () =>
- {
- const method = 'REGISTER';
- const ruri = 'sip:testrealm@host.com';
- const cnonce = '0a4f113b';
- const credentials =
- {
- username : 'testuser',
- password : 'testpassword',
- realm : 'testrealm@host.com',
- ha1 : null
- };
- const challenge =
- {
- algorithm : 'MD5',
- realm : 'testrealm@host.com',
- nonce : '5a071f75353f667787615249c62dcc7b15a4828f',
- opaque : null,
- stale : null,
- qop : 'auth'
- };
-
- const digest = new DigestAuthentication(credentials);
-
- digest.authenticate({ method, ruri }, challenge, cnonce);
-
- expect(digest._response).toBe('a69b9c2ea0dea1437a21df6ddc9b05e4');
- });
-
- test('digest authenticate qop = auth-int and empty body', () =>
- {
- const method = 'REGISTER';
- const ruri = 'sip:testrealm@host.com';
- const cnonce = '0a4f113b';
- const credentials =
- {
- username : 'testuser',
- password : 'testpassword',
- realm : 'testrealm@host.com',
- ha1 : null
- };
- const challenge =
- {
- algorithm : 'MD5',
- realm : 'testrealm@host.com',
- nonce : '5a071f75353f667787615249c62dcc7b15a4828f',
- opaque : null,
- stale : null,
- qop : 'auth-int'
- };
-
- const digest = new DigestAuthentication(credentials);
-
- digest.authenticate({ method, ruri }, challenge, cnonce);
-
- expect(digest._response).toBe('82b3cab8b1c4df404434db6a0581650c');
- });
-
- test('digest authenticate qop = auth-int and non-empty body', () =>
- {
- const method = 'REGISTER';
- const ruri = 'sip:testrealm@host.com';
- const body = 'TEST BODY';
- const cnonce = '0a4f113b';
- const credentials =
- {
- username : 'testuser',
- password : 'testpassword',
- realm : 'testrealm@host.com',
- ha1 : null
- };
- const challenge =
- {
- algorithm : 'MD5',
- realm : 'testrealm@host.com',
- nonce : '5a071f75353f667787615249c62dcc7b15a4828f',
- opaque : null,
- stale : null,
- qop : 'auth-int'
- };
-
- const digest = new DigestAuthentication(credentials);
-
- digest.authenticate({ method, ruri, body }, challenge, cnonce);
-
- expect(digest._response).toBe('7bf0e9de3fbb5da121974509d617f532');
- });
+describe('DigestAuthentication', () => {
+ test('parse no auth testrealm@host.com -RFC 2617-', () => {
+ const method = 'GET';
+ const ruri = '/dir/index.html';
+ const cnonce = '0a4f113b';
+ const credentials = {
+ username: 'Mufasa',
+ password: 'Circle Of Life',
+ realm: 'testrealm@host.com',
+ ha1: null,
+ };
+ const challenge = {
+ algorithm: 'MD5',
+ realm: 'testrealm@host.com',
+ nonce: 'dcd98b7102dd2f0e8b11d0f600bfb0c093',
+ opaque: '5ccc069c403ebaf9f0171e9517f40e41',
+ stale: null,
+ qop: 'auth',
+ };
+
+ const digest = new DigestAuthentication(credentials);
+
+ digest.authenticate({ method, ruri }, challenge, cnonce);
+
+ expect(digest._response).toBe('6629fae49393a05397450978507c4ef1');
+ });
+
+ test('digest authenticate qop = null', () => {
+ const method = 'REGISTER';
+ const ruri = 'sip:testrealm@host.com';
+ const credentials = {
+ username: 'testuser',
+ password: 'testpassword',
+ realm: 'testrealm@host.com',
+ ha1: null,
+ };
+ const challenge = {
+ algorithm: 'MD5',
+ realm: 'testrealm@host.com',
+ nonce: '5a071f75353f667787615249c62dcc7b15a4828f',
+ opaque: null,
+ stale: null,
+ qop: null,
+ };
+
+ const digest = new DigestAuthentication(credentials);
+
+ digest.authenticate({ method, ruri }, challenge);
+
+ expect(digest._response).toBe('f99e05f591f147facbc94ff23b4b1dee');
+ });
+
+ test('digest authenticate qop = auth', () => {
+ const method = 'REGISTER';
+ const ruri = 'sip:testrealm@host.com';
+ const cnonce = '0a4f113b';
+ const credentials = {
+ username: 'testuser',
+ password: 'testpassword',
+ realm: 'testrealm@host.com',
+ ha1: null,
+ };
+ const challenge = {
+ algorithm: 'MD5',
+ realm: 'testrealm@host.com',
+ nonce: '5a071f75353f667787615249c62dcc7b15a4828f',
+ opaque: null,
+ stale: null,
+ qop: 'auth',
+ };
+
+ const digest = new DigestAuthentication(credentials);
+
+ digest.authenticate({ method, ruri }, challenge, cnonce);
+
+ expect(digest._response).toBe('a69b9c2ea0dea1437a21df6ddc9b05e4');
+ });
+
+ test('digest authenticate qop = auth-int and empty body', () => {
+ const method = 'REGISTER';
+ const ruri = 'sip:testrealm@host.com';
+ const cnonce = '0a4f113b';
+ const credentials = {
+ username: 'testuser',
+ password: 'testpassword',
+ realm: 'testrealm@host.com',
+ ha1: null,
+ };
+ const challenge = {
+ algorithm: 'MD5',
+ realm: 'testrealm@host.com',
+ nonce: '5a071f75353f667787615249c62dcc7b15a4828f',
+ opaque: null,
+ stale: null,
+ qop: 'auth-int',
+ };
+
+ const digest = new DigestAuthentication(credentials);
+
+ digest.authenticate({ method, ruri }, challenge, cnonce);
+
+ expect(digest._response).toBe('82b3cab8b1c4df404434db6a0581650c');
+ });
+
+ test('digest authenticate qop = auth-int and non-empty body', () => {
+ const method = 'REGISTER';
+ const ruri = 'sip:testrealm@host.com';
+ const body = 'TEST BODY';
+ const cnonce = '0a4f113b';
+ const credentials = {
+ username: 'testuser',
+ password: 'testpassword',
+ realm: 'testrealm@host.com',
+ ha1: null,
+ };
+ const challenge = {
+ algorithm: 'MD5',
+ realm: 'testrealm@host.com',
+ nonce: '5a071f75353f667787615249c62dcc7b15a4828f',
+ opaque: null,
+ stale: null,
+ qop: 'auth-int',
+ };
+
+ const digest = new DigestAuthentication(credentials);
+
+ digest.authenticate({ method, ruri, body }, challenge, cnonce);
+
+ expect(digest._response).toBe('7bf0e9de3fbb5da121974509d617f532');
+ });
});
diff --git a/test/test-normalizeTarget.js b/test/test-normalizeTarget.js
index 7d48000..add8e69 100644
--- a/test/test-normalizeTarget.js
+++ b/test/test-normalizeTarget.js
@@ -1,63 +1,57 @@
require('./include/common');
const JsSIP = require('../');
-
-describe('normalizeTarget', () =>
-{
- test('valid targets', () =>
- {
- const domain = 'jssip.net';
-
- function test_ok(given_data, expected)
- {
- const uri = JsSIP.Utils.normalizeTarget(given_data, domain);
-
- expect(uri instanceof (JsSIP.URI)).toBeTruthy();
- expect(uri.toString()).toEqual(expected);
- }
-
- test_ok('%61lice', 'sip:alice@jssip.net');
- test_ok('ALICE', 'sip:ALICE@jssip.net');
- test_ok('alice@DOMAIN.com', 'sip:alice@domain.com');
- test_ok('iñaki', 'sip:i%C3%B1aki@jssip.net');
- test_ok('€€€', 'sip:%E2%82%AC%E2%82%AC%E2%82%AC@jssip.net');
- test_ok('iñaki@aliax.net', 'sip:i%C3%B1aki@aliax.net');
- test_ok('SIP:iñaki@aliax.net:7070', 'sip:i%C3%B1aki@aliax.net:7070');
- test_ok('SIPs:iñaki@aliax.net:7070', 'sip:i%C3%B1aki@aliax.net:7070');
- test_ok('ibc@gmail.com@aliax.net', 'sip:ibc%40gmail.com@aliax.net');
- test_ok('alice-1:passwd', 'sip:alice-1:passwd@jssip.net');
- test_ok('SIP:alice-2:passwd', 'sip:alice-2:passwd@jssip.net');
- test_ok('sips:alice-2:passwd', 'sip:alice-2:passwd@jssip.net');
- test_ok('alice-3:passwd@domain.COM', 'sip:alice-3:passwd@domain.com');
- test_ok('SIP:alice-4:passwd@domain.COM', 'sip:alice-4:passwd@domain.com');
- test_ok('sip:+1234@aliax.net', 'sip:+1234@aliax.net');
- test_ok('+999', 'sip:+999@jssip.net');
- test_ok('*999', 'sip:*999@jssip.net');
- test_ok('#999/?:1234', 'sip:%23999/?:1234@jssip.net');
- test_ok('tel:+12345678', 'sip:+12345678@jssip.net');
- test_ok('tel:(+34)-944-43-89', 'sip:+349444389@jssip.net');
- test_ok('+123.456.78-9', 'sip:+123456789@jssip.net');
- test_ok('+ALICE-123.456.78-9', 'sip:+ALICE-123.456.78-9@jssip.net');
- });
-
- test('invalid targets', () =>
- {
- const domain = 'jssip.net';
-
- function test_error(given_data)
- {
- expect(JsSIP.Utils.normalizeTarget(given_data, domain)).toBe(undefined);
- }
-
- test_error(null);
- test_error(undefined);
- test_error(NaN);
- test_error(false);
- test_error(true);
- test_error('');
- test_error('ibc@iñaki.com');
- test_error('ibc@aliax.net;;;;;');
-
- expect(JsSIP.Utils.normalizeTarget('alice')).toBe(undefined);
- });
+describe('normalizeTarget', () => {
+ test('valid targets', () => {
+ const domain = 'jssip.net';
+
+ function test_ok(given_data, expected) {
+ const uri = JsSIP.Utils.normalizeTarget(given_data, domain);
+
+ expect(uri instanceof JsSIP.URI).toBeTruthy();
+ expect(uri.toString()).toEqual(expected);
+ }
+
+ test_ok('%61lice', 'sip:alice@jssip.net');
+ test_ok('ALICE', 'sip:ALICE@jssip.net');
+ test_ok('alice@DOMAIN.com', 'sip:alice@domain.com');
+ test_ok('iñaki', 'sip:i%C3%B1aki@jssip.net');
+ test_ok('€€€', 'sip:%E2%82%AC%E2%82%AC%E2%82%AC@jssip.net');
+ test_ok('iñaki@aliax.net', 'sip:i%C3%B1aki@aliax.net');
+ test_ok('SIP:iñaki@aliax.net:7070', 'sip:i%C3%B1aki@aliax.net:7070');
+ test_ok('SIPs:iñaki@aliax.net:7070', 'sip:i%C3%B1aki@aliax.net:7070');
+ test_ok('ibc@gmail.com@aliax.net', 'sip:ibc%40gmail.com@aliax.net');
+ test_ok('alice-1:passwd', 'sip:alice-1:passwd@jssip.net');
+ test_ok('SIP:alice-2:passwd', 'sip:alice-2:passwd@jssip.net');
+ test_ok('sips:alice-2:passwd', 'sip:alice-2:passwd@jssip.net');
+ test_ok('alice-3:passwd@domain.COM', 'sip:alice-3:passwd@domain.com');
+ test_ok('SIP:alice-4:passwd@domain.COM', 'sip:alice-4:passwd@domain.com');
+ test_ok('sip:+1234@aliax.net', 'sip:+1234@aliax.net');
+ test_ok('+999', 'sip:+999@jssip.net');
+ test_ok('*999', 'sip:*999@jssip.net');
+ test_ok('#999/?:1234', 'sip:%23999/?:1234@jssip.net');
+ test_ok('tel:+12345678', 'sip:+12345678@jssip.net');
+ test_ok('tel:(+34)-944-43-89', 'sip:+349444389@jssip.net');
+ test_ok('+123.456.78-9', 'sip:+123456789@jssip.net');
+ test_ok('+ALICE-123.456.78-9', 'sip:+ALICE-123.456.78-9@jssip.net');
+ });
+
+ test('invalid targets', () => {
+ const domain = 'jssip.net';
+
+ function test_error(given_data) {
+ expect(JsSIP.Utils.normalizeTarget(given_data, domain)).toBe(undefined);
+ }
+
+ test_error(null);
+ test_error(undefined);
+ test_error(NaN);
+ test_error(false);
+ test_error(true);
+ test_error('');
+ test_error('ibc@iñaki.com');
+ test_error('ibc@aliax.net;;;;;');
+
+ expect(JsSIP.Utils.normalizeTarget('alice')).toBe(undefined);
+ });
});
diff --git a/test/test-parser.js b/test/test-parser.js
index 34cbef2..8970bbf 100644
--- a/test/test-parser.js
+++ b/test/test-parser.js
@@ -3,395 +3,411 @@ const JsSIP = require('../');
const testUA = require('./include/testUA');
const Parser = require('../src/Parser');
+describe('parser', () => {
+ test('parse URI', () => {
+ const data =
+ 'SIP:%61liCE@versaTICA.Com:6060;TRansport=TCp;Foo=ABc;baz?X-Header-1=AaA1&X-Header-2=BbB&x-header-1=AAA2';
+ const uri = JsSIP.URI.parse(data);
+
+ // Parsed data.
+ expect(uri instanceof JsSIP.URI).toBeTruthy();
+ expect(uri.scheme).toBe('sip');
+ expect(uri.user).toBe('aliCE');
+ expect(uri.host).toBe('versatica.com');
+ expect(uri.port).toBe(6060);
+ expect(uri.hasParam('transport')).toBe(true);
+ expect(uri.hasParam('nooo')).toBe(false);
+ expect(uri.getParam('transport')).toBe('tcp');
+ expect(uri.getParam('foo')).toBe('ABc');
+ expect(uri.getParam('baz')).toBe(null);
+ expect(uri.getParam('nooo')).toBe(undefined);
+ expect(uri.getHeader('x-header-1')).toEqual(['AaA1', 'AAA2']);
+ expect(uri.getHeader('X-HEADER-2')).toEqual(['BbB']);
+ expect(uri.getHeader('nooo')).toBe(undefined);
+ expect(uri.toString()).toBe(
+ 'sip:aliCE@versatica.com:6060;transport=tcp;foo=ABc;baz?X-Header-1=AaA1&X-Header-1=AAA2&X-Header-2=BbB'
+ );
+ expect(uri.toAor()).toBe('sip:aliCE@versatica.com');
+
+ // Alter data.
+ uri.user = 'Iñaki:PASSWD';
+ expect(uri.user).toBe('Iñaki:PASSWD');
+ expect(uri.deleteParam('foo')).toBe('ABc');
+ expect(uri.deleteHeader('x-header-1')).toEqual(['AaA1', 'AAA2']);
+ expect(uri.toString()).toBe(
+ 'sip:I%C3%B1aki:PASSWD@versatica.com:6060;transport=tcp;baz?X-Header-2=BbB'
+ );
+ expect(uri.toAor()).toBe('sip:I%C3%B1aki:PASSWD@versatica.com');
+ uri.clearParams();
+ uri.clearHeaders();
+ uri.port = null;
+ expect(uri.toString()).toBe('sip:I%C3%B1aki:PASSWD@versatica.com');
+ expect(uri.toAor()).toBe('sip:I%C3%B1aki:PASSWD@versatica.com');
+ });
+
+ test('parse NameAddr', () => {
+ const data =
+ ' "Iñaki ðđøþ foo \\"bar\\" \\\\\\\\ \\\\ \\\\d \\\\\\\\d \\\\\' \\\\\\"sdf\\\\\\"" ' +
+ '<SIP:%61liCE@versaTICA.Com:6060;TRansport=TCp;Foo=ABc;baz?X-Header-1=AaA1&X-Header-2=BbB&x-header-1=AAA2>;QWE=QWE;ASd';
+ const name = JsSIP.NameAddrHeader.parse(data);
+
+ // Parsed data.
+ expect(name instanceof JsSIP.NameAddrHeader).toBeTruthy();
+ expect(name.display_name).toBe(
+ 'Iñaki ðđøþ foo "bar" \\\\ \\ \\d \\\\d \\\' \\"sdf\\"'
+ );
+ expect(name.hasParam('qwe')).toBe(true);
+ expect(name.hasParam('asd')).toBe(true);
+ expect(name.hasParam('nooo')).toBe(false);
+ expect(name.getParam('qwe')).toBe('QWE');
+ expect(name.getParam('asd')).toBe(null);
+
+ const uri = name.uri;
+
+ expect(uri instanceof JsSIP.URI).toBeTruthy();
+ expect(uri.scheme).toBe('sip');
+ expect(uri.user).toBe('aliCE');
+ expect(uri.host).toBe('versatica.com');
+ expect(uri.port).toBe(6060);
+ expect(uri.hasParam('transport')).toBe(true);
+ expect(uri.hasParam('nooo')).toBe(false);
+ expect(uri.getParam('transport')).toBe('tcp');
+ expect(uri.getParam('foo')).toBe('ABc');
+ expect(uri.getParam('baz')).toBe(null);
+ expect(uri.getParam('nooo')).toBe(undefined);
+ expect(uri.getHeader('x-header-1')).toEqual(['AaA1', 'AAA2']);
+ expect(uri.getHeader('X-HEADER-2')).toEqual(['BbB']);
+ expect(uri.getHeader('nooo')).toBe(undefined);
+
+ // Alter data.
+ name.display_name = 'Foo Bar';
+ expect(name.display_name).toBe('Foo Bar');
+ name.display_name = null;
+ expect(name.display_name).toBe(null);
+ expect(name.toString()).toBe(
+ '<sip:aliCE@versatica.com:6060;transport=tcp;foo=ABc;baz?X-Header-1=AaA1&X-Header-1=AAA2&X-Header-2=BbB>;qwe=QWE;asd'
+ );
+ uri.user = 'Iñaki:PASSWD';
+ expect(uri.toAor()).toBe('sip:I%C3%B1aki:PASSWD@versatica.com');
+ });
+
+ test('parse invalid NameAddr with non UTF-8 characters', () => {
+ const buffer = Buffer.from([0xc0]);
+ const data = `"${buffer.toString()}"<sip:foo@bar.com>`;
+ const name = JsSIP.NameAddrHeader.parse(data);
+
+ // Parsed data.
+ expect(name instanceof JsSIP.NameAddrHeader).toBeTruthy();
+ expect(name.display_name).toBe(buffer.toString());
+
+ const uri = name.uri;
+
+ expect(uri instanceof JsSIP.URI).toBeTruthy();
+ expect(uri.scheme).toBe('sip');
+ expect(uri.user).toBe('foo');
+ expect(uri.host).toBe('bar.com');
+ expect(uri.port).toBe(undefined);
+ });
+
+ test('parse NameAddr with token display_name', () => {
+ const data =
+ 'Foo Foo Bar\tBaz<SIP:%61liCE@versaTICA.Com:6060;TRansport=TCp;Foo=ABc;baz?X-Header-1=AaA1&X-Header-2=BbB&x-header-1=AAA2>;QWE=QWE;ASd';
+ const name = JsSIP.NameAddrHeader.parse(data);
+
+ // Parsed data.
+ expect(name instanceof JsSIP.NameAddrHeader).toBeTruthy();
+ expect(name.display_name).toBe('Foo Foo Bar Baz');
+ });
+
+ test('parse NameAddr with no space between DQUOTE and LAQUOT', () => {
+ const data =
+ '"Foo"<SIP:%61liCE@versaTICA.Com:6060;TRansport=TCp;Foo=ABc;baz?X-Header-1=AaA1&X-Header-2=BbB&x-header-1=AAA2>;QWE=QWE;ASd';
+ const name = JsSIP.NameAddrHeader.parse(data);
+
+ // Parsed data.
+ expect(name instanceof JsSIP.NameAddrHeader).toBeTruthy();
+ expect(name.display_name).toBe('Foo');
+ });
+
+ test('parse NameAddr with no display_name', () => {
+ const data =
+ '<SIP:%61liCE@versaTICA.Com:6060;TRansport=TCp;Foo=ABc;baz?X-Header-1=AaA1&X-Header-2=BbB&x-header-1=AAA2>;QWE=QWE;ASd';
+ const name = JsSIP.NameAddrHeader.parse(data);
+
+ // Parsed data.
+ expect(name instanceof JsSIP.NameAddrHeader).toBeTruthy();
+ expect(name.display_name).toBe(undefined);
+ });
+
+ test('parse multiple Contact', () => {
+ const data =
+ '"Iñaki @ł€" <SIP:+1234@ALIAX.net;Transport=WS>;+sip.Instance="abCD", sip:bob@biloxi.COM;headerParam, <sip:DOMAIN.com:5>';
+ const contacts = JsSIP.Grammar.parse(data, 'Contact');
+
+ expect(contacts instanceof Array).toBeTruthy();
+ expect(contacts.length).toBe(3);
+ const c1 = contacts[0].parsed;
+ const c2 = contacts[1].parsed;
+ const c3 = contacts[2].parsed;
+
+ // Parsed data.
+ expect(c1 instanceof JsSIP.NameAddrHeader).toBeTruthy();
+ expect(c1.display_name).toBe('Iñaki @ł€');
+ expect(c1.hasParam('+sip.instance')).toBe(true);
+ expect(c1.hasParam('nooo')).toBe(false);
+ expect(c1.getParam('+SIP.instance')).toBe('"abCD"');
+ expect(c1.getParam('nooo')).toBe(undefined);
+ expect(c1.uri instanceof JsSIP.URI).toBeTruthy();
+ expect(c1.uri.scheme).toBe('sip');
+ expect(c1.uri.user).toBe('+1234');
+ expect(c1.uri.host).toBe('aliax.net');
+ expect(c1.uri.port).toBe(undefined);
+ expect(c1.uri.getParam('transport')).toBe('ws');
+ expect(c1.uri.getParam('foo')).toBe(undefined);
+ expect(c1.uri.getHeader('X-Header')).toBe(undefined);
+ expect(c1.toString()).toBe(
+ '"Iñaki @ł€" <sip:+1234@aliax.net;transport=ws>;+sip.instance="abCD"'
+ );
+
+ // Alter data.
+ c1.display_name = '€€€';
+ expect(c1.display_name).toBe('€€€');
+ c1.uri.user = '+999';
+ expect(c1.uri.user).toBe('+999');
+ c1.setParam('+sip.instance', '"zxCV"');
+ expect(c1.getParam('+SIP.instance')).toBe('"zxCV"');
+ c1.setParam('New-Param', null);
+ expect(c1.hasParam('NEW-param')).toBe(true);
+ c1.uri.setParam('New-Param', null);
+ expect(c1.toString()).toBe(
+ '"€€€" <sip:+999@aliax.net;transport=ws;new-param>;+sip.instance="zxCV";new-param'
+ );
+
+ // Parsed data.
+ expect(c2 instanceof JsSIP.NameAddrHeader).toBeTruthy();
+ expect(c2.display_name).toBe(undefined);
+ expect(c2.hasParam('HEADERPARAM')).toBe(true);
+ expect(c2.uri instanceof JsSIP.URI).toBeTruthy();
+ expect(c2.uri.scheme).toBe('sip');
+ expect(c2.uri.user).toBe('bob');
+ expect(c2.uri.host).toBe('biloxi.com');
+ expect(c2.uri.port).toBe(undefined);
+ expect(c2.uri.hasParam('headerParam')).toBe(false);
+ expect(c2.toString()).toBe('<sip:bob@biloxi.com>;headerparam');
+
+ // Alter data.
+ c2.display_name = '@ł€ĸłæß';
+ expect(c2.toString()).toBe('"@ł€ĸłæß" <sip:bob@biloxi.com>;headerparam');
+
+ // Parsed data.
+ expect(c3 instanceof JsSIP.NameAddrHeader).toBeTruthy();
+ expect(c3.displayName).toBe(undefined);
+ expect(c3.uri instanceof JsSIP.URI).toBeTruthy();
+ expect(c3.uri.scheme).toBe('sip');
+ expect(c3.uri.user).toBe(undefined);
+ expect(c3.uri.host).toBe('domain.com');
+ expect(c3.uri.port).toBe(5);
+ expect(c3.uri.hasParam('nooo')).toBe(false);
+ expect(c3.toString()).toBe('<sip:domain.com:5>');
+
+ // Alter data.
+ c3.uri.setParam('newUriParam', 'zxCV');
+ c3.setParam('newHeaderParam', 'zxCV');
+ expect(c3.toString()).toBe(
+ '<sip:domain.com:5;newuriparam=zxCV>;newheaderparam=zxCV'
+ );
+ });
+
+ test('parse Via', () => {
+ let data =
+ 'SIP / 3.0 \r\n / UDP [1:ab::FF]:6060 ;\r\n BRanch=1234;Param1=Foo;paRAM2;param3=Bar';
+ let via = JsSIP.Grammar.parse(data, 'Via');
+
+ expect(via.protocol).toBe('SIP');
+ expect(via.transport).toBe('UDP');
+ expect(via.host).toBe('[1:ab::FF]');
+ expect(via.host_type).toBe('IPv6');
+ expect(via.port).toBe(6060);
+ expect(via.branch).toBe('1234');
+ expect(via.params).toEqual({
+ param1: 'Foo',
+ param2: undefined,
+ param3: 'Bar',
+ });
+
+ data =
+ 'SIP / 3.0 \r\n / UDP [1:ab::FF]:6060 ;\r\n BRanch=1234;rport=1111;Param1=Foo;paRAM2;param3=Bar';
+ via = JsSIP.Grammar.parse(data, 'Via');
+
+ expect(via.protocol).toBe('SIP');
+ expect(via.transport).toBe('UDP');
+ expect(via.host).toBe('[1:ab::FF]');
+ expect(via.host_type).toBe('IPv6');
+ expect(via.port).toBe(6060);
+ expect(via.branch).toBe('1234');
+ expect(via.rport).toBe(1111);
+ expect(via.params).toEqual({
+ param1: 'Foo',
+ param2: undefined,
+ param3: 'Bar',
+ });
+
+ data =
+ 'SIP / 3.0 \r\n / UDP [1:ab::FF]:6060 ;\r\n BRanch=1234;rport;Param1=Foo;paRAM2;param3=Bar';
+ via = JsSIP.Grammar.parse(data, 'Via');
+
+ expect(via.protocol).toBe('SIP');
+ expect(via.transport).toBe('UDP');
+ expect(via.host).toBe('[1:ab::FF]');
+ expect(via.host_type).toBe('IPv6');
+ expect(via.port).toBe(6060);
+ expect(via.branch).toBe('1234');
+ expect(via.rport).toBe(undefined);
+ expect(via.params).toEqual({
+ param1: 'Foo',
+ param2: undefined,
+ param3: 'Bar',
+ });
+ });
+
+ test('parse CSeq', () => {
+ const data = '123456 CHICKEN';
+ const cseq = JsSIP.Grammar.parse(data, 'CSeq');
+
+ expect(cseq.value).toBe(123456);
+ expect(cseq.method).toBe('CHICKEN');
+ });
+
+ test('parse authentication challenge', () => {
+ const data =
+ 'Digest realm = "[1:ABCD::abc]", nonce = "31d0a89ed7781ce6877de5cb032bf114", qop="AUTH,autH-INt", algorithm = md5 , stale = TRUE , opaque = "00000188"';
+ const auth = JsSIP.Grammar.parse(data, 'challenge');
+
+ expect(auth.realm).toBe('[1:ABCD::abc]');
+ expect(auth.nonce).toBe('31d0a89ed7781ce6877de5cb032bf114');
+ expect(auth.qop).toEqual(['auth', 'auth-int']);
+ expect(auth.algorithm).toBe('MD5');
+ expect(auth.stale).toBe(true);
+ expect(auth.opaque).toBe('00000188');
+ });
+
+ test('parse Event', () => {
+ const data = 'Presence;Param1=QWe;paraM2';
+ const event = JsSIP.Grammar.parse(data, 'Event');
+
+ expect(event.event).toBe('presence');
+ expect(event.params).toEqual({ param1: 'QWe', param2: undefined });
+ });
+
+ test('parse Session-Expires', () => {
+ let data, session_expires;
+
+ data = '180;refresher=uac';
+ session_expires = JsSIP.Grammar.parse(data, 'Session_Expires');
+
+ expect(session_expires.expires).toBe(180);
+ expect(session_expires.refresher).toBe('uac');
+
+ data = '210 ; refresher = UAS ; foo = bar';
+ session_expires = JsSIP.Grammar.parse(data, 'Session_Expires');
+
+ expect(session_expires.expires).toBe(210);
+ expect(session_expires.refresher).toBe('uas');
+ });
+
+ test('parse Reason', () => {
+ let data, reason;
+
+ data = 'SIP ; cause = 488 ; text = "Wrong SDP"';
+ reason = JsSIP.Grammar.parse(data, 'Reason');
+
+ expect(reason.protocol).toBe('sip');
+ expect(reason.cause).toBe(488);
+ expect(reason.text).toBe('Wrong SDP');
+
+ data = 'ISUP; cause=500 ; LALA = foo';
+ reason = JsSIP.Grammar.parse(data, 'Reason');
+
+ expect(reason.protocol).toBe('isup');
+ expect(reason.cause).toBe(500);
+ expect(reason.text).toBe(undefined);
+ expect(reason.params.lala).toBe('foo');
+ });
+
+ test('parse host', () => {
+ let data, parsed;
+
+ data = 'versatica.com';
+ expect((parsed = JsSIP.Grammar.parse(data, 'host'))).not.toBe(-1);
+ expect(parsed.host_type).toBe('domain');
+
+ data = 'myhost123';
+ expect((parsed = JsSIP.Grammar.parse(data, 'host'))).not.toBe(-1);
+ expect(parsed.host_type).toBe('domain');
+
+ data = '1.2.3.4';
+ expect((parsed = JsSIP.Grammar.parse(data, 'host'))).not.toBe(-1);
+ expect(parsed.host_type).toBe('IPv4');
+
+ data = '[1:0:fF::432]';
+ expect((parsed = JsSIP.Grammar.parse(data, 'host'))).not.toBe(-1);
+ expect(parsed.host_type).toBe('IPv6');
+
+ data = '1.2.3.444';
+ expect((parsed = JsSIP.Grammar.parse(data, 'host'))).toBe(-1);
+
+ data = 'iñaki.com';
+ expect((parsed = JsSIP.Grammar.parse(data, 'host'))).toBe(-1);
+
+ data = '1.2.3.bar.qwe-asd.foo';
+ expect((parsed = JsSIP.Grammar.parse(data, 'host'))).not.toBe(-1);
+ expect(parsed.host_type).toBe('domain');
+
+ data = '1.2.3.4.bar.qwe-asd.foo';
+ expect((parsed = JsSIP.Grammar.parse(data, 'host'))).not.toBe(-1);
+ expect(parsed.host_type).toBe('domain');
+ });
+
+ test('parse Refer-To', () => {
+ let data, parsed;
+
+ data = 'sip:alice@versatica.com';
+ expect((parsed = JsSIP.Grammar.parse(data, 'Refer_To'))).not.toBe(-1);
+ expect(parsed.uri.scheme).toBe('sip');
+ expect(parsed.uri.user).toBe('alice');
+ expect(parsed.uri.host).toBe('versatica.com');
+
+ data = '<sip:bob@versatica.com?Accept-Contact=sip:bobsdesk.versatica.com>';
+ expect((parsed = JsSIP.Grammar.parse(data, 'Refer_To'))).not.toBe(-1);
+ expect(parsed.uri.scheme).toBe('sip');
+ expect(parsed.uri.user).toBe('bob');
+ expect(parsed.uri.host).toBe('versatica.com');
+ expect(parsed.uri.hasHeader('Accept-Contact')).toBe(true);
+ });
+
+ test('parse Replaces', () => {
+ let parsed;
-describe('parser', () =>
-{
- test('parse URI', () =>
- {
- const data = 'SIP:%61liCE@versaTICA.Com:6060;TRansport=TCp;Foo=ABc;baz?X-Header-1=AaA1&X-Header-2=BbB&x-header-1=AAA2';
- const uri = JsSIP.URI.parse(data);
-
- // Parsed data.
- expect(uri instanceof (JsSIP.URI)).toBeTruthy();
- expect(uri.scheme).toBe('sip');
- expect(uri.user).toBe('aliCE');
- expect(uri.host).toBe('versatica.com');
- expect(uri.port).toBe(6060);
- expect(uri.hasParam('transport')).toBe(true);
- expect(uri.hasParam('nooo')).toBe(false);
- expect(uri.getParam('transport')).toBe('tcp');
- expect(uri.getParam('foo')).toBe('ABc');
- expect(uri.getParam('baz')).toBe(null);
- expect(uri.getParam('nooo')).toBe(undefined);
- expect(uri.getHeader('x-header-1')).toEqual([ 'AaA1', 'AAA2' ]);
- expect(uri.getHeader('X-HEADER-2')).toEqual([ 'BbB' ]);
- expect(uri.getHeader('nooo')).toBe(undefined);
- expect(uri.toString()).toBe('sip:aliCE@versatica.com:6060;transport=tcp;foo=ABc;baz?X-Header-1=AaA1&X-Header-1=AAA2&X-Header-2=BbB');
- expect(uri.toAor()).toBe('sip:aliCE@versatica.com');
-
- // Alter data.
- uri.user = 'Iñaki:PASSWD';
- expect(uri.user).toBe('Iñaki:PASSWD');
- expect(uri.deleteParam('foo')).toBe('ABc');
- expect(uri.deleteHeader('x-header-1')).toEqual([ 'AaA1', 'AAA2' ]);
- expect(uri.toString()).toBe('sip:I%C3%B1aki:PASSWD@versatica.com:6060;transport=tcp;baz?X-Header-2=BbB');
- expect(uri.toAor()).toBe('sip:I%C3%B1aki:PASSWD@versatica.com');
- uri.clearParams();
- uri.clearHeaders();
- uri.port = null;
- expect(uri.toString()).toBe('sip:I%C3%B1aki:PASSWD@versatica.com');
- expect(uri.toAor()).toBe('sip:I%C3%B1aki:PASSWD@versatica.com');
- });
-
- test('parse NameAddr', () =>
- {
- const data = ' "Iñaki ðđøþ foo \\"bar\\" \\\\\\\\ \\\\ \\\\d \\\\\\\\d \\\\\' \\\\\\"sdf\\\\\\"" ' +
- '<SIP:%61liCE@versaTICA.Com:6060;TRansport=TCp;Foo=ABc;baz?X-Header-1=AaA1&X-Header-2=BbB&x-header-1=AAA2>;QWE=QWE;ASd';
- const name = JsSIP.NameAddrHeader.parse(data);
-
- // Parsed data.
- expect(name instanceof (JsSIP.NameAddrHeader)).toBeTruthy();
- expect(name.display_name).toBe('Iñaki ðđøþ foo "bar" \\\\ \\ \\d \\\\d \\\' \\"sdf\\"');
- expect(name.hasParam('qwe')).toBe(true);
- expect(name.hasParam('asd')).toBe(true);
- expect(name.hasParam('nooo')).toBe(false);
- expect(name.getParam('qwe')).toBe('QWE');
- expect(name.getParam('asd')).toBe(null);
-
- const uri = name.uri;
-
- expect(uri instanceof (JsSIP.URI)).toBeTruthy();
- expect(uri.scheme).toBe('sip');
- expect(uri.user).toBe('aliCE');
- expect(uri.host).toBe('versatica.com');
- expect(uri.port).toBe(6060);
- expect(uri.hasParam('transport')).toBe(true);
- expect(uri.hasParam('nooo')).toBe(false);
- expect(uri.getParam('transport')).toBe('tcp');
- expect(uri.getParam('foo')).toBe('ABc');
- expect(uri.getParam('baz')).toBe(null);
- expect(uri.getParam('nooo')).toBe(undefined);
- expect(uri.getHeader('x-header-1')).toEqual([ 'AaA1', 'AAA2' ]);
- expect(uri.getHeader('X-HEADER-2')).toEqual([ 'BbB' ]);
- expect(uri.getHeader('nooo')).toBe(undefined);
-
- // Alter data.
- name.display_name = 'Foo Bar';
- expect(name.display_name).toBe('Foo Bar');
- name.display_name = null;
- expect(name.display_name).toBe(null);
- expect(name.toString()).toBe('<sip:aliCE@versatica.com:6060;transport=tcp;foo=ABc;baz?X-Header-1=AaA1&X-Header-1=AAA2&X-Header-2=BbB>;qwe=QWE;asd');
- uri.user = 'Iñaki:PASSWD';
- expect(uri.toAor()).toBe('sip:I%C3%B1aki:PASSWD@versatica.com');
- });
-
- test('parse invalid NameAddr with non UTF-8 characters', () =>
- {
- const buffer = Buffer.from([ 0xC0 ]);
- const data = `"${buffer.toString()}"` +
- '<sip:foo@bar.com>';
- const name = JsSIP.NameAddrHeader.parse(data);
-
- // Parsed data.
- expect(name instanceof (JsSIP.NameAddrHeader)).toBeTruthy();
- expect(name.display_name).toBe(buffer.toString());
-
- const uri = name.uri;
-
- expect(uri instanceof (JsSIP.URI)).toBeTruthy();
- expect(uri.scheme).toBe('sip');
- expect(uri.user).toBe('foo');
- expect(uri.host).toBe('bar.com');
- expect(uri.port).toBe(undefined);
- });
-
- test('parse NameAddr with token display_name', () =>
- {
- const data = 'Foo Foo Bar\tBaz<SIP:%61liCE@versaTICA.Com:6060;TRansport=TCp;Foo=ABc;baz?X-Header-1=AaA1&X-Header-2=BbB&x-header-1=AAA2>;QWE=QWE;ASd';
- const name = JsSIP.NameAddrHeader.parse(data);
-
- // Parsed data.
- expect(name instanceof (JsSIP.NameAddrHeader)).toBeTruthy();
- expect(name.display_name).toBe('Foo Foo Bar Baz');
- });
-
- test('parse NameAddr with no space between DQUOTE and LAQUOT', () =>
- {
- const data = '"Foo"<SIP:%61liCE@versaTICA.Com:6060;TRansport=TCp;Foo=ABc;baz?X-Header-1=AaA1&X-Header-2=BbB&x-header-1=AAA2>;QWE=QWE;ASd';
- const name = JsSIP.NameAddrHeader.parse(data);
-
- // Parsed data.
- expect(name instanceof (JsSIP.NameAddrHeader)).toBeTruthy();
- expect(name.display_name).toBe('Foo');
- });
-
- test('parse NameAddr with no display_name', () =>
- {
- const data = '<SIP:%61liCE@versaTICA.Com:6060;TRansport=TCp;Foo=ABc;baz?X-Header-1=AaA1&X-Header-2=BbB&x-header-1=AAA2>;QWE=QWE;ASd';
- const name = JsSIP.NameAddrHeader.parse(data);
-
- // Parsed data.
- expect(name instanceof (JsSIP.NameAddrHeader)).toBeTruthy();
- expect(name.display_name).toBe(undefined);
- });
-
- test('parse multiple Contact', () =>
- {
- const data = '"Iñaki @ł€" <SIP:+1234@ALIAX.net;Transport=WS>;+sip.Instance="abCD", sip:bob@biloxi.COM;headerParam, <sip:DOMAIN.com:5>';
- const contacts = JsSIP.Grammar.parse(data, 'Contact');
-
- expect(contacts instanceof (Array)).toBeTruthy();
- expect(contacts.length).toBe(3);
- const c1 = contacts[0].parsed;
- const c2 = contacts[1].parsed;
- const c3 = contacts[2].parsed;
-
- // Parsed data.
- expect(c1 instanceof (JsSIP.NameAddrHeader)).toBeTruthy();
- expect(c1.display_name).toBe('Iñaki @ł€');
- expect(c1.hasParam('+sip.instance')).toBe(true);
- expect(c1.hasParam('nooo')).toBe(false);
- expect(c1.getParam('+SIP.instance')).toBe('"abCD"');
- expect(c1.getParam('nooo')).toBe(undefined);
- expect(c1.uri instanceof (JsSIP.URI)).toBeTruthy();
- expect(c1.uri.scheme).toBe('sip');
- expect(c1.uri.user).toBe('+1234');
- expect(c1.uri.host).toBe('aliax.net');
- expect(c1.uri.port).toBe(undefined);
- expect(c1.uri.getParam('transport')).toBe('ws');
- expect(c1.uri.getParam('foo')).toBe(undefined);
- expect(c1.uri.getHeader('X-Header')).toBe(undefined);
- expect(c1.toString()).toBe('"Iñaki @ł€" <sip:+1234@aliax.net;transport=ws>;+sip.instance="abCD"');
-
- // Alter data.
- c1.display_name = '€€€';
- expect(c1.display_name).toBe('€€€');
- c1.uri.user = '+999';
- expect(c1.uri.user).toBe('+999');
- c1.setParam('+sip.instance', '"zxCV"');
- expect(c1.getParam('+SIP.instance')).toBe('"zxCV"');
- c1.setParam('New-Param', null);
- expect(c1.hasParam('NEW-param')).toBe(true);
- c1.uri.setParam('New-Param', null);
- expect(c1.toString()).toBe('"€€€" <sip:+999@aliax.net;transport=ws;new-param>;+sip.instance="zxCV";new-param');
-
- // Parsed data.
- expect(c2 instanceof (JsSIP.NameAddrHeader)).toBeTruthy();
- expect(c2.display_name).toBe(undefined);
- expect(c2.hasParam('HEADERPARAM')).toBe(true);
- expect(c2.uri instanceof (JsSIP.URI)).toBeTruthy();
- expect(c2.uri.scheme).toBe('sip');
- expect(c2.uri.user).toBe('bob');
- expect(c2.uri.host).toBe('biloxi.com');
- expect(c2.uri.port).toBe(undefined);
- expect(c2.uri.hasParam('headerParam')).toBe(false);
- expect(c2.toString()).toBe('<sip:bob@biloxi.com>;headerparam');
-
- // Alter data.
- c2.display_name = '@ł€ĸłæß';
- expect(c2.toString()).toBe('"@ł€ĸłæß" <sip:bob@biloxi.com>;headerparam');
-
- // Parsed data.
- expect(c3 instanceof (JsSIP.NameAddrHeader)).toBeTruthy();
- expect(c3.displayName).toBe(undefined);
- expect(c3.uri instanceof (JsSIP.URI)).toBeTruthy();
- expect(c3.uri.scheme).toBe('sip');
- expect(c3.uri.user).toBe(undefined);
- expect(c3.uri.host).toBe('domain.com');
- expect(c3.uri.port).toBe(5);
- expect(c3.uri.hasParam('nooo')).toBe(false);
- expect(c3.toString()).toBe('<sip:domain.com:5>');
-
- // Alter data.
- c3.uri.setParam('newUriParam', 'zxCV');
- c3.setParam('newHeaderParam', 'zxCV');
- expect(c3.toString()).toBe('<sip:domain.com:5;newuriparam=zxCV>;newheaderparam=zxCV');
- });
-
- test('parse Via', () =>
- {
- let data = 'SIP / 3.0 \r\n / UDP [1:ab::FF]:6060 ;\r\n BRanch=1234;Param1=Foo;paRAM2;param3=Bar';
- let via = JsSIP.Grammar.parse(data, 'Via');
-
- expect(via.protocol).toBe('SIP');
- expect(via.transport).toBe('UDP');
- expect(via.host).toBe('[1:ab::FF]');
- expect(via.host_type).toBe('IPv6');
- expect(via.port).toBe(6060);
- expect(via.branch).toBe('1234');
- expect(via.params).toEqual({ param1: 'Foo', param2: undefined, param3: 'Bar' });
-
- data = 'SIP / 3.0 \r\n / UDP [1:ab::FF]:6060 ;\r\n BRanch=1234;rport=1111;Param1=Foo;paRAM2;param3=Bar';
- via = JsSIP.Grammar.parse(data, 'Via');
-
- expect(via.protocol).toBe('SIP');
- expect(via.transport).toBe('UDP');
- expect(via.host).toBe('[1:ab::FF]');
- expect(via.host_type).toBe('IPv6');
- expect(via.port).toBe(6060);
- expect(via.branch).toBe('1234');
- expect(via.rport).toBe(1111);
- expect(via.params).toEqual({ param1: 'Foo', param2: undefined, param3: 'Bar' });
-
- data = 'SIP / 3.0 \r\n / UDP [1:ab::FF]:6060 ;\r\n BRanch=1234;rport;Param1=Foo;paRAM2;param3=Bar';
- via = JsSIP.Grammar.parse(data, 'Via');
-
- expect(via.protocol).toBe('SIP');
- expect(via.transport).toBe('UDP');
- expect(via.host).toBe('[1:ab::FF]');
- expect(via.host_type).toBe('IPv6');
- expect(via.port).toBe(6060);
- expect(via.branch).toBe('1234');
- expect(via.rport).toBe(undefined);
- expect(via.params).toEqual({ param1: 'Foo', param2: undefined, param3: 'Bar' });
- });
-
- test('parse CSeq', () =>
- {
- const data = '123456 CHICKEN';
- const cseq = JsSIP.Grammar.parse(data, 'CSeq');
-
- expect(cseq.value).toBe(123456);
- expect(cseq.method).toBe('CHICKEN');
- });
-
- test('parse authentication challenge', () =>
- {
- const data = 'Digest realm = "[1:ABCD::abc]", nonce = "31d0a89ed7781ce6877de5cb032bf114", qop="AUTH,autH-INt", algorithm = md5 , stale = TRUE , opaque = "00000188"';
- const auth = JsSIP.Grammar.parse(data, 'challenge');
-
- expect(auth.realm).toBe('[1:ABCD::abc]');
- expect(auth.nonce).toBe('31d0a89ed7781ce6877de5cb032bf114');
- expect(auth.qop).toEqual([ 'auth', 'auth-int' ]);
- expect(auth.algorithm).toBe('MD5');
- expect(auth.stale).toBe(true);
- expect(auth.opaque).toBe('00000188');
- });
-
- test('parse Event', () =>
- {
- const data = 'Presence;Param1=QWe;paraM2';
- const event = JsSIP.Grammar.parse(data, 'Event');
-
- expect(event.event).toBe('presence');
- expect(event.params).toEqual({ param1: 'QWe', param2: undefined });
- });
-
- test('parse Session-Expires', () =>
- {
- let data, session_expires;
-
- data = '180;refresher=uac';
- session_expires = JsSIP.Grammar.parse(data, 'Session_Expires');
-
- expect(session_expires.expires).toBe(180);
- expect(session_expires.refresher).toBe('uac');
-
- data = '210 ; refresher = UAS ; foo = bar';
- session_expires = JsSIP.Grammar.parse(data, 'Session_Expires');
-
- expect(session_expires.expires).toBe(210);
- expect(session_expires.refresher).toBe('uas');
- });
-
- test('parse Reason', () =>
- {
- let data, reason;
-
- data = 'SIP ; cause = 488 ; text = "Wrong SDP"';
- reason = JsSIP.Grammar.parse(data, 'Reason');
-
- expect(reason.protocol).toBe('sip');
- expect(reason.cause).toBe(488);
- expect(reason.text).toBe('Wrong SDP');
-
- data = 'ISUP; cause=500 ; LALA = foo';
- reason = JsSIP.Grammar.parse(data, 'Reason');
-
- expect(reason.protocol).toBe('isup');
- expect(reason.cause).toBe(500);
- expect(reason.text).toBe(undefined);
- expect(reason.params.lala).toBe('foo');
- });
-
- test('parse host', () =>
- {
- let data, parsed;
-
- data = 'versatica.com';
- expect((parsed = JsSIP.Grammar.parse(data, 'host'))).not.toBe(-1);
- expect(parsed.host_type).toBe('domain');
-
- data = 'myhost123';
- expect((parsed = JsSIP.Grammar.parse(data, 'host'))).not.toBe(-1);
- expect(parsed.host_type).toBe('domain');
-
- data = '1.2.3.4';
- expect((parsed = JsSIP.Grammar.parse(data, 'host'))).not.toBe(-1);
- expect(parsed.host_type).toBe('IPv4');
-
- data = '[1:0:fF::432]';
- expect((parsed = JsSIP.Grammar.parse(data, 'host'))).not.toBe(-1);
- expect(parsed.host_type).toBe('IPv6');
-
- data = '1.2.3.444';
- expect((parsed = JsSIP.Grammar.parse(data, 'host'))).toBe(-1);
-
- data = 'iñaki.com';
- expect((parsed = JsSIP.Grammar.parse(data, 'host'))).toBe(-1);
-
- data = '1.2.3.bar.qwe-asd.foo';
- expect((parsed = JsSIP.Grammar.parse(data, 'host'))).not.toBe(-1);
- expect(parsed.host_type).toBe('domain');
-
- data = '1.2.3.4.bar.qwe-asd.foo';
- expect((parsed = JsSIP.Grammar.parse(data, 'host'))).not.toBe(-1);
- expect(parsed.host_type).toBe('domain');
- });
-
- test('parse Refer-To', () =>
- {
- let data, parsed;
-
- data = 'sip:alice@versatica.com';
- expect((parsed = JsSIP.Grammar.parse(data, 'Refer_To'))).not.toBe(-1);
- expect(parsed.uri.scheme).toBe('sip');
- expect(parsed.uri.user).toBe('alice');
- expect(parsed.uri.host).toBe('versatica.com');
-
- data = '<sip:bob@versatica.com?Accept-Contact=sip:bobsdesk.versatica.com>';
- expect((parsed = JsSIP.Grammar.parse(data, 'Refer_To'))).not.toBe(-1);
- expect(parsed.uri.scheme).toBe('sip');
- expect(parsed.uri.user).toBe('bob');
- expect(parsed.uri.host).toBe('versatica.com');
- expect(parsed.uri.hasHeader('Accept-Contact')).toBe(true);
- });
-
- test('parse Replaces', () =>
- {
- let parsed;
-
- const data = '5t2gpbrbi72v79p1i8mr;to-tag=03aq91cl9n;from-tag=kun98clbf7';
-
- expect((parsed = JsSIP.Grammar.parse(data, 'Replaces'))).not.toBe(-1);
- expect(parsed.call_id).toBe('5t2gpbrbi72v79p1i8mr');
- expect(parsed.to_tag).toBe('03aq91cl9n');
- expect(parsed.from_tag).toBe('kun98clbf7');
- });
-
- test('parse Status Line', () =>
- {
- const data = 'SIP/2.0 420 Bad Extension';
- let parsed;
-
- expect((parsed = JsSIP.Grammar.parse(data, 'Status_Line'))).not.toBe(-1);
- expect(parsed.status_code).toBe(420);
- });
-
- test('parse message', () =>
- {
- // eslint-disable-next-line no-multi-str
- const data = 'INVITE sip:bob@biloxi.com SIP/2.0\r\n\
+ const data = '5t2gpbrbi72v79p1i8mr;to-tag=03aq91cl9n;from-tag=kun98clbf7';
+
+ expect((parsed = JsSIP.Grammar.parse(data, 'Replaces'))).not.toBe(-1);
+ expect(parsed.call_id).toBe('5t2gpbrbi72v79p1i8mr');
+ expect(parsed.to_tag).toBe('03aq91cl9n');
+ expect(parsed.from_tag).toBe('kun98clbf7');
+ });
+
+ test('parse Status Line', () => {
+ const data = 'SIP/2.0 420 Bad Extension';
+ let parsed;
+
+ expect((parsed = JsSIP.Grammar.parse(data, 'Status_Line'))).not.toBe(-1);
+ expect(parsed.status_code).toBe(420);
+ });
+
+ test('parse message', () => {
+ const data =
+ // eslint-disable-next-line no-multi-str
+ 'INVITE sip:bob@biloxi.com SIP/2.0\r\n\
Via: SIP/2.0/TCP useragent.cisco.com;branch=z9hG4bK-a111\r\n\
To: <sip:bob@biloxi.com>\r\n\
From: "Anonymous" <sip:anonymous@anonymous.invalid>;tag=9802748\r\n\
@@ -401,24 +417,26 @@ Max-Forwards: 70\r\n\
Privacy: id\r\n\
P-Preferred-Identity: "Cullen Jennings" <sip:fluffy@cisco.com>\r\n\r\n';
- const config = testUA.UA_CONFIGURATION;
- const wsSocket = new JsSIP.WebSocketInterface(testUA.SOCKET_DESCRIPTION.url);
+ const config = testUA.UA_CONFIGURATION;
+ const wsSocket = new JsSIP.WebSocketInterface(
+ testUA.SOCKET_DESCRIPTION.url
+ );
- config.sockets = wsSocket;
+ config.sockets = wsSocket;
- const ua = new JsSIP.UA(config);
- const message = Parser.parseMessage(data, ua);
+ const ua = new JsSIP.UA(config);
+ const message = Parser.parseMessage(data, ua);
- expect(message.hasHeader('P-Preferred-Identity')).toBe(true);
+ expect(message.hasHeader('P-Preferred-Identity')).toBe(true);
- const pai = message.getHeader('P-Preferred-Identity');
- const nameAddress = JsSIP.NameAddrHeader.parse(pai);
+ const pai = message.getHeader('P-Preferred-Identity');
+ const nameAddress = JsSIP.NameAddrHeader.parse(pai);
- expect(nameAddress instanceof JsSIP.NameAddrHeader).toBeTruthy();
- expect(nameAddress.uri.user).toBe('fluffy');
- expect(nameAddress.uri.host).toBe('cisco.com');
+ expect(nameAddress instanceof JsSIP.NameAddrHeader).toBeTruthy();
+ expect(nameAddress.uri.user).toBe('fluffy');
+ expect(nameAddress.uri.host).toBe('cisco.com');
- expect(message.hasHeader('Privacy')).toBe(true);
- expect(message.getHeader('Privacy')).toBe('id');
- });
+ expect(message.hasHeader('Privacy')).toBe(true);
+ expect(message.getHeader('Privacy')).toBe('id');
+ });
});
diff --git a/test/test-properties.js b/test/test-properties.js
index 5b9d7d5..0a1d4ee 100644
--- a/test/test-properties.js
+++ b/test/test-properties.js
@@ -2,16 +2,12 @@ require('./include/common');
const JsSIP = require('../');
const pkg = require('../package.json');
+describe('Properties', () => {
+ test('should have a name property', () => {
+ expect(JsSIP.name).toEqual(pkg.title);
+ });
-describe('Properties', () =>
-{
- test('should have a name property', () =>
- {
- expect(JsSIP.name).toEqual(pkg.title);
- });
-
- test('should have a version property', () =>
- {
- expect(JsSIP.version).toEqual(pkg.version);
- });
+ test('should have a version property', () => {
+ expect(JsSIP.version).toEqual(pkg.version);
+ });
});
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..1251e87
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,43 @@
+{
+ "compileOnSave": true,
+ "compilerOptions": {
+ "lib": ["es2024", "dom"],
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "outDir": "lib",
+ "rootDir": "src",
+ "allowJs": true,
+ // Do not generate declaration files automatically yet.
+ // "declaration": true,
+ // "declarationMap": true,
+ // "emitDeclarationOnly": false,
+ // "declarationDir": "lib",
+ "isolatedModules": true,
+ "verbatimModuleSyntax": false,
+ "useDefineForClassFields": true,
+ "esModuleInterop": false,
+ "allowImportingTsExtensions": false,
+ "allowUnreachableCode": false,
+ "allowUnusedLabels": false,
+ "alwaysStrict": true,
+ "exactOptionalPropertyTypes": false,
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitAny": true,
+ "noImplicitOverride": true,
+ "noImplicitReturns": true,
+ "noImplicitThis": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noUncheckedIndexedAccess": true,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "strict": true,
+ "strictBindCallApply": true,
+ "strictBuiltinIteratorReturn": true,
+ "strictFunctionTypes": true,
+ "strictNullChecks": true,
+ "strictPropertyInitialization": true,
+ "useUnknownInCatchVariables": true,
+ "noUncheckedSideEffectImports": true,
+ },
+ "include": ["src"],
+}