|
|
|
|
|
|
|
|
|
|
|
|
|
const canRequire = typeof(require) == 'function' |
|
if (typeof(fetch) !== 'function' && canRequire) var fetch = require('node-fetch') |
|
const md5 = require('md5') |
|
const base64 = require('base-64') |
|
|
|
const supported_algorithms = ['MD5', 'MD5-sess'] |
|
|
|
const parse = (raw, field, trim=true) => { |
|
const regex = new RegExp(`${field}=("[^"]*"|[^,]*)`, "i") |
|
const match = regex.exec(raw) |
|
if (match) |
|
return trim ? match[1].replace(/[\s"]/g, '') : match[1] |
|
return null |
|
} |
|
|
|
class DigestClient { |
|
constructor(user, password, options={}) { |
|
this.user = user |
|
this.password = password |
|
this.nonceRaw = 'abcdef0123456789' |
|
this.logger = options.logger |
|
this.precomputedHash = options.precomputedHash |
|
|
|
let algorithm = options.algorithm || 'MD5' |
|
if (!supported_algorithms.includes(algorithm)) { |
|
if (this.logger) this.logger.warn(`Unsupported algorithm ${algorithm}, will try with MD5`) |
|
algorithm = 'MD5' |
|
} |
|
this.digest = { nc: 0, algorithm, realm: '' } |
|
this.hasAuth = false |
|
const _cnonceSize = parseInt(options.cnonceSize) |
|
this.cnonceSize = isNaN(_cnonceSize) ? 32 : _cnonceSize |
|
|
|
|
|
|
|
this.statusCode = options.statusCode |
|
this.basic = options.basic || false |
|
} |
|
|
|
async fetch (url, options={}) { |
|
if (this.basic) return fetch(url, this.addBasicAuth(options)) |
|
const resp = await fetch(url, this.addAuth(url, options)) |
|
if (resp.status == 401 || (resp.status == this.statusCode && this.statusCode)) { |
|
this.hasAuth = false |
|
await this.parseAuth(resp.headers.get('www-authenticate')) |
|
if (this.hasAuth) { |
|
const respFinal = await fetch(url, this.addAuth(url, options)) |
|
if (respFinal.status == 401 || respFinal.status == this.statusCode) { |
|
this.hasAuth = false |
|
} else { |
|
this.digest.nc++ |
|
} |
|
return respFinal |
|
} |
|
} else this.digest.nc++ |
|
return resp |
|
} |
|
|
|
addBasicAuth (options={}) { |
|
let _options = {} |
|
if (typeof(options.factory) == 'function') { |
|
_options = options.factory() |
|
} else { |
|
_options = options |
|
} |
|
|
|
const auth = 'Basic ' + base64.encode(this.user + ":" + this.password) |
|
_options.headers = _options.headers || {} |
|
_options.headers.Authorization = auth; |
|
if (typeof(_options.headers.set) == 'function') { |
|
_options.headers.set('Authorization', auth) |
|
} |
|
|
|
if (this.logger) this.logger.debug(options) |
|
return _options |
|
} |
|
|
|
static computeHash(user, realm, password) { |
|
return md5(`${user}:${realm}:${password}`); |
|
} |
|
|
|
addAuth (url, options) { |
|
if (typeof(options.factory) == 'function') options = options.factory() |
|
if (!this.hasAuth) return options |
|
if (this.logger) this.logger.info(`requesting with auth carried`) |
|
|
|
const isRequest = typeof(url) === 'object' && typeof(url.url) === 'string' |
|
const urlStr = isRequest ? url.url : url |
|
const _url = urlStr.replace('//', '') |
|
const uri = _url.indexOf('/') == -1 ? '/' : _url.slice(_url.indexOf('/')) |
|
const method = options.method ? options.method.toUpperCase() : 'GET' |
|
|
|
let ha1 = this.precomputedHash ? this.password : DigestClient.computeHash(this.user, this.digest.realm, this.password) |
|
if (this.digest.algorithm === 'MD5-sess') { |
|
ha1 = md5(`${ha1}:${this.digest.nonce}:${this.digest.cnonce}`); |
|
} |
|
|
|
|
|
let _ha2 = '' |
|
if (this.digest.qop === 'auth-int') { |
|
|
|
if (this.logger) this.logger.warn('Sorry, auth-int is not implemented in this plugin') |
|
|
|
|
|
} |
|
const ha2 = md5(`${method}:${uri}${_ha2}`); |
|
|
|
const ncString = ('00000000'+this.digest.nc).slice(-8) |
|
|
|
let _response = `${ha1}:${this.digest.nonce}:${ncString}:${this.digest.cnonce}:${this.digest.qop}:${ha2}` |
|
if (!this.digest.qop) _response = `${ha1}:${this.digest.nonce}:${ha2}` |
|
const response = md5(_response); |
|
|
|
const opaqueString = this.digest.opaque !== null ? `opaque="${this.digest.opaque}",` : '' |
|
const qopString = this.digest.qop ? `qop="${this.digest.qop}",` : '' |
|
const digest = `${this.digest.scheme} username="${this.user}",realm="${this.digest.realm}",\ |
|
nonce="${this.digest.nonce}",uri="${uri}",${opaqueString}${qopString}\ |
|
algorithm="${this.digest.algorithm}",response="${response}",nc=${ncString},cnonce="${this.digest.cnonce}"` |
|
options.headers = options.headers || {} |
|
options.headers.Authorization = digest |
|
if (typeof(options.headers.set) == 'function') { |
|
options.headers.set('Authorization', digest) |
|
} |
|
|
|
if (this.logger) this.logger.debug(options) |
|
|
|
|
|
const _options = {} |
|
Object.assign(_options, options) |
|
delete _options.factory |
|
return _options; |
|
} |
|
|
|
async parseAuth (h) { |
|
this.lastAuth = h |
|
|
|
if (!h || h.length < 5) { |
|
this.hasAuth = false |
|
return |
|
} |
|
|
|
this.hasAuth = true |
|
|
|
this.digest.scheme = h.split(/\s/)[0] |
|
|
|
this.digest.realm = (parse(h, 'realm', false) || '').replace(/["]/g, '') |
|
|
|
this.digest.qop = this.parseQop(h) |
|
|
|
this.digest.opaque = parse(h, 'opaque') |
|
|
|
this.digest.nonce = parse(h, 'nonce') || '' |
|
|
|
this.digest.cnonce = this.makeNonce() |
|
this.digest.nc++ |
|
} |
|
|
|
parseQop (rawAuth) { |
|
|
|
|
|
|
|
|
|
|
|
const _qop = parse(rawAuth, 'qop') |
|
|
|
if (_qop !== null) { |
|
const qops = _qop.split(',') |
|
if (qops.includes('auth')) return 'auth' |
|
else if (qops.includes('auth-int')) return 'auth-int' |
|
} |
|
|
|
return null |
|
} |
|
|
|
makeNonce () { |
|
let uid = '' |
|
for (let i = 0; i < this.cnonceSize; ++i) { |
|
uid += this.nonceRaw[Math.floor(Math.random() * this.nonceRaw.length)]; |
|
} |
|
return uid |
|
} |
|
|
|
static parse(...args) { |
|
return parse(...args) |
|
} |
|
} |
|
|
|
if (typeof(window) === "object") window.DigestFetch = DigestClient |
|
module.exports = DigestClient |
|
|