| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | 1x 1x 1x 1x 1x 1x 37x 37x 37x 27x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 2x 2x 2x 2x 2x 2x 2x 2x 5x 6x 6x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 8x 8x 1x 1x 7x 7x 7x 7x 7x 7x 7x 7x 11x 11x 11x 11x 2x 7x 7x 224x 7x 5x 1x 1x | /// !----------------------------------------------------------------------------------------------------------- /// | // | `digest-fetch` is a wrapper of `node-fetch` or `fetch` to provide http digest authentication boostraping. // | /// !----------------------------------------------------------------------------------------------------------- const canRequire = typeof(require) == 'function' Eif (typeof(fetch) !== 'function' && canRequire) var fetch = require('node-fetch') // if (typeof(cryptojs) !== 'function' && canRequire) var cryptojs = require('crypto-js') const cryptojs = require('crypto-js') 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' Iif (!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 // cnonce length 32 as default // Custom authentication failure code for avoiding browser prompt: // https://stackoverflow.com/questions/9859627/how-to-prevent-browser-to-invoke-basic-auth-popup-and-handle-401-error-using-jqu this.statusCode = options.statusCode || 401 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 == 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 == this.statusCode) { this.hasAuth = false } else { this.digest.nc++ } return respFinal } } else this.digest.nc++ return resp } addBasicAuth (options={}) { let _options = {} Iif (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 Iif (this.logger) this.logger.debug(options) return _options } static computeHash(user, realm, password) { return cryptojs.MD5(`${user}:${realm}:${password}`).toString(); } addAuth (url, options) { Iif (typeof(options.factory) == 'function') options = options.factory() if (!this.hasAuth) return options Iif (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) Iif (this.digest.algorithm === 'MD5-sess') { ha1 = cryptojs.MD5(`${ha1}:${this.digest.nonce}:${this.digest.cnonce}`).toString() } // optional MD5(entityBody) for 'auth-int' let _ha2 = '' Iif (this.digest.qop === 'auth-int') { // not implemented for auth-int if (this.logger) this.logger.warn('Sorry, auth-int is not implemented in this plugin') // const entityBody = xxx // _ha2 = ':' + cryptojs.MD5(entityBody).toString() } const ha2 = cryptojs.MD5(`${method}:${uri}${_ha2}`).toString() const ncString = ('00000000'+this.digest.nc).slice(-8) let _response = `${ha1}:${this.digest.nonce}:${ncString}:${this.digest.cnonce}:${this.digest.qop}:${ha2}` Iif (!this.digest.qop) _response = `${ha1}:${this.digest.nonce}:${ha2}` const response = cryptojs.MD5(_response).toString() 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 Iif (this.logger) this.logger.debug(options) // const {factory, ..._options} = 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) { // Following https://en.wikipedia.org/wiki/Digest_access_authentication // to parse valid qop // Samples // : qop="auth,auth-init",realm= // : qop=auth,realm= const _qop = parse(rawAuth, 'qop') Eif (_qop !== null) { const qops = _qop.split(',') if (qops.includes('auth')) return 'auth' else Eif (qops.includes('auth-int')) return 'auth-int' } // when not specified 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) } } Iif (typeof(window) === "object") window.DigestFetch = DigestClient module.exports = DigestClient |