drachtio-srf 4.4.61 → 4.4.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/dialog.js +32 -13
- package/lib/drachtio-agent.js +5 -1
- package/lib/request.js +3 -3
- package/lib/response.js +2 -2
- package/lib/sip-parser/message.js +103 -0
- package/lib/sip-parser/parser.js +426 -0
- package/lib/srf.js +4 -4
- package/package.json +5 -2
- package/test/docker-compose-testbed.yaml +11 -0
- package/test/index.js +21 -9
- package/test/parser.js +8 -0
- package/test/scenarios/uas-bye-with-auth.xml +164 -0
- package/test/uac.js +66 -43
- package/test/unit-tests/parser.js +150 -0
package/lib/dialog.js
CHANGED
|
@@ -4,7 +4,7 @@ const only = require('only') ;
|
|
|
4
4
|
const methods = require('sip-methods') ;
|
|
5
5
|
const debug = require('debug')('drachtio:srf') ;
|
|
6
6
|
const SipError = require('./sip_error');
|
|
7
|
-
const { parseUri } = require('
|
|
7
|
+
const { parseUri } = require('./sip-parser/parser');
|
|
8
8
|
const sdpTransform = require('sdp-transform');
|
|
9
9
|
|
|
10
10
|
|
|
@@ -12,7 +12,7 @@ const sdpTransform = require('sdp-transform');
|
|
|
12
12
|
* Class representing a SIP Dialog.
|
|
13
13
|
*
|
|
14
14
|
* Note that instances of this class are not created directly by your code;
|
|
15
|
-
* rather they are returned from the {@link Srf#createUAC}, {@link Srf#
|
|
15
|
+
* rather they are returned from the {@link Srf#createUAC}, {@link Srf#createUAS}, and {@link Srf#createB2BUA}
|
|
16
16
|
* @class
|
|
17
17
|
* @extends EventEmitter
|
|
18
18
|
*/
|
|
@@ -34,6 +34,7 @@ class Dialog extends Emitter {
|
|
|
34
34
|
this.type = type ;
|
|
35
35
|
this.req = opts.req ;
|
|
36
36
|
this.res = opts.res ;
|
|
37
|
+
this.auth = opts.auth;
|
|
37
38
|
this.agent = this.res.agent ;
|
|
38
39
|
this.onHold = false ;
|
|
39
40
|
this.connected = true ;
|
|
@@ -171,7 +172,12 @@ class Dialog extends Emitter {
|
|
|
171
172
|
* or NOTIFY (in the case of SUBSCRIBE)
|
|
172
173
|
* @param {Object} [opts] configuration options
|
|
173
174
|
* @param {Object} [opts.headers] SIP headers to add to the outgoing BYE or NOTIFY
|
|
174
|
-
* @param
|
|
175
|
+
* @param {Object|Function} [opts.auth] sip credentials to use if challenged,
|
|
176
|
+
* or a function invoked with (req, res) and returning (err, username, password) where req is the
|
|
177
|
+
* request that was sent and res is the response that included the digest challenge
|
|
178
|
+
* @param {string} [opts.auth.username] sip username
|
|
179
|
+
* @param {string} [opts.auth.password] sip password
|
|
180
|
+
* @param {function} [callback] if provided, callback with signature <code>(err, msg)</code>
|
|
175
181
|
* that provides the BYE or NOTIFY message that was sent to terminate the dialog
|
|
176
182
|
* @return {Promise|Dialog} if no callback is supplied, otherwise a reference to the Dialog
|
|
177
183
|
*/
|
|
@@ -183,18 +189,29 @@ class Dialog extends Emitter {
|
|
|
183
189
|
}
|
|
184
190
|
this.queuedRequests = [];
|
|
185
191
|
|
|
192
|
+
const removeDialog = (err, res, callback) => {
|
|
193
|
+
this.connected = false ;
|
|
194
|
+
this.srf.removeDialog(this) ;
|
|
195
|
+
callback(err, res) ;
|
|
196
|
+
this.removeAllListeners();
|
|
197
|
+
};
|
|
198
|
+
|
|
186
199
|
const __x = (callback) => {
|
|
187
200
|
if (this.dialogType === 'INVITE') {
|
|
188
201
|
this.agent.request({
|
|
189
202
|
method: 'BYE',
|
|
190
203
|
headers: opts.headers || {},
|
|
191
204
|
stackDialogId: this.id,
|
|
205
|
+
auth: opts.auth || this.auth,
|
|
192
206
|
_socket: this.socket
|
|
193
207
|
}, (err, bye) => {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
208
|
+
if (err || !bye) {
|
|
209
|
+
return removeDialog(err, bye, callback);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
bye.on('response', () => {
|
|
213
|
+
removeDialog(err, bye, callback);
|
|
214
|
+
});
|
|
198
215
|
}) ;
|
|
199
216
|
if (this._emitter) {
|
|
200
217
|
Object.assign(this._state, {state: 'terminated'});
|
|
@@ -212,10 +229,7 @@ class Dialog extends Emitter {
|
|
|
212
229
|
stackDialogId: this.id,
|
|
213
230
|
_socket: this.socket
|
|
214
231
|
}, (err, notify) => {
|
|
215
|
-
|
|
216
|
-
this.srf.removeDialog(this) ;
|
|
217
|
-
callback(err, notify) ;
|
|
218
|
-
this.removeAllListeners();
|
|
232
|
+
removeDialog(err, notify, callback);
|
|
219
233
|
}) ;
|
|
220
234
|
}
|
|
221
235
|
};
|
|
@@ -309,7 +323,7 @@ class Dialog extends Emitter {
|
|
|
309
323
|
stackDialogId: this.id,
|
|
310
324
|
body: this.local.sdp,
|
|
311
325
|
_socket: this.socket,
|
|
312
|
-
headers:
|
|
326
|
+
headers:
|
|
313
327
|
opts.headers ? opts.headers : {'Contact': this.local.contact}
|
|
314
328
|
}, (err, req) => {
|
|
315
329
|
if (err) {
|
|
@@ -367,6 +381,11 @@ class Dialog extends Emitter {
|
|
|
367
381
|
* @param {string} opts.method - SIP method to use for the request
|
|
368
382
|
* @param {Object} [opts.headers] - SIP headers to apply to the request
|
|
369
383
|
* @param {string} [opts.body] - body of the SIP request
|
|
384
|
+
* @param {Object|Function} [opts.auth] sip credentials to use if challenged,
|
|
385
|
+
* or a function invoked with (req, res) and returning (err, username, password) where req is the
|
|
386
|
+
* request that was sent and res is the response that included the digest challenge
|
|
387
|
+
* @param {string} [opts.auth.username] sip username
|
|
388
|
+
* @param {string} [opts.auth.password] sip password
|
|
370
389
|
* @param {function} [callback] - callback invoked with signature <code>(err, req)</code>
|
|
371
390
|
* when operation has completed
|
|
372
391
|
* @return {Promise|Dialog} if no callback is supplied a Promise that resolves to the response received,
|
|
@@ -383,7 +402,7 @@ class Dialog extends Emitter {
|
|
|
383
402
|
method: method,
|
|
384
403
|
stackDialogId: this.id,
|
|
385
404
|
headers: opts.headers || {},
|
|
386
|
-
auth: opts.auth,
|
|
405
|
+
auth: opts.auth || this.auth,
|
|
387
406
|
_socket: this.socket,
|
|
388
407
|
body: opts.body
|
|
389
408
|
}, (err, req) => {
|
package/lib/drachtio-agent.js
CHANGED
|
@@ -2,7 +2,7 @@ const Emitter = require('events');
|
|
|
2
2
|
const debug = require('debug')('drachtio:agent');
|
|
3
3
|
const debugSocket = require('debug')('drachtio:socket');
|
|
4
4
|
const WireProtocol = require('./wire-protocol') ;
|
|
5
|
-
const SipMessage = require('
|
|
5
|
+
const SipMessage = require('./sip-parser/message');
|
|
6
6
|
const Request = require('./request') ;
|
|
7
7
|
const Response = require('./response') ;
|
|
8
8
|
const DigestClient = require('./digest-client') ;
|
|
@@ -658,6 +658,10 @@ class DrachtioAgent extends Emitter {
|
|
|
658
658
|
this.pendingSipAuthTxnIdUpdate.set(res.req.stackTxnId, {});
|
|
659
659
|
const client = new DigestClient(res) ;
|
|
660
660
|
client.authenticate((err, req) => {
|
|
661
|
+
if (!req) {
|
|
662
|
+
sr.req.emit('response', res, ack);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
661
665
|
// move all listeners from the old request to the new one we just generated
|
|
662
666
|
res.req.listeners('response').forEach((l) => { req.on('response', l) ; }) ;
|
|
663
667
|
res.req.emit('authenticate', req) ;
|
package/lib/request.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
const Emitter = require('events').EventEmitter ;
|
|
2
|
-
const sip = require('drachtio-sip') ;
|
|
3
2
|
const delegate = require('delegates') ;
|
|
4
3
|
const assert = require('assert') ;
|
|
5
4
|
const noop = require('node-noop').noop;
|
|
6
5
|
const debug = require('debug')('drachtio:request');
|
|
6
|
+
const SipMessage = require('./sip-parser/message');
|
|
7
7
|
|
|
8
8
|
class Request extends Emitter {
|
|
9
9
|
|
|
@@ -11,7 +11,7 @@ class Request extends Emitter {
|
|
|
11
11
|
super() ;
|
|
12
12
|
|
|
13
13
|
if (msg) {
|
|
14
|
-
assert(msg instanceof
|
|
14
|
+
assert(msg instanceof SipMessage) ;
|
|
15
15
|
this.msg = msg ;
|
|
16
16
|
this.meta = meta ;
|
|
17
17
|
}
|
|
@@ -148,7 +148,7 @@ class Request extends Emitter {
|
|
|
148
148
|
//add a new response to the array
|
|
149
149
|
const address = meta.address ;
|
|
150
150
|
const port = +meta.port;
|
|
151
|
-
const msg = new
|
|
151
|
+
const msg = new SipMessage(rawMsg) ;
|
|
152
152
|
const obj = {
|
|
153
153
|
time: meta.time,
|
|
154
154
|
status: msg.status,
|
package/lib/response.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
const Emitter = require('events').EventEmitter ;
|
|
2
|
-
const sip = require('drachtio-sip') ;
|
|
3
2
|
const delegate = require('delegates') ;
|
|
4
3
|
const status_codes = require('sip-status') ;
|
|
5
4
|
const only = require('only') ;
|
|
6
5
|
const noop = require('node-noop').noop;
|
|
7
6
|
const assert = require('assert') ;
|
|
8
7
|
const debug = require('debug')('drachtio:response');
|
|
8
|
+
const SipMessage = require('./sip-parser/message') ;
|
|
9
9
|
class Response extends Emitter {
|
|
10
10
|
|
|
11
11
|
constructor(agent) {
|
|
12
12
|
super() ;
|
|
13
13
|
this._agent = agent ;
|
|
14
|
-
this.msg = new
|
|
14
|
+
this.msg = new SipMessage() ;
|
|
15
15
|
this.finished = false ;
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const only = require('only') ;
|
|
2
|
+
const parser = require('./parser') ;
|
|
3
|
+
|
|
4
|
+
class SipMessage {
|
|
5
|
+
constructor(msg) {
|
|
6
|
+
|
|
7
|
+
this.headers = {};
|
|
8
|
+
|
|
9
|
+
if (msg) {
|
|
10
|
+
if (typeof msg === 'string') {
|
|
11
|
+
this.raw = msg ;
|
|
12
|
+
const obj = parser.parseSipMessage(msg, true) ;
|
|
13
|
+
if (!obj) throw new Error('failed to parse sip message');
|
|
14
|
+
msg = obj;
|
|
15
|
+
}
|
|
16
|
+
Object.assign(this.headers, msg.headers || {});
|
|
17
|
+
Object.assign(this, only(msg, 'body method version status reason uri payload'));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get type() {
|
|
22
|
+
if (this.method) return 'request' ;
|
|
23
|
+
else if (this.status) return 'response' ;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get calledNumber() {
|
|
27
|
+
const user = this.uri.match(/sips?:(.*?)@/) ;
|
|
28
|
+
if (user && user.length > 1) {
|
|
29
|
+
return user[1].split(';')[0] ;
|
|
30
|
+
}
|
|
31
|
+
return '' ;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get callingNumber() {
|
|
35
|
+
const header = this.has('p-asserted-identity') ? this.get('p-asserted-identity') : this.get('from') ;
|
|
36
|
+
const user = header.match(/sips?:(.*?)@/) ;
|
|
37
|
+
if (user && user.length > 1) {
|
|
38
|
+
return user[1].split(';')[0] ;
|
|
39
|
+
}
|
|
40
|
+
return '' ;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get callingName() {
|
|
44
|
+
const header = this.has('p-asserted-identity') ? this.get('p-asserted-identity') : this.get('from') ;
|
|
45
|
+
const user = header.match(/^\"(.+)\"\s*<sips?:.+@/);
|
|
46
|
+
if (user && user.length > 1) {
|
|
47
|
+
return user[1];
|
|
48
|
+
}
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get canFormDialog() {
|
|
53
|
+
return ('INVITE' === this.method || 'SUBSCRIBE' === this.method) && !this.get('to').tag ;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
set(hdr, value) {
|
|
57
|
+
const hdrs = {} ;
|
|
58
|
+
if (typeof hdr === 'string') hdrs[hdr] = value ;
|
|
59
|
+
else {
|
|
60
|
+
Object.assign(hdrs, hdr);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
Object.keys(hdrs).forEach((key) => {
|
|
64
|
+
const name = parser.getHeaderName(key) ;
|
|
65
|
+
const newValue = hdrs[key] ;
|
|
66
|
+
let v = '' ;
|
|
67
|
+
if (name in this.headers) {
|
|
68
|
+
v += this.headers[name] ;
|
|
69
|
+
v += ',' ;
|
|
70
|
+
}
|
|
71
|
+
v += newValue ;
|
|
72
|
+
this.headers[name] = v;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return this ;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get(hdr) {
|
|
79
|
+
if (this.has(hdr)) { return this.headers[parser.getHeaderName(hdr)] ; }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
has(hdr) {
|
|
83
|
+
const name = parser.getHeaderName(hdr) ;
|
|
84
|
+
return name in this.headers ;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getParsedHeader(hdr) {
|
|
88
|
+
const name = parser.getHeaderName(hdr) ;
|
|
89
|
+
const v = this.headers[name] ;
|
|
90
|
+
const fn = parser.getParser(hdr.toLowerCase()) ;
|
|
91
|
+
return fn({s:v, i:0}) ;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
toString() {
|
|
95
|
+
return parser.stringifySipMessage(this) ;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
SipMessage.parseUri = parser.parseUri ;
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
exports = module.exports = SipMessage ;
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
|
|
2
|
+
exports = module.exports = {
|
|
3
|
+
parseSipMessage: parseMessage,
|
|
4
|
+
stringifySipMessage: stringify,
|
|
5
|
+
stringifyUri: stringifyUri,
|
|
6
|
+
parseUri: parseUri,
|
|
7
|
+
getParser: function(hdr) {
|
|
8
|
+
return parsers[hdr] || parseGenericHeader ;
|
|
9
|
+
},
|
|
10
|
+
getStringifier: function(hdr) {
|
|
11
|
+
return stringifiers[hdr];
|
|
12
|
+
},
|
|
13
|
+
getHeaderName: getHeaderName
|
|
14
|
+
} ;
|
|
15
|
+
|
|
16
|
+
const headerNames = {
|
|
17
|
+
'call-id': 'Call-ID',
|
|
18
|
+
'content-length': 'Content-Length'
|
|
19
|
+
} ;
|
|
20
|
+
|
|
21
|
+
const customHeaderNames = [
|
|
22
|
+
'Diversion'
|
|
23
|
+
] ;
|
|
24
|
+
|
|
25
|
+
function getHeaderName(hdr) {
|
|
26
|
+
if (0 === hdr.indexOf('X-') ||
|
|
27
|
+
(0 === hdr.indexOf('P-') && 0 !== hdr.indexOf('P-Asserted')) ||
|
|
28
|
+
-1 !== customHeaderNames.indexOf(hdr)) return hdr ;
|
|
29
|
+
const name = unescape(hdr).toLowerCase();
|
|
30
|
+
return compactForm[name] || name;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseResponse(rs, m) {
|
|
34
|
+
const r = rs.match(/^SIP\/(\d+\.\d+)\s+(\d+)\s*(.*)\s*$/);
|
|
35
|
+
|
|
36
|
+
if (r) {
|
|
37
|
+
m.version = r[1];
|
|
38
|
+
m.status = +r[2];
|
|
39
|
+
m.reason = r[3];
|
|
40
|
+
|
|
41
|
+
return m;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseRequest(rq, m) {
|
|
46
|
+
const r = rq.match(/^([\w\-.!%*_+`'~]+)\s([^\s]+)\sSIP\s*\/\s*(\d+\.\d+)/);
|
|
47
|
+
|
|
48
|
+
if (r) {
|
|
49
|
+
m.method = unescape(r[1]);
|
|
50
|
+
m.uri = r[2];
|
|
51
|
+
m.version = r[3];
|
|
52
|
+
|
|
53
|
+
return m;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function applyRegex(regex, data) {
|
|
58
|
+
regex.lastIndex = data.i;
|
|
59
|
+
const r = regex.exec(data.s);
|
|
60
|
+
|
|
61
|
+
if (r && (r.index === data.i)) {
|
|
62
|
+
data.i = regex.lastIndex;
|
|
63
|
+
return r;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseParams(data, hdr) {
|
|
68
|
+
hdr.params = hdr.params || {};
|
|
69
|
+
|
|
70
|
+
const re = /\s*;\s*([\w\-.!%*_+`'~]+)(?:\s*=\s*([\w\-.!%*_+`'~]+|"[^"\\]*(\\.[^"\\]*)*"))?/g;
|
|
71
|
+
|
|
72
|
+
for (let r = applyRegex(re, data); r; r = applyRegex(re, data)) {
|
|
73
|
+
hdr.params[r[1].toLowerCase()] = r[2];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return hdr;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseMultiHeader(parser, d, h) {
|
|
80
|
+
h = h || [];
|
|
81
|
+
|
|
82
|
+
const re = /\s*,\s*/g;
|
|
83
|
+
do {
|
|
84
|
+
h.push(parser(d));
|
|
85
|
+
} while (d.i < d.s.length && applyRegex(re, d));
|
|
86
|
+
|
|
87
|
+
return h;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function parseGenericHeader(d, h) {
|
|
91
|
+
return h ? h + ',' + d.s : d.s;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseAOR(data) {
|
|
95
|
+
// eslint-disable-next-line max-len
|
|
96
|
+
const r = applyRegex(/((?:[\w\-.!%*_+`'~]+)(?:\s+[\w\-.!%*_+`'~]+)*|"[^"\\]*(?:\\.[^"\\]*)*")?\s*\<\s*([^>]*)\s*\>|((?:[^\s@"<]@)?[^\s;]+)/g, data);
|
|
97
|
+
return parseParams(data, {name: r[1], uri: r[2] || r[3] || ''});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseAorWithUri(data) {
|
|
101
|
+
const r = parseAOR(data);
|
|
102
|
+
r.uri = parseUri(r.uri);
|
|
103
|
+
return r;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function parseVia(data) {
|
|
107
|
+
const r = applyRegex(/SIP\s*\/\s*(\d+\.\d+)\s*\/\s*([\S]+)\s+([^\s;:]+)(?:\s*:\s*(\d+))?/g, data);
|
|
108
|
+
return parseParams(data, {version: r[1], protocol: r[2], host: r[3], port: r[4] && +r[4]});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function parseCSeq(d) {
|
|
112
|
+
const r = /(\d+)\s*([\S]+)/.exec(d.s);
|
|
113
|
+
return { seq: +r[1], method: unescape(r[2]) };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function parseAuthHeader(d) {
|
|
117
|
+
const r1 = applyRegex(/([^\s]*)\s+/g, d);
|
|
118
|
+
const a = {scheme: r1[1]};
|
|
119
|
+
|
|
120
|
+
let r2 = applyRegex(/([^\s,"=]*)\s*=\s*([^\s,"]+|"[^"\\]*(?:\\.[^"\\]*)*")\s*/g, d);
|
|
121
|
+
a[r2[1]] = r2[2];
|
|
122
|
+
|
|
123
|
+
while (r2 = applyRegex(/,\s*([^\s,"=]*)\s*=\s*([^\s,"]+|"[^"\\]*(?:\\.[^"\\]*)*")\s*/g, d)) {
|
|
124
|
+
a[r2[1]] = r2[2];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return a;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function parseAuthenticationInfoHeader(d) {
|
|
131
|
+
const a = {};
|
|
132
|
+
let r = applyRegex(/([^\s,"=]*)\s*=\s*([^\s,"]+|"[^"\\]*(?:\\.[^"\\]*)*")\s*/g, d);
|
|
133
|
+
a[r[1]] = r[2];
|
|
134
|
+
|
|
135
|
+
while (r = applyRegex(/,\s*([^\s,"=]*)\s*=\s*([^\s,"]+|"[^"\\]*(?:\\.[^"\\]*)*")\s*/g, d)) {
|
|
136
|
+
a[r[1]] = r[2];
|
|
137
|
+
}
|
|
138
|
+
return a;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const compactForm = {
|
|
142
|
+
i: 'call-id',
|
|
143
|
+
m: 'contact',
|
|
144
|
+
e: 'contact-encoding',
|
|
145
|
+
l: 'content-length',
|
|
146
|
+
c: 'content-type',
|
|
147
|
+
f: 'from',
|
|
148
|
+
s: 'subject',
|
|
149
|
+
k: 'supported',
|
|
150
|
+
t: 'to',
|
|
151
|
+
v: 'via'
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const parsers = {
|
|
155
|
+
'to': parseAOR,
|
|
156
|
+
'from': parseAOR,
|
|
157
|
+
'contact': function(v, h) {
|
|
158
|
+
if (v == '*')
|
|
159
|
+
return v;
|
|
160
|
+
else
|
|
161
|
+
return parseMultiHeader(parseAOR, v, h);
|
|
162
|
+
},
|
|
163
|
+
'route': parseMultiHeader.bind(0, parseAorWithUri),
|
|
164
|
+
'record-route': parseMultiHeader.bind(0, parseAorWithUri),
|
|
165
|
+
'path': parseMultiHeader.bind(0, parseAorWithUri),
|
|
166
|
+
'cseq': parseCSeq,
|
|
167
|
+
'content-length': function(v) { return +v.s; },
|
|
168
|
+
'via': parseMultiHeader.bind(0, parseVia),
|
|
169
|
+
'www-authenticate': parseMultiHeader.bind(0, parseAuthHeader),
|
|
170
|
+
'proxy-authenticate': parseMultiHeader.bind(0, parseAuthHeader),
|
|
171
|
+
'authorization': parseMultiHeader.bind(0, parseAuthHeader),
|
|
172
|
+
'proxy-authorization': parseMultiHeader.bind(0, parseAuthHeader),
|
|
173
|
+
'authentication-info': parseAuthenticationInfoHeader,
|
|
174
|
+
'refer-to': parseAOR,
|
|
175
|
+
'referred-by': parseAOR,
|
|
176
|
+
'p-asserted-identity': parseAOR,
|
|
177
|
+
'remote-party-id': parseAOR
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
function parse(data, lazy) {
|
|
182
|
+
data = data.split(/\r\n(?![ \t])/);
|
|
183
|
+
|
|
184
|
+
if (data[0] === '')
|
|
185
|
+
return;
|
|
186
|
+
|
|
187
|
+
const m = {};
|
|
188
|
+
|
|
189
|
+
if (!(parseResponse(data[0], m) || parseRequest(data[0], m)))
|
|
190
|
+
return;
|
|
191
|
+
|
|
192
|
+
m.headers = {};
|
|
193
|
+
|
|
194
|
+
for (let i = 1; i < data.length; ++i) {
|
|
195
|
+
const r = data[i].match(/^([\S]*?)\s*:\s*([\s\S]*)$/);
|
|
196
|
+
if (!r) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const name = getHeaderName(r[1]) ;
|
|
201
|
+
const fnParse = parsers[name] || parseGenericHeader ;
|
|
202
|
+
|
|
203
|
+
if (lazy === true) {
|
|
204
|
+
let v = '' ;
|
|
205
|
+
if (name in m.headers) {
|
|
206
|
+
v += m.headers[name] ;
|
|
207
|
+
v += ',' ;
|
|
208
|
+
}
|
|
209
|
+
v += r[2] ;
|
|
210
|
+
m.headers[name] = v ;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
m.headers[name] = fnParse({s:r[2], i:0}, m.headers[name]) ;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return m;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function parseUri(s) {
|
|
221
|
+
if (typeof s === 'object')
|
|
222
|
+
return s;
|
|
223
|
+
|
|
224
|
+
// eslint-disable-next-line max-len
|
|
225
|
+
//const re = /^(sips?):(?:([^\s>:@]+)(?::([^\s@>]+))?@)?([\w\-\.]+)(?::(\d+))?((?:;[^\s=\?>;]+(?:=[^\s?\;]+)?)*)(?:\?(([^\s&=>]+=[^\s&=>]+)(&[^\s&=>]+=[^\s&=>]+)*))?$/;
|
|
226
|
+
// eslint-disable-next-line max-len
|
|
227
|
+
const re = /^(sips?):(?:([^\s>:@]+)(?::([^\s@>]+))?@)?(?:(|(?:\[.*\])|(?:[0-9A-Za-z\-_]+\.)+[0-9A-Za-z\-_]+)|(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}))(?::(\d+))?((?:;[^\s=\?>;]+(?:=[^\s?\;]+)?)*)(?:\?(([^\s&=>]+=[^\s&=>]+)(&[^\s&=>]+=[^\s&=>]+)*))?$/;
|
|
228
|
+
|
|
229
|
+
const r = re.exec(s);
|
|
230
|
+
|
|
231
|
+
if (r) {
|
|
232
|
+
return {
|
|
233
|
+
family: /\[.*\]/.test(r[4]) ? 'ipv6' : 'ipv4',
|
|
234
|
+
scheme: r[1],
|
|
235
|
+
user: r[2],
|
|
236
|
+
password: r[3],
|
|
237
|
+
host: r[4],
|
|
238
|
+
port: +r[5],
|
|
239
|
+
params: (r[6].match(/([^;=]+)(=([^;=]+))?/g) || [])
|
|
240
|
+
.map(function(s) { return s.split('='); })
|
|
241
|
+
.reduce(function(params, x) { params[x[0]] = x[1] || null; return params;}, {}),
|
|
242
|
+
headers: ((r[7] || '').match(/[^&=]+=[^&=]+/g) || [])
|
|
243
|
+
.map(function(s) { return s.split('='); })
|
|
244
|
+
.reduce(function(params, x) { params[x[0]] = x[1]; return params; }, {})
|
|
245
|
+
} ;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function stringifyVersion(v) {
|
|
250
|
+
return v || '2.0';
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function stringifyParams(params) {
|
|
254
|
+
let s = '';
|
|
255
|
+
for (const n in params) {
|
|
256
|
+
s += ';' + n + (params[n] ? '=' + params[n] : '');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return s;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function stringifyUri(uri) {
|
|
263
|
+
if (typeof uri === 'string')
|
|
264
|
+
return uri;
|
|
265
|
+
|
|
266
|
+
let s = (uri.scheme || 'sip') + ':';
|
|
267
|
+
|
|
268
|
+
if (uri.user) {
|
|
269
|
+
if (uri.password)
|
|
270
|
+
s += uri.user + ':' + uri.password + '@';
|
|
271
|
+
else
|
|
272
|
+
s += uri.user + '@';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
s += uri.host;
|
|
276
|
+
|
|
277
|
+
if (uri.port)
|
|
278
|
+
s += ':' + uri.port;
|
|
279
|
+
|
|
280
|
+
if (uri.params)
|
|
281
|
+
s += stringifyParams(uri.params);
|
|
282
|
+
|
|
283
|
+
if (uri.headers) {
|
|
284
|
+
const h = Object.keys(uri.headers).map(function(x) { return x + '=' + uri.headers[x];}).join('&');
|
|
285
|
+
if (h.length) s += '?' + h;
|
|
286
|
+
}
|
|
287
|
+
return s;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function stringifyAOR(aor) {
|
|
291
|
+
return (aor.name || '') + ' <' + stringifyUri(aor.uri) + '>' + stringifyParams(aor.params);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function stringifyAuthHeader(a) {
|
|
295
|
+
const s = [];
|
|
296
|
+
|
|
297
|
+
for (const n in a) {
|
|
298
|
+
if (n !== 'scheme' && a[n] !== undefined) {
|
|
299
|
+
s.push(n + '=' + a[n]);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return a.scheme ? a.scheme + ' ' + s.join(',') : s.join(',');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
exports.stringifyAuthHeader = stringifyAuthHeader;
|
|
307
|
+
|
|
308
|
+
const stringifiers = {
|
|
309
|
+
via: function(h) {
|
|
310
|
+
return h.map(function(via) {
|
|
311
|
+
return 'Via: SIP/' + stringifyVersion(via.version) + '/' + via.protocol.toUpperCase() + ' ' +
|
|
312
|
+
via.host + (via.port ? ':' + via.port : '') + stringifyParams(via.params) + '\r\n';
|
|
313
|
+
}).join('');
|
|
314
|
+
},
|
|
315
|
+
to: function(h) {
|
|
316
|
+
return 'To: ' + stringifyAOR(h) + '\r\n';
|
|
317
|
+
},
|
|
318
|
+
from: function(h) {
|
|
319
|
+
return 'From: ' + stringifyAOR(h) + '\r\n';
|
|
320
|
+
},
|
|
321
|
+
contact: function(h) {
|
|
322
|
+
return 'Contact: ' + ((h !== '*' && h.length) ? h.map(stringifyAOR).join(', ') : '*') + '\r\n';
|
|
323
|
+
},
|
|
324
|
+
route: function(h) {
|
|
325
|
+
return h.length ? 'Route: ' + h.map(stringifyAOR).join(', ') + '\r\n' : '';
|
|
326
|
+
},
|
|
327
|
+
'record-route': function(h) {
|
|
328
|
+
return h.length ? 'Record-Route: ' + h.map(stringifyAOR).join(', ') + '\r\n' : '';
|
|
329
|
+
},
|
|
330
|
+
'path': function(h) {
|
|
331
|
+
return h.length ? 'Path: ' + h.map(stringifyAOR).join(', ') + '\r\n' : '';
|
|
332
|
+
},
|
|
333
|
+
cseq: function(cseq) {
|
|
334
|
+
return 'CSeq: ' + cseq.seq + ' ' + cseq.method + '\r\n';
|
|
335
|
+
},
|
|
336
|
+
'www-authenticate': function(h) {
|
|
337
|
+
return h.map(function(x) { return 'WWW-Authenticate: ' + stringifyAuthHeader(x) + '\r\n'; }).join('');
|
|
338
|
+
},
|
|
339
|
+
'proxy-authenticate': function(h) {
|
|
340
|
+
return h.map(function(x) { return 'Proxy-Authenticate: ' + stringifyAuthHeader(x) + '\r\n'; }).join('');
|
|
341
|
+
},
|
|
342
|
+
'authorization': function(h) {
|
|
343
|
+
return h.map(function(x) { return 'Authorization: ' + stringifyAuthHeader(x) + '\r\n'; }).join('');
|
|
344
|
+
},
|
|
345
|
+
'proxy-authorization': function(h) {
|
|
346
|
+
return h.map(function(x) { return 'Proxy-Authorization: ' + stringifyAuthHeader(x) + '\r\n'; }).join('');
|
|
347
|
+
},
|
|
348
|
+
'authentication-info': function(h) {
|
|
349
|
+
return 'Authentication-Info: ' + stringifyAuthHeader(h) + '\r\n';
|
|
350
|
+
},
|
|
351
|
+
'refer-to': function(h) { return 'Refer-To: ' + stringifyAOR(h) + '\r\n'; }
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
function stringify(m) {
|
|
355
|
+
let s;
|
|
356
|
+
if (m.status) {
|
|
357
|
+
s = 'SIP/' + stringifyVersion(m.version) + ' ' + m.status + ' ' + m.reason + '\r\n';
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
s = m.method + ' ' + stringifyUri(m.uri) + ' SIP/' + stringifyVersion(m.version) + '\r\n';
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
m.headers['content-length'] = (m.body || '').length;
|
|
364
|
+
|
|
365
|
+
for (const n in m.headers) {
|
|
366
|
+
if (typeof m.headers[n] === 'string' || !stringifiers[n])
|
|
367
|
+
s += (headerNames[n] || n) + ': ' + m.headers[n] + '\r\n';
|
|
368
|
+
else
|
|
369
|
+
s += stringifiers[n](m.headers[n].parsed, n);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
s += '\r\n';
|
|
373
|
+
|
|
374
|
+
if (m.body)
|
|
375
|
+
s += m.body;
|
|
376
|
+
|
|
377
|
+
return s;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
function parseMessage(s, lazy) {
|
|
382
|
+
const r = s.toString('utf8').split('\r\n\r\n');
|
|
383
|
+
if (r) {
|
|
384
|
+
const m = parse(r[0], lazy);
|
|
385
|
+
if (m) {
|
|
386
|
+
r.shift() ;
|
|
387
|
+
if (m.headers['content-length']) {
|
|
388
|
+
const body = 1 === r.length ? r[0] : r.join('\r\n\r\n') ;
|
|
389
|
+
const c = Math.max(0, Math.min(m.headers['content-length'], Buffer.byteLength(body, 'utf8')));
|
|
390
|
+
m.body = body.substring(0, c);
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
m.body = r[0];
|
|
394
|
+
}
|
|
395
|
+
m.payload = [] ;
|
|
396
|
+
|
|
397
|
+
let arr ;
|
|
398
|
+
if (r.length > 1 &&
|
|
399
|
+
m.headers['content-type'] &&
|
|
400
|
+
-1 !== m.headers['content-type'].indexOf('multipart/') &&
|
|
401
|
+
(arr = /.*;\s*boundary=\"?(.*?)\"?$/.exec(m.headers['content-type']))) {
|
|
402
|
+
|
|
403
|
+
const segments = m.body.split('--' + arr[1] + '\r\n');
|
|
404
|
+
if (0 === segments[0].length) {
|
|
405
|
+
segments.shift() ;
|
|
406
|
+
}
|
|
407
|
+
for (let i = 0; i < segments.length; i++) {
|
|
408
|
+
const ct = /Content-Type:\s*(.*)\r\n/.exec(segments[i]) ;
|
|
409
|
+
const stanzas = segments[i].split('\r\n\r\n') ;
|
|
410
|
+
m.payload.push({
|
|
411
|
+
type: ct ? ct[1] : null,
|
|
412
|
+
content: stanzas[1]
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
m.payload.push({
|
|
418
|
+
type: m.headers['content-type'],
|
|
419
|
+
content: m.body
|
|
420
|
+
}) ;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return m;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
package/lib/srf.js
CHANGED
|
@@ -3,7 +3,7 @@ const Dialog = require('./dialog') ;
|
|
|
3
3
|
const assert = require('assert') ;
|
|
4
4
|
const Emitter = require('events') ;
|
|
5
5
|
const delegate = require('delegates') ;
|
|
6
|
-
const parser = require('
|
|
6
|
+
const parser = require('./sip-parser/parser');
|
|
7
7
|
const methods = require('sip-methods') ;
|
|
8
8
|
const SipError = require('./sip_error') ;
|
|
9
9
|
const async = require('async') ;
|
|
@@ -557,7 +557,7 @@ class Srf extends Emitter {
|
|
|
557
557
|
return new Promise((resolve) => {
|
|
558
558
|
ack({body: localSdp}) ;
|
|
559
559
|
|
|
560
|
-
const dialog = new Dialog(this, 'uac', {req: req, res: res}) ;
|
|
560
|
+
const dialog = new Dialog(this, 'uac', {req: req, res: res, auth: opts.auth}) ;
|
|
561
561
|
dialog.local.sdp = localSdp ;
|
|
562
562
|
this.addDialog(dialog) ;
|
|
563
563
|
resolve(dialog) ;
|
|
@@ -582,7 +582,7 @@ class Srf extends Emitter {
|
|
|
582
582
|
|
|
583
583
|
if ((200 === res.status && method === 'INVITE') ||
|
|
584
584
|
((202 === res.status || 200 === res.status) && method === 'SUBSCRIBE')) {
|
|
585
|
-
const dialog = new Dialog(this, 'uac', {req: req, res: res}) ;
|
|
585
|
+
const dialog = new Dialog(this, 'uac', {req: req, res: res, auth: opts.auth}) ;
|
|
586
586
|
if (req._dialogState) {
|
|
587
587
|
dialog.stateEmitter = {
|
|
588
588
|
emitter: opts.dialogStateEmitter,
|
|
@@ -1670,7 +1670,7 @@ class Srf extends Emitter {
|
|
|
1670
1670
|
}
|
|
1671
1671
|
|
|
1672
1672
|
static get SipMessage() {
|
|
1673
|
-
return require('
|
|
1673
|
+
return require('./sip-parser/message');
|
|
1674
1674
|
}
|
|
1675
1675
|
|
|
1676
1676
|
static get SipRequest() {
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drachtio-srf",
|
|
3
|
-
"version": "4.4.
|
|
3
|
+
"version": "4.4.64",
|
|
4
4
|
"description": "drachtio signaling resource framework",
|
|
5
5
|
"main": "lib/srf.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "NODE_ENV=test node test/ ",
|
|
8
8
|
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
|
|
9
|
+
"unittests": "./node_modules/.bin/mocha ./test/unit-tests/",
|
|
9
10
|
"jslint": "eslint lib"
|
|
10
11
|
},
|
|
11
12
|
"engines": {
|
|
@@ -26,7 +27,6 @@
|
|
|
26
27
|
"debug": "^3.2.7",
|
|
27
28
|
"delegates": "^0.1.0",
|
|
28
29
|
"deprecate": "^1.1.1",
|
|
29
|
-
"drachtio-sip": "^0.6.3",
|
|
30
30
|
"node-noop": "0.0.1",
|
|
31
31
|
"only": "0.0.2",
|
|
32
32
|
"sdp-transform": "^2.14.1",
|
|
@@ -40,7 +40,10 @@
|
|
|
40
40
|
"config": "^3.3.3",
|
|
41
41
|
"eslint": "^7.18.0",
|
|
42
42
|
"eslint-plugin-promise": "^4.2.1",
|
|
43
|
+
"mocha": "^9.2.0",
|
|
43
44
|
"nyc": "^15.1.0",
|
|
45
|
+
"should": "^13.2.3",
|
|
46
|
+
"sip-message-examples": "0.0.6",
|
|
44
47
|
"tape": "^5.2.2"
|
|
45
48
|
}
|
|
46
49
|
}
|
|
@@ -150,3 +150,14 @@ services:
|
|
|
150
150
|
networks:
|
|
151
151
|
testbed:
|
|
152
152
|
ipv4_address: 172.29.0.23
|
|
153
|
+
|
|
154
|
+
sipp-uas-bye-with-auth:
|
|
155
|
+
image: drachtio/sipp:latest
|
|
156
|
+
command: sipp -sf /tmp/uas-bye-with-auth.xml
|
|
157
|
+
container_name: sipp-uas-bye-with-auth
|
|
158
|
+
volumes:
|
|
159
|
+
- ./scenarios:/tmp
|
|
160
|
+
tty: true
|
|
161
|
+
networks:
|
|
162
|
+
testbed:
|
|
163
|
+
ipv4_address: 172.29.0.24
|
package/test/index.js
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
|
-
require('
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
const exec = require('child_process').exec;
|
|
2
|
+
|
|
3
|
+
console.log('Run unit tests');
|
|
4
|
+
|
|
5
|
+
exec('"npm" run unittests', (err, stdout, stderr) => {
|
|
6
|
+
console.log(stdout);
|
|
7
|
+
|
|
8
|
+
console.log('Run integration tests');
|
|
9
|
+
|
|
10
|
+
require('./docker_start');
|
|
11
|
+
require('./b2b');
|
|
12
|
+
require('./reinvite-tests');
|
|
13
|
+
require('./uac');
|
|
14
|
+
require('./uas');
|
|
15
|
+
require('./proxy');
|
|
16
|
+
require('./utils');
|
|
17
|
+
require('./refer');
|
|
18
|
+
require('./docker_stop');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
|
package/test/parser.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="ISO-8859-1" ?>
|
|
2
|
+
<!DOCTYPE scenario SYSTEM "sipp.dtd">
|
|
3
|
+
|
|
4
|
+
<!-- This program is free software; you can redistribute it and/or -->
|
|
5
|
+
<!-- modify it under the terms of the GNU General Public License as -->
|
|
6
|
+
<!-- published by the Free Software Foundation; either version 2 of the -->
|
|
7
|
+
<!-- License, or (at your option) any later version. -->
|
|
8
|
+
<!-- -->
|
|
9
|
+
<!-- This program is distributed in the hope that it will be useful, -->
|
|
10
|
+
<!-- but WITHOUT ANY WARRANTY; without even the implied warranty of -->
|
|
11
|
+
<!-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -->
|
|
12
|
+
<!-- GNU General Public License for more details. -->
|
|
13
|
+
<!-- -->
|
|
14
|
+
<!-- You should have received a copy of the GNU General Public License -->
|
|
15
|
+
<!-- along with this program; if not, write to the -->
|
|
16
|
+
<!-- Free Software Foundation, Inc., -->
|
|
17
|
+
<!-- 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -->
|
|
18
|
+
<!-- -->
|
|
19
|
+
<!-- Sipp default 'uas' scenario. -->
|
|
20
|
+
<!-- -->
|
|
21
|
+
|
|
22
|
+
<scenario name="Basic UAS responder">
|
|
23
|
+
<!-- By adding rrs="true" (Record Route Sets), the route sets -->
|
|
24
|
+
<!-- are saved and used for following messages sent. Useful to test -->
|
|
25
|
+
<!-- against stateful SIP proxies/B2BUAs. -->
|
|
26
|
+
<recv request="INVITE" crlf="true">
|
|
27
|
+
</recv>
|
|
28
|
+
|
|
29
|
+
<send>
|
|
30
|
+
<![CDATA[
|
|
31
|
+
|
|
32
|
+
SIP/2.0 100 Trying
|
|
33
|
+
[last_Via:]
|
|
34
|
+
[last_From:]
|
|
35
|
+
[last_To:]
|
|
36
|
+
[last_Call-ID:]
|
|
37
|
+
[last_CSeq:]
|
|
38
|
+
Content-Length: 0
|
|
39
|
+
|
|
40
|
+
]]>
|
|
41
|
+
</send>
|
|
42
|
+
|
|
43
|
+
<send retrans="500">
|
|
44
|
+
<![CDATA[
|
|
45
|
+
|
|
46
|
+
SIP/2.0 407 Proxy Authentication Required
|
|
47
|
+
[last_Via:]
|
|
48
|
+
[last_From:]
|
|
49
|
+
[last_To:];tag=[pid]SIPpTag01[call_number]
|
|
50
|
+
[last_Call-ID:]
|
|
51
|
+
[last_CSeq:]
|
|
52
|
+
Proxy-Authenticate: Digest realm="local", nonce="4cdbb733645816512687270b83d2ae5d11e4d9d8"
|
|
53
|
+
Content-Length: 0
|
|
54
|
+
|
|
55
|
+
]]>
|
|
56
|
+
</send>
|
|
57
|
+
|
|
58
|
+
<recv request="ACK"
|
|
59
|
+
rtd="true"
|
|
60
|
+
crlf="true">
|
|
61
|
+
</recv>
|
|
62
|
+
|
|
63
|
+
<recv request="INVITE" crlf="true">
|
|
64
|
+
<action>
|
|
65
|
+
<ereg regexp=".*" search_in="hdr" header="Proxy-Authorization:" assign_to="1" />
|
|
66
|
+
</action>
|
|
67
|
+
</recv>
|
|
68
|
+
|
|
69
|
+
<send>
|
|
70
|
+
<![CDATA[
|
|
71
|
+
|
|
72
|
+
SIP/2.0 180 Ringing
|
|
73
|
+
[last_Via:]
|
|
74
|
+
[last_From:]
|
|
75
|
+
[last_To:];tag=[pid]SIPpTag01[call_number]
|
|
76
|
+
[last_Call-ID:]
|
|
77
|
+
[last_CSeq:]
|
|
78
|
+
Subject:[$1]
|
|
79
|
+
Content-Length: 0
|
|
80
|
+
|
|
81
|
+
]]>
|
|
82
|
+
</send>
|
|
83
|
+
|
|
84
|
+
<send retrans="500">
|
|
85
|
+
<![CDATA[
|
|
86
|
+
|
|
87
|
+
SIP/2.0 200 OK
|
|
88
|
+
[last_Via:]
|
|
89
|
+
[last_From:]
|
|
90
|
+
[last_To:];tag=[pid]SIPpTag01[call_number]
|
|
91
|
+
[last_Call-ID:]
|
|
92
|
+
[last_CSeq:]
|
|
93
|
+
Contact: <sip:[local_ip]:[local_port];transport=[transport]>
|
|
94
|
+
Content-Type: application/sdp
|
|
95
|
+
Content-Length: [len]
|
|
96
|
+
|
|
97
|
+
v=0
|
|
98
|
+
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
|
|
99
|
+
s=-
|
|
100
|
+
c=IN IP[media_ip_type] [media_ip]
|
|
101
|
+
t=0 0
|
|
102
|
+
m=audio [media_port] RTP/AVP 0
|
|
103
|
+
a=rtpmap:0 PCMU/8000
|
|
104
|
+
|
|
105
|
+
]]>
|
|
106
|
+
</send>
|
|
107
|
+
|
|
108
|
+
<recv request="ACK"
|
|
109
|
+
optional="true"
|
|
110
|
+
rtd="true"
|
|
111
|
+
crlf="true">
|
|
112
|
+
</recv>
|
|
113
|
+
<recv request="BYE">
|
|
114
|
+
</recv>
|
|
115
|
+
|
|
116
|
+
<send retrans="500">
|
|
117
|
+
<![CDATA[
|
|
118
|
+
|
|
119
|
+
SIP/2.0 407 Proxy Authentication Required
|
|
120
|
+
[last_Via:]
|
|
121
|
+
[last_From:]
|
|
122
|
+
[last_To:];tag=[pid]SIPpTag01[call_number]
|
|
123
|
+
[last_Call-ID:]
|
|
124
|
+
[last_CSeq:]
|
|
125
|
+
Proxy-Authenticate: Digest realm="local", nonce="4cdbb733645816512687270b83d2ae5d11e4d9d8"
|
|
126
|
+
Content-Length: 0
|
|
127
|
+
|
|
128
|
+
]]>
|
|
129
|
+
</send>
|
|
130
|
+
|
|
131
|
+
<recv request="BYE">
|
|
132
|
+
<action>
|
|
133
|
+
<ereg regexp=".*" search_in="hdr" header="Proxy-Authorization:" assign_to="1" />
|
|
134
|
+
</action>
|
|
135
|
+
</recv>
|
|
136
|
+
|
|
137
|
+
<send>
|
|
138
|
+
<![CDATA[
|
|
139
|
+
|
|
140
|
+
SIP/2.0 200 OK
|
|
141
|
+
[last_Via:]
|
|
142
|
+
[last_From:]
|
|
143
|
+
[last_To:]
|
|
144
|
+
[last_Call-ID:]
|
|
145
|
+
[last_CSeq:]
|
|
146
|
+
Contact: <sip:[local_ip]:[local_port];transport=[transport]>
|
|
147
|
+
Content-Length: 0
|
|
148
|
+
|
|
149
|
+
]]>
|
|
150
|
+
</send>
|
|
151
|
+
|
|
152
|
+
<!-- Keep the call open for a while in case the 200 is lost to be -->
|
|
153
|
+
<!-- able to retransmit it if we receive the BYE again. -->
|
|
154
|
+
<timewait milliseconds="4000"/>
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
<!-- definition of the response time repartition table (unit is ms) -->
|
|
158
|
+
<ResponseTimeRepartition value="10, 20, 30, 40, 50, 100, 150, 200"/>
|
|
159
|
+
|
|
160
|
+
<!-- definition of the call length repartition table (unit is ms) -->
|
|
161
|
+
<CallLengthRepartition value="10, 50, 100, 500, 1000, 5000, 10000"/>
|
|
162
|
+
|
|
163
|
+
</scenario>
|
|
164
|
+
|
package/test/uac.js
CHANGED
|
@@ -10,7 +10,7 @@ process.on('unhandledRejection', (reason, p) => {
|
|
|
10
10
|
function connect(srf) {
|
|
11
11
|
return new Promise((resolve, reject) => {
|
|
12
12
|
srf.connect(config.get('drachtio-sut'));
|
|
13
|
-
srf.on('connect', () => { resolve();});
|
|
13
|
+
srf.on('connect', () => { resolve(); });
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -40,7 +40,7 @@ test('UAC', (t) => {
|
|
|
40
40
|
headers: {
|
|
41
41
|
To: 'sip:dhorton@sip.drachtio.org',
|
|
42
42
|
From: 'sip:dhorton@sip.drachtio.org'
|
|
43
|
-
},
|
|
43
|
+
},
|
|
44
44
|
followRedirects: true,
|
|
45
45
|
keepUriOnRedirect: true
|
|
46
46
|
});
|
|
@@ -60,7 +60,7 @@ test('UAC', (t) => {
|
|
|
60
60
|
}
|
|
61
61
|
});
|
|
62
62
|
})
|
|
63
|
-
|
|
63
|
+
.then((uac) => {
|
|
64
64
|
srf.disconnect();
|
|
65
65
|
return t.pass('Srf#createUAC returns a Promise that resolves with the uac dialog');
|
|
66
66
|
})
|
|
@@ -94,52 +94,52 @@ test('UAC', (t) => {
|
|
|
94
94
|
})
|
|
95
95
|
.then(() => {
|
|
96
96
|
return srf.createUAC('sip:sipp-uas', {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
97
|
+
method: 'INVITE',
|
|
98
|
+
callingNumber: '12345',
|
|
99
|
+
headers: {
|
|
100
|
+
'Subject': 'sending Contact based on callingNumber'
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
103
|
})
|
|
104
104
|
.then((uac) => uac.destroy())
|
|
105
105
|
.then(() => {
|
|
106
106
|
srf.disconnect();
|
|
107
107
|
return t.pass('Srf#createUAC accepts opts.callingNumber');
|
|
108
108
|
})
|
|
109
|
-
|
|
109
|
+
|
|
110
110
|
.then(() => {
|
|
111
111
|
srf = new Srf();
|
|
112
112
|
return connect(srf);
|
|
113
113
|
})
|
|
114
114
|
.then(() => {
|
|
115
115
|
return srf.createUAC('sip:sipp-uas', {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
116
|
+
method: 'INVITE',
|
|
117
|
+
callingNumber: '12345',
|
|
118
|
+
headers: {
|
|
119
|
+
'Subject': 'sending explicit Contact',
|
|
120
|
+
'Contact': 'sip:foo@localhost'
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
123
|
})
|
|
124
124
|
.then((uac) => uac.destroy())
|
|
125
125
|
.then(() => {
|
|
126
126
|
srf.disconnect();
|
|
127
127
|
return t.pass('Srf#createUAC accepts opts.callingNumber');
|
|
128
128
|
})
|
|
129
|
-
|
|
129
|
+
|
|
130
130
|
.then(() => {
|
|
131
131
|
srf = new Srf();
|
|
132
132
|
return connect(srf);
|
|
133
133
|
})
|
|
134
134
|
.then(() => {
|
|
135
135
|
return srf.createUAC('sip:sipp-uas', {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
136
|
+
method: 'INVITE',
|
|
137
|
+
callingNumber: '12345',
|
|
138
|
+
headers: {
|
|
139
|
+
'Subject': 'sending explicit contact',
|
|
140
|
+
'contact': 'sip:foo@localhost'
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
143
|
})
|
|
144
144
|
.then((uac) => uac.destroy())
|
|
145
145
|
.then(() => {
|
|
@@ -215,6 +215,29 @@ test('UAC', (t) => {
|
|
|
215
215
|
srf.disconnect();
|
|
216
216
|
return t.pass('Srf#createUAC can handle digest authentication');
|
|
217
217
|
})
|
|
218
|
+
.then(() => {
|
|
219
|
+
srf = new Srf();
|
|
220
|
+
return connect(srf);
|
|
221
|
+
})
|
|
222
|
+
.then(() => {
|
|
223
|
+
return srf.createUAC('sip:172.29.0.24', {
|
|
224
|
+
method: 'INVITE',
|
|
225
|
+
headers: {
|
|
226
|
+
To: 'sip:dhorton@sip.drachtio.org',
|
|
227
|
+
From: 'sip:dhorton@sip.drachtio.org'
|
|
228
|
+
},
|
|
229
|
+
auth: {
|
|
230
|
+
username: 'foo',
|
|
231
|
+
password: 'bar'
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
})
|
|
235
|
+
.then((uac) => {
|
|
236
|
+
return uac.destroy().then(() => {
|
|
237
|
+
srf.disconnect();
|
|
238
|
+
return t.pass('Srf#createUAC can handle bye with digest authentication');
|
|
239
|
+
});
|
|
240
|
+
})
|
|
218
241
|
|
|
219
242
|
.then(() => {
|
|
220
243
|
srf = new Srf();
|
|
@@ -266,15 +289,15 @@ test('UAC', (t) => {
|
|
|
266
289
|
password: 'bar'
|
|
267
290
|
}
|
|
268
291
|
})
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
292
|
+
.then((req) => {
|
|
293
|
+
req.on('response', (res) => {
|
|
294
|
+
if (res.status === 200) return resolve();
|
|
295
|
+
reject(new Error(`REGISTER was rejected after auth with ${res.status}`));
|
|
296
|
+
});
|
|
297
|
+
})
|
|
298
|
+
.catch((err) => {
|
|
299
|
+
t.end(err, 'Srf#request returns a Promise');
|
|
273
300
|
});
|
|
274
|
-
})
|
|
275
|
-
.catch((err) => {
|
|
276
|
-
t.end(err, 'Srf#request returns a Promise');
|
|
277
|
-
});
|
|
278
301
|
});
|
|
279
302
|
})
|
|
280
303
|
.then((uac) => {
|
|
@@ -301,15 +324,15 @@ test('UAC', (t) => {
|
|
|
301
324
|
password: 'bar'
|
|
302
325
|
}
|
|
303
326
|
})
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
327
|
+
.then((req) => {
|
|
328
|
+
req.on('response', (res) => {
|
|
329
|
+
if (res.status === 200) return resolve();
|
|
330
|
+
reject(new Error(`REGISTER was rejected after auth with ${res.status}`));
|
|
331
|
+
});
|
|
332
|
+
})
|
|
333
|
+
.catch((err) => {
|
|
334
|
+
t.end(err, 'srf.request accepts opts.uri');
|
|
308
335
|
});
|
|
309
|
-
})
|
|
310
|
-
.catch((err) => {
|
|
311
|
-
t.end(err, 'srf.request accepts opts.uri');
|
|
312
|
-
});
|
|
313
336
|
});
|
|
314
337
|
})
|
|
315
338
|
.then((uac) => {
|
|
@@ -364,7 +387,7 @@ test('UAC', (t) => {
|
|
|
364
387
|
cbRequest: (err, req) => inviteSent = req,
|
|
365
388
|
cbProvisional: (response) => {
|
|
366
389
|
if (response.status < 200) {
|
|
367
|
-
inviteSent.cancel({headers: {'Reason': 'SIP;cause=200;text="Call completed elsewhere"'}});
|
|
390
|
+
inviteSent.cancel({ headers: { 'Reason': 'SIP;cause=200;text="Call completed elsewhere"' } });
|
|
368
391
|
}
|
|
369
392
|
}
|
|
370
393
|
}, (err, dlg) => {
|
|
@@ -386,4 +409,4 @@ test('UAC', (t) => {
|
|
|
386
409
|
t.error(err);
|
|
387
410
|
});
|
|
388
411
|
|
|
389
|
-
|
|
412
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
require('assert');
|
|
2
|
+
require('mocha');
|
|
3
|
+
require('should');
|
|
4
|
+
|
|
5
|
+
const assert = require('assert');
|
|
6
|
+
const examples = require('sip-message-examples');
|
|
7
|
+
const SipMessage = require('../../lib/sip-parser/message');
|
|
8
|
+
const parser = require('../../lib/sip-parser/parser');
|
|
9
|
+
const parseUri = parser.parseUri;
|
|
10
|
+
const Srf = require('../..');
|
|
11
|
+
assert.ok(typeof parseUri === 'function');
|
|
12
|
+
assert.ok(typeof Srf.parseUri === 'function');
|
|
13
|
+
console.log(`typeof parseUri is ${typeof parseUri}`);
|
|
14
|
+
console.log(`typeof Srf.parseUri is ${typeof Srf.parseUri}`);
|
|
15
|
+
|
|
16
|
+
describe('Parser', function () {
|
|
17
|
+
it('should provide headers as string values', function () {
|
|
18
|
+
var msg = new SipMessage(examples('invite'));
|
|
19
|
+
(typeof msg.get('from')).should.eql('string');
|
|
20
|
+
});
|
|
21
|
+
it('should optionally provide a parsed header', function () {
|
|
22
|
+
var msg = new SipMessage(examples('invite'));
|
|
23
|
+
var obj = msg.getParsedHeader('from');
|
|
24
|
+
obj.should.be.type('object');
|
|
25
|
+
obj.should.have.property('uri');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('getting a header should return the same value provided to set', function () {
|
|
29
|
+
var msg = new SipMessage();
|
|
30
|
+
msg.set('From', '<sip:daveh@localhost>;tag=1234');
|
|
31
|
+
msg.get('From').should.eql('<sip:daveh@localhost>;tag=1234');
|
|
32
|
+
});
|
|
33
|
+
it('setting a header should be case insensitive', function () {
|
|
34
|
+
var msg = new SipMessage();
|
|
35
|
+
msg.set('from', '<sip:daveh@localhost>;tag=1234');
|
|
36
|
+
msg.get('From').should.eql('<sip:daveh@localhost>;tag=1234');
|
|
37
|
+
});
|
|
38
|
+
it('getting a header should be case insensitive', function () {
|
|
39
|
+
var msg = new SipMessage();
|
|
40
|
+
msg.set('From', '<sip:daveh@localhost>;tag=1234');
|
|
41
|
+
msg.get('from').should.eql('<sip:daveh@localhost>;tag=1234');
|
|
42
|
+
});
|
|
43
|
+
it('should parse multiple headers into an array', function () {
|
|
44
|
+
var msg = new SipMessage(examples('invite'));
|
|
45
|
+
var via = msg.getParsedHeader('via');
|
|
46
|
+
via.should.be.an.array;
|
|
47
|
+
via.should.have.length(2);
|
|
48
|
+
});
|
|
49
|
+
it('should coalesce multiple calls to set', function () {
|
|
50
|
+
var msg = new SipMessage();
|
|
51
|
+
msg.set('via', 'SIP/2.0/UDP 10.1.10.101;branch=z9hG4bKac619477600');
|
|
52
|
+
msg.set('via', 'SIP/2.0/UDP 10.1.10.103;branch=z9hG4bKac619477603');
|
|
53
|
+
var via = msg.getParsedHeader('via');
|
|
54
|
+
via.should.be.an.array;
|
|
55
|
+
via.should.have.length(2);
|
|
56
|
+
parser.getStringifier('via')([via[1]]).should.eql('Via: SIP/2.0/UDP 10.1.10.103;branch=z9hG4bKac619477603\r\n');
|
|
57
|
+
});
|
|
58
|
+
it('should set multiple headers at once', function () {
|
|
59
|
+
var msg = new SipMessage();
|
|
60
|
+
msg.set({
|
|
61
|
+
to: '<sip:5753606@10.1.10.1>',
|
|
62
|
+
i: '619455480112200022407@10.1.10.101'
|
|
63
|
+
});
|
|
64
|
+
msg.get('call-id').should.eql('619455480112200022407@10.1.10.101');
|
|
65
|
+
msg.get('to').should.eql('<sip:5753606@10.1.10.1>');
|
|
66
|
+
})
|
|
67
|
+
it('should parse an invite request', function () {
|
|
68
|
+
|
|
69
|
+
var msg = new SipMessage(examples('invite'));
|
|
70
|
+
(msg.getParsedHeader('from').uri === null).should.be.false;
|
|
71
|
+
msg.type.should.eql('request');
|
|
72
|
+
(msg.body === null).should.be.false;
|
|
73
|
+
msg.canFormDialog.should.be.true;
|
|
74
|
+
});
|
|
75
|
+
it('should parse compact headers', function () {
|
|
76
|
+
|
|
77
|
+
var msg = new SipMessage(examples('invite-compact'));
|
|
78
|
+
msg.getParsedHeader('from').should.be.an.object;
|
|
79
|
+
msg.getParsedHeader('to').should.be.an.object;
|
|
80
|
+
msg.getParsedHeader('via').should.be.an.array;
|
|
81
|
+
});
|
|
82
|
+
it('should parse a response', function () {
|
|
83
|
+
var msg = new SipMessage(examples('200ok'));
|
|
84
|
+
msg.type.should.eql('response');
|
|
85
|
+
(msg.body === null).should.be.false;
|
|
86
|
+
});
|
|
87
|
+
it('should parse called number', function () {
|
|
88
|
+
var msg = new SipMessage(examples('invite'));
|
|
89
|
+
msg.calledNumber.should.eql('5753606');
|
|
90
|
+
});
|
|
91
|
+
it('should parse calling number', function () {
|
|
92
|
+
var msg = new SipMessage(examples('invite'));
|
|
93
|
+
msg.callingNumber.should.eql('4083084809');
|
|
94
|
+
});
|
|
95
|
+
it('should parse ipv4 dot decimal sip uri', function () {
|
|
96
|
+
var uri = parseUri('sip:104461@10.1.0.100:61219;rinstance=39ccb7d8db4387b1;transport=tcp');
|
|
97
|
+
uri.family.should.eql('ipv4');
|
|
98
|
+
uri.host.should.eql('10.1.0.100');
|
|
99
|
+
uri.port.should.eql(61219);
|
|
100
|
+
});
|
|
101
|
+
it('should parse ipv4 hostname sip uri', function () {
|
|
102
|
+
var uri = parseUri('sip:104461@foo.bar.com:61219;rinstance=39ccb7d8db4387b1;transport=tcp');
|
|
103
|
+
uri.family.should.eql('ipv4');
|
|
104
|
+
uri.host.should.eql('foo.bar.com');
|
|
105
|
+
uri.port.should.eql(61219);
|
|
106
|
+
});
|
|
107
|
+
it('should parse ipv6 sip uri', function () {
|
|
108
|
+
var uri = parseUri('sip:104461@[2601:182:cd00:d4c6:604b:16f1:3f5a:44f8]:61219;rinstance=39ccb7d8db4387b1;transport=tcp');
|
|
109
|
+
uri.family.should.eql('ipv6');
|
|
110
|
+
uri.host.should.eql('[2601:182:cd00:d4c6:604b:16f1:3f5a:44f8]');
|
|
111
|
+
uri.port.should.eql(61219);
|
|
112
|
+
});
|
|
113
|
+
it('should parse a sip uri with a dash or underscore', function () {
|
|
114
|
+
var uri = parseUri('sip:116751x0@cor10-san.sip.phone.com');
|
|
115
|
+
uri.family.should.eql('ipv4');
|
|
116
|
+
uri.host.should.eql('cor10-san.sip.phone.com');
|
|
117
|
+
uri.user.should.eql('116751x0');
|
|
118
|
+
});
|
|
119
|
+
it('should parse a sips uri', function () {
|
|
120
|
+
var uri = parseUri('sips:116751x0@cor10-san.sip.phone.com');
|
|
121
|
+
uri.family.should.eql('ipv4');
|
|
122
|
+
uri.host.should.eql('cor10-san.sip.phone.com');
|
|
123
|
+
uri.user.should.eql('116751x0');
|
|
124
|
+
uri.scheme.should.eql('sips');
|
|
125
|
+
});
|
|
126
|
+
it('should parse a multi-part header', function () {
|
|
127
|
+
var msg = new SipMessage(examples('siprec'));
|
|
128
|
+
msg.payload.length.should.eql(2);
|
|
129
|
+
});
|
|
130
|
+
it('should parse a multi-part header with whitespace before boundary', function () {
|
|
131
|
+
var msg = new SipMessage(examples('siprec2'));
|
|
132
|
+
msg.payload.length.should.eql(2);
|
|
133
|
+
});
|
|
134
|
+
it('should parse a multi-part header with quoted boundary', function () {
|
|
135
|
+
var msg = new SipMessage(examples('siprec3'));
|
|
136
|
+
msg.payload.length.should.eql(2);
|
|
137
|
+
});
|
|
138
|
+
it('should parse a sip uri with a dash or underscore', function () {
|
|
139
|
+
var uri = parseUri('sip:service@test_sipp-uas_1.com');
|
|
140
|
+
uri.family.should.eql('ipv4');
|
|
141
|
+
uri.host.should.eql('test_sipp-uas_1.com');
|
|
142
|
+
});
|
|
143
|
+
it('should parse calling name', function () {
|
|
144
|
+
var msg = new SipMessage();
|
|
145
|
+
msg.set('From', '"Dave" <sip:daveh@localhost>;tag=1234');
|
|
146
|
+
msg.get('From').should.eql('"Dave" <sip:daveh@localhost>;tag=1234');
|
|
147
|
+
msg.callingName.should.eql('Dave');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|