All files digest-fetch-src.js

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 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208            1x 1x 1x 1x 1x 1x   1x   1x 61x 61x 61x 45x 16x         19x 19x 19x 19x 19x 19x   19x 19x       19x 4x 15x 4x   19x 19x 19x 19x       19x 19x                                             2x 2x     2x     2x 2x 2x 2x       2x 2x       11x       33x       12x 12x 11x   11x 11x 11x 11x 11x   11x 11x         11x 11x           11x   11x   11x 11x 11x   11x 11x 11x     11x 11x 11x       11x     11x 11x 11x 11x       14x   14x 1x 1x     13x   13x   13x   13x   13x   13x   13x 13x                 17x   17x 17x 17x 2x             13x 13x 416x   13x       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')
const md5 = require('md5')
const sha256 = require('js-sha256').sha256
const sha512256 = require('js-sha512').sha512_256
const base64 = require('base-64')
 
const supported_algorithms = ['MD5', 'MD5-sess', 'SHA-256', 'SHA-256-sess', 'SHA-512-256', 'SHA-512-256-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.hashFunc = md5;
    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'
    }
    if (algorithm.startsWith('SHA-256')) {
      this.hashFunc = sha256
    } else if (algorithm.startsWith('SHA-512-256')) {
      this.hashFunc = sha512256
    }
    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
    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
      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 = {}
    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 (typeof(_options.headers.set) == 'function') {
      _options.headers.set('Authorization', auth)
    }
 
    Iif (this.logger) this.logger.debug(options)
    return _options
  }
 
  computeHash(user, realm, password) {
    return this.hashWithAlgorithm(`${user}:${realm}:${password}`);
  }
 
  hashWithAlgorithm(data) {
    return this.hashFunc(data)
  }
 
  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 : this.computeHash(this.user, this.digest.realm, this.password)
    Iif (this.digest.algorithm.endsWith('-sess')) {
      ha1 = this.hashWithAlgorithm(`${ha1}:${this.digest.nonce}:${this.digest.cnonce}`);
    }
 
    // optional Hash(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 = ':' + hash(entityBody)
    }
    const ha2 = this.hashWithAlgorithm(`${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}`
    Iif (!this.digest.qop) _response = `${ha1}:${this.digest.nonce}:${ha2}`
    const response = this.hashWithAlgorithm(_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
    Iif (typeof(options.headers.set) == 'function') {
      options.headers.set('Authorization', digest)
    }
 
    Iif (this.logger) this.logger.debug(options)
 
    // const {factory, ..._options} = options
    const _options = {}
    Object.assign(_options, options)
    delete _options.factory
    return _options;
  }
 
  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