1'use strict' 2 3const config = require('./config.js') 4const errors = require('./errors.js') 5const LRU = require('lru-cache') 6 7module.exports = checkResponse 8function checkResponse (method, res, registry, startTime, opts) { 9 opts = config(opts) 10 if (res.headers.has('npm-notice') && !res.headers.has('x-local-cache')) { 11 opts.log.notice('', res.headers.get('npm-notice')) 12 } 13 checkWarnings(res, registry, opts) 14 if (res.status >= 400) { 15 logRequest(method, res, startTime, opts) 16 return checkErrors(method, res, startTime, opts) 17 } else { 18 res.body.on('end', () => logRequest(method, res, startTime, opts)) 19 if (opts.ignoreBody) { 20 res.body.resume() 21 res.body = null 22 } 23 return res 24 } 25} 26 27function logRequest (method, res, startTime, opts) { 28 const elapsedTime = Date.now() - startTime 29 const attempt = res.headers.get('x-fetch-attempts') 30 const attemptStr = attempt && attempt > 1 ? ` attempt #${attempt}` : '' 31 const cacheStr = res.headers.get('x-local-cache') ? ' (from cache)' : '' 32 33 let urlStr 34 try { 35 const URL = require('url').URL 36 const url = new URL(res.url) 37 if (url.password) { 38 url.password = '***' 39 } 40 urlStr = url.toString() 41 } catch (er) { 42 urlStr = res.url 43 } 44 45 opts.log.http( 46 'fetch', 47 `${method.toUpperCase()} ${res.status} ${urlStr} ${elapsedTime}ms${attemptStr}${cacheStr}` 48 ) 49} 50 51const WARNING_REGEXP = /^\s*(\d{3})\s+(\S+)\s+"(.*)"\s+"([^"]+)"/ 52const BAD_HOSTS = new LRU({ max: 50 }) 53 54function checkWarnings (res, registry, opts) { 55 if (res.headers.has('warning') && !BAD_HOSTS.has(registry)) { 56 const warnings = {} 57 res.headers.raw()['warning'].forEach(w => { 58 const match = w.match(WARNING_REGEXP) 59 if (match) { 60 warnings[match[1]] = { 61 code: match[1], 62 host: match[2], 63 message: match[3], 64 date: new Date(match[4]) 65 } 66 } 67 }) 68 BAD_HOSTS.set(registry, true) 69 if (warnings['199']) { 70 if (warnings['199'].message.match(/ENOTFOUND/)) { 71 opts.log.warn('registry', `Using stale data from ${registry} because the host is inaccessible -- are you offline?`) 72 } else { 73 opts.log.warn('registry', `Unexpected warning for ${registry}: ${warnings['199'].message}`) 74 } 75 } 76 if (warnings['111']) { 77 // 111 Revalidation failed -- we're using stale data 78 opts.log.warn( 79 'registry', 80 `Using stale data from ${registry} due to a request error during revalidation.` 81 ) 82 } 83 } 84} 85 86function checkErrors (method, res, startTime, opts) { 87 return res.buffer() 88 .catch(() => null) 89 .then(body => { 90 let parsed = body 91 try { 92 parsed = JSON.parse(body.toString('utf8')) 93 } catch (e) {} 94 if (res.status === 401 && res.headers.get('www-authenticate')) { 95 const auth = res.headers.get('www-authenticate') 96 .split(/,\s*/) 97 .map(s => s.toLowerCase()) 98 if (auth.indexOf('ipaddress') !== -1) { 99 throw new errors.HttpErrorAuthIPAddress( 100 method, res, parsed, opts.spec 101 ) 102 } else if (auth.indexOf('otp') !== -1) { 103 throw new errors.HttpErrorAuthOTP( 104 method, res, parsed, opts.spec 105 ) 106 } else { 107 throw new errors.HttpErrorAuthUnknown( 108 method, res, parsed, opts.spec 109 ) 110 } 111 } else if (res.status === 401 && body != null && /one-time pass/.test(body.toString('utf8'))) { 112 // Heuristic for malformed OTP responses that don't include the www-authenticate header. 113 throw new errors.HttpErrorAuthOTP( 114 method, res, parsed, opts.spec 115 ) 116 } else { 117 throw new errors.HttpErrorGeneral( 118 method, res, parsed, opts.spec 119 ) 120 } 121 }) 122} 123