All files digest-fetch-src.js

72.07% Statements 80/111
53.25% Branches 41/77
85.71% Functions 6/7
78.95% Lines 75/95
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 1641x 1x   1x 1x   1x       7x 7x 7x 7x   7x 7x       7x 7x 7x 7x       7x 7x                                             2x 2x     2x     2x 2x 2x   2x 2x       5x 5x 4x 4x 4x 4x   4x 4x         4x 4x           4x   4x   4x 4x 4x   4x 4x 4x     4x 4x 4x     4x 4x 4x 4x       5x   5x 1x 1x     4x   4x   4x 4x   4x   4x 4x   4x 4x   4x 4x           4x 4x 4x 4x               4x 4x 128x   4x         1x 1x  
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']
 
class DigestClient {
  constructor(user, password, options={}) {
    this.user = user
    this.password = password
    this.nonceRaw = 'abcdef0123456789'
    this.logger = options.logger
 
    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 }
    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
  }
 
  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 _url = url.replace('//', '')
    const uri = _url.indexOf('/') == -1 ? '/' : _url.slice(_url.indexOf('/'))
    const method = options.method ? options.method.toUpperCase() : 'GET'
 
    let ha1 = cryptojs.MD5(`${this.user}:${this.digest.realm}:${this.password}`).toString()
    Iif (this.digest.algorithm === 'MD5-sess') {
      ha1 = cryptojs.MD5(`${ha1}:${this.digest.nonce}:${this.digest.cnone}`).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 ? `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]
 
    const _realm = /realm=\"([^\"]+)\"/i.exec(h) 
    Eif (_realm) this.digest.realm = _realm[1]
 
    this.digest.qop = this.parseQop(h)
 
    const _opaque = /opaque=\"([^\"]+)\"/i.exec(h) 
    Iif (_opaque) this.digest.opaque = _opaque[1]
    
    const _nonce = /nonce=\"([^\"]+)\"/i.exec(h) 
    Eif (_nonce) this.digest.nonce = _nonce[1]
 
    this.digest.cnonce = this.makeNonce()
    this.digest.nc++
  }
 
  parseQop (rawAuth) {
    // Following https://en.wikipedia.org/wiki/Digest_access_authentication
    // to parse valid qop
    const _qop = /qop=\"([^\"]+)\"/i.exec(rawAuth)
    Eif (_qop) {
      const qops = _qop[1].replace(/\s/, '').split(',')
      Eif (qops.includes('auth')) return 'auth'
      else if (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
  }
 
}
 
Iif (typeof(window) === "object") window.DigestFetch = DigestClient
module.exports = DigestClient