UNPKG

4.68 kBJavaScriptView Raw
1const crypto = require('crypto');
2
3module.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 // get the username and password - either provided directly or via a callback
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 // note: we pass the original request and the 401 (or whatever) response in case the caller wants to see it
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 // bump CSeq and preserve Call-Id
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 // preserve tag on From header as well
50 delete headers.from;
51 headers['From'] = this.req.get('from') ;
52
53
54 // Generate cnonce
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 // Generate response hash
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 // Setup response parameters
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 // if the original request was sent to a request-uri that looks to have a DNS name,
104 // let's avoid another DNS lookup on the second request -- i.e. when challenged,
105 // we want to send our credentialled request to the same server that challenged us
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} ;