1 | const crypto = require('crypto');
|
2 |
|
3 | module.exports = class DigestClient {
|
4 |
|
5 | constructor(res) {
|
6 | this.res = res ;
|
7 | this.req = res.req ;
|
8 | this.agent = res.agent ;
|
9 |
|
10 | this.nc = 0;
|
11 | }
|
12 |
|
13 | authenticate(callback) {
|
14 | var options = this.req._originalParams.options ;
|
15 |
|
16 |
|
17 | var fn ;
|
18 | if (typeof options.auth === 'function') {
|
19 | fn = options.auth ;
|
20 | }
|
21 | else if (typeof options.auth === 'object') {
|
22 | fn = (req, res, callback) => { return callback(null, options.auth.username, options.auth.password); };
|
23 | }
|
24 | else {
|
25 | callback(new Error('no credentials were supplied to reply to server authentication challenge')) ;
|
26 | }
|
27 |
|
28 |
|
29 | fn(this.req, this.res, (err, username, password) => {
|
30 | if (err) {
|
31 | return callback(err);
|
32 | }
|
33 |
|
34 | var header = this.res.statusCode === 407 ? 'proxy-authenticate' : 'www-authenticate' ;
|
35 | var challenge = this._parseChallenge(this.res.get(header));
|
36 |
|
37 | var ha1 = crypto.createHash('md5');
|
38 | ha1.update([username, challenge.realm, password].join(':'));
|
39 | var ha2 = crypto.createHash('md5');
|
40 | ha2.update([options.method, options.uri].join(':'));
|
41 |
|
42 |
|
43 | var headers = options.headers || {};
|
44 | var seq = this.req.getParsedHeader('cseq').seq ;
|
45 | seq++ ;
|
46 | headers['CSeq'] = '' + seq + ' ' + this.req.method ;
|
47 | headers['call-id'] = this.req.get('call-id') ;
|
48 |
|
49 |
|
50 | delete headers.from;
|
51 | headers['From'] = this.req.get('from') ;
|
52 |
|
53 |
|
54 |
|
55 | var cnonce = false;
|
56 | var nc = false;
|
57 | if (typeof challenge.qop === 'string') {
|
58 | var cnonceHash = crypto.createHash('md5');
|
59 | cnonceHash.update(Math.random().toString(36));
|
60 | cnonce = cnonceHash.digest('hex').substr(0, 8);
|
61 | nc = this._updateNC();
|
62 | }
|
63 |
|
64 |
|
65 | var response = crypto.createHash('md5');
|
66 | var responseParams = [
|
67 | ha1.digest('hex'),
|
68 | challenge.nonce
|
69 | ];
|
70 |
|
71 | if (cnonce) {
|
72 | responseParams.push(nc);
|
73 | responseParams.push(cnonce);
|
74 | }
|
75 |
|
76 | if (challenge.qop) {
|
77 | responseParams.push(challenge.qop);
|
78 | }
|
79 | responseParams.push(ha2.digest('hex'));
|
80 | response.update(responseParams.join(':'));
|
81 |
|
82 |
|
83 | var authParams = {
|
84 | username: username,
|
85 | realm: challenge.realm,
|
86 | nonce: challenge.nonce,
|
87 | uri: options.uri,
|
88 | response: response.digest('hex'),
|
89 | algorithm: 'MD5'
|
90 | };
|
91 | if (challenge.qop) { authParams.qop = challenge.qop; }
|
92 | if (challenge.opaque) { authParams.opaque = challenge.opaque; }
|
93 |
|
94 | if (cnonce) {
|
95 | authParams.nc = nc;
|
96 | authParams.cnonce = cnonce;
|
97 | }
|
98 |
|
99 | headers[407 === this.res.statusCode ? 'Proxy-Authorization' : 'Authorization'] = this._compileParams(authParams);
|
100 |
|
101 | options.headers = headers;
|
102 |
|
103 |
|
104 |
|
105 |
|
106 | const originalUri = options.uri;
|
107 | if (!originalUri.match(/sips?:[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/)) {
|
108 | const proxy = originalUri.replace(/(sips*):[^;]*/, `$1:${this.res.source_address}:${this.res.source_port}`);
|
109 | Object.assign(options, {proxy});
|
110 | }
|
111 | this.agent.request(options, callback) ;
|
112 | }) ;
|
113 | }
|
114 |
|
115 | _updateNC() {
|
116 | const max = 99999999;
|
117 | this.nc++;
|
118 | if (this.nc > max) {
|
119 | this.nc = 1;
|
120 | }
|
121 | const padding = new Array(8).join('0') + '';
|
122 | const nc = this.nc + '';
|
123 | return padding.substr(0, 8 - nc.length) + nc;
|
124 | }
|
125 |
|
126 | _compileParams(params) {
|
127 | const parts = [];
|
128 | for (const i in params) {
|
129 | if (['nc', 'algorithm'].includes(i)) parts.push(`${i}=${params[i]}`);
|
130 | else parts.push(`${i}="${params[i]}"`);
|
131 | }
|
132 | return `Digest ${parts.join(',')}`;
|
133 | }
|
134 |
|
135 | _parseChallenge(digest) {
|
136 | const prefix = 'Digest ';
|
137 | const challenge = digest.substr(digest.indexOf(prefix) + prefix.length);
|
138 | const parts = challenge.split(',');
|
139 | const length = parts.length;
|
140 | const params = {};
|
141 | for (let i = 0; i < length; i++) {
|
142 | const part = parts[i].match(/^\s*?([a-zA-Z0-0]+)="?(.*?)"?\s*?$/);
|
143 | if (part && part.length > 2) {
|
144 | params[part[1]] = part[2];
|
145 | }
|
146 | }
|
147 | return params;
|
148 | }
|
149 | } ;
|