Software zum Installieren eines Smart-Mirror Frameworks , zum Nutzen von hochschulrelevanten Informationen, auf einem Raspberry-Pi.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

digest-fetch-src.js 6.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. /// !-----------------------------------------------------------------------------------------------------------
  2. /// |
  3. // | `digest-fetch` is a wrapper of `node-fetch` or `fetch` to provide http digest authentication boostraping.
  4. // |
  5. /// !-----------------------------------------------------------------------------------------------------------
  6. const canRequire = typeof(require) == 'function'
  7. if (typeof(fetch) !== 'function' && canRequire) var fetch = require('node-fetch')
  8. const md5 = require('md5')
  9. const base64 = require('base-64')
  10. const supported_algorithms = ['MD5', 'MD5-sess']
  11. const parse = (raw, field, trim=true) => {
  12. const regex = new RegExp(`${field}=("[^"]*"|[^,]*)`, "i")
  13. const match = regex.exec(raw)
  14. if (match)
  15. return trim ? match[1].replace(/[\s"]/g, '') : match[1]
  16. return null
  17. }
  18. class DigestClient {
  19. constructor(user, password, options={}) {
  20. this.user = user
  21. this.password = password
  22. this.nonceRaw = 'abcdef0123456789'
  23. this.logger = options.logger
  24. this.precomputedHash = options.precomputedHash
  25. let algorithm = options.algorithm || 'MD5'
  26. if (!supported_algorithms.includes(algorithm)) {
  27. if (this.logger) this.logger.warn(`Unsupported algorithm ${algorithm}, will try with MD5`)
  28. algorithm = 'MD5'
  29. }
  30. this.digest = { nc: 0, algorithm, realm: '' }
  31. this.hasAuth = false
  32. const _cnonceSize = parseInt(options.cnonceSize)
  33. this.cnonceSize = isNaN(_cnonceSize) ? 32 : _cnonceSize // cnonce length 32 as default
  34. // Custom authentication failure code for avoiding browser prompt:
  35. // https://stackoverflow.com/questions/9859627/how-to-prevent-browser-to-invoke-basic-auth-popup-and-handle-401-error-using-jqu
  36. this.statusCode = options.statusCode
  37. this.basic = options.basic || false
  38. }
  39. async fetch (url, options={}) {
  40. if (this.basic) return fetch(url, this.addBasicAuth(options))
  41. const resp = await fetch(url, this.addAuth(url, options))
  42. if (resp.status == 401 || (resp.status == this.statusCode && this.statusCode)) {
  43. this.hasAuth = false
  44. await this.parseAuth(resp.headers.get('www-authenticate'))
  45. if (this.hasAuth) {
  46. const respFinal = await fetch(url, this.addAuth(url, options))
  47. if (respFinal.status == 401 || respFinal.status == this.statusCode) {
  48. this.hasAuth = false
  49. } else {
  50. this.digest.nc++
  51. }
  52. return respFinal
  53. }
  54. } else this.digest.nc++
  55. return resp
  56. }
  57. addBasicAuth (options={}) {
  58. let _options = {}
  59. if (typeof(options.factory) == 'function') {
  60. _options = options.factory()
  61. } else {
  62. _options = options
  63. }
  64. const auth = 'Basic ' + base64.encode(this.user + ":" + this.password)
  65. _options.headers = _options.headers || {}
  66. _options.headers.Authorization = auth;
  67. if (typeof(_options.headers.set) == 'function') {
  68. _options.headers.set('Authorization', auth)
  69. }
  70. if (this.logger) this.logger.debug(options)
  71. return _options
  72. }
  73. static computeHash(user, realm, password) {
  74. return md5(`${user}:${realm}:${password}`);
  75. }
  76. addAuth (url, options) {
  77. if (typeof(options.factory) == 'function') options = options.factory()
  78. if (!this.hasAuth) return options
  79. if (this.logger) this.logger.info(`requesting with auth carried`)
  80. const isRequest = typeof(url) === 'object' && typeof(url.url) === 'string'
  81. const urlStr = isRequest ? url.url : url
  82. const _url = urlStr.replace('//', '')
  83. const uri = _url.indexOf('/') == -1 ? '/' : _url.slice(_url.indexOf('/'))
  84. const method = options.method ? options.method.toUpperCase() : 'GET'
  85. let ha1 = this.precomputedHash ? this.password : DigestClient.computeHash(this.user, this.digest.realm, this.password)
  86. if (this.digest.algorithm === 'MD5-sess') {
  87. ha1 = md5(`${ha1}:${this.digest.nonce}:${this.digest.cnonce}`);
  88. }
  89. // optional MD5(entityBody) for 'auth-int'
  90. let _ha2 = ''
  91. if (this.digest.qop === 'auth-int') {
  92. // not implemented for auth-int
  93. if (this.logger) this.logger.warn('Sorry, auth-int is not implemented in this plugin')
  94. // const entityBody = xxx
  95. // _ha2 = ':' + md5(entityBody)
  96. }
  97. const ha2 = md5(`${method}:${uri}${_ha2}`);
  98. const ncString = ('00000000'+this.digest.nc).slice(-8)
  99. let _response = `${ha1}:${this.digest.nonce}:${ncString}:${this.digest.cnonce}:${this.digest.qop}:${ha2}`
  100. if (!this.digest.qop) _response = `${ha1}:${this.digest.nonce}:${ha2}`
  101. const response = md5(_response);
  102. const opaqueString = this.digest.opaque !== null ? `opaque="${this.digest.opaque}",` : ''
  103. const qopString = this.digest.qop ? `qop="${this.digest.qop}",` : ''
  104. const digest = `${this.digest.scheme} username="${this.user}",realm="${this.digest.realm}",\
  105. nonce="${this.digest.nonce}",uri="${uri}",${opaqueString}${qopString}\
  106. algorithm="${this.digest.algorithm}",response="${response}",nc=${ncString},cnonce="${this.digest.cnonce}"`
  107. options.headers = options.headers || {}
  108. options.headers.Authorization = digest
  109. if (typeof(options.headers.set) == 'function') {
  110. options.headers.set('Authorization', digest)
  111. }
  112. if (this.logger) this.logger.debug(options)
  113. // const {factory, ..._options} = options
  114. const _options = {}
  115. Object.assign(_options, options)
  116. delete _options.factory
  117. return _options;
  118. }
  119. async parseAuth (h) {
  120. this.lastAuth = h
  121. if (!h || h.length < 5) {
  122. this.hasAuth = false
  123. return
  124. }
  125. this.hasAuth = true
  126. this.digest.scheme = h.split(/\s/)[0]
  127. this.digest.realm = (parse(h, 'realm', false) || '').replace(/["]/g, '')
  128. this.digest.qop = this.parseQop(h)
  129. this.digest.opaque = parse(h, 'opaque')
  130. this.digest.nonce = parse(h, 'nonce') || ''
  131. this.digest.cnonce = this.makeNonce()
  132. this.digest.nc++
  133. }
  134. parseQop (rawAuth) {
  135. // Following https://en.wikipedia.org/wiki/Digest_access_authentication
  136. // to parse valid qop
  137. // Samples
  138. // : qop="auth,auth-init",realm=
  139. // : qop=auth,realm=
  140. const _qop = parse(rawAuth, 'qop')
  141. if (_qop !== null) {
  142. const qops = _qop.split(',')
  143. if (qops.includes('auth')) return 'auth'
  144. else if (qops.includes('auth-int')) return 'auth-int'
  145. }
  146. // when not specified
  147. return null
  148. }
  149. makeNonce () {
  150. let uid = ''
  151. for (let i = 0; i < this.cnonceSize; ++i) {
  152. uid += this.nonceRaw[Math.floor(Math.random() * this.nonceRaw.length)];
  153. }
  154. return uid
  155. }
  156. static parse(...args) {
  157. return parse(...args)
  158. }
  159. }
  160. if (typeof(window) === "object") window.DigestFetch = DigestClient
  161. module.exports = DigestClient