-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(api): baseline kinda-working API impl
- Loading branch information
Showing
4 changed files
with
312 additions
and
114 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
'use strict' | ||
|
||
module.exports = getAuth | ||
function getAuth (conf) { | ||
const AUTH = {} | ||
const iterator = typeof conf.forEach === 'function' | ||
? conf | ||
: conf.keys | ||
iterator.forEach((k) => { | ||
const authMatchGlobal = k.match( | ||
/^(_authToken|username|_password|password|email|always-auth|_auth)$/ | ||
) | ||
const authMatchScoped = k[0] === '/' && k.match( | ||
/(.*):(_authToken|username|_password|password|email|always-auth|_auth)$/ | ||
) | ||
|
||
// if it matches scoped it will also match global | ||
if (authMatchGlobal || authMatchScoped) { | ||
let nerfDart = null | ||
let key = null | ||
let val = null | ||
|
||
if (authMatchScoped) { | ||
nerfDart = authMatchScoped[1] | ||
key = authMatchScoped[2] | ||
val = conf.get(k) | ||
if (!AUTH[nerfDart]) { | ||
AUTH[nerfDart] = { | ||
alwaysAuth: !!conf.get('always-auth') | ||
} | ||
} | ||
} else { | ||
key = authMatchGlobal[1] | ||
val = conf.get(k) | ||
AUTH.alwaysAuth = !!conf.get('always-auth') | ||
} | ||
|
||
const auth = authMatchScoped ? AUTH[nerfDart] : AUTH | ||
if (key === '_authToken') { | ||
auth.token = val | ||
} else if (key.match(/password$/i)) { | ||
auth.password = | ||
// the config file stores password auth already-encoded. pacote expects | ||
// the actual username/password pair. | ||
Buffer.from(val, 'base64').toString('utf8') | ||
} else if (key === 'always-auth') { | ||
auth.alwaysAuth = val === 'false' ? false : !!val | ||
} else { | ||
auth[key] = val | ||
} | ||
} | ||
}) | ||
return AUTH | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
'use strict' | ||
|
||
const errors = require('./errors.js') | ||
const LRU = require('lru-cache') | ||
|
||
module.exports = checkResponse | ||
function checkResponse (method, res, registry, startTime, opts) { | ||
if (res.headers.has('npm-notice') && !res.headers.has('x-local-cache')) { | ||
opts.log.notice('', res.headers.get('npm-notice')) | ||
} | ||
checkWarnings(res, registry, opts) | ||
if (res.status >= 400) { | ||
logRequest(method, res, startTime, opts) | ||
return checkErrors(method, res, startTime, opts) | ||
} else { | ||
res.body.on('end', () => logRequest(method, res, startTime, opts)) | ||
return res | ||
} | ||
} | ||
|
||
function logRequest (method, res, startTime, opts) { | ||
const elapsedTime = Date.now() - startTime | ||
const attempt = res.headers.get('x-fetch-attempts') | ||
const attemptStr = attempt && attempt > 1 ? ` attempt #${attempt}` : '' | ||
const cacheStr = res.headers.get('x-local-cache') ? ' (from cache)' : '' | ||
opts.log.http( | ||
'fetch', | ||
`${method.toUpperCase()} ${res.status} ${res.url} ${elapsedTime}ms${attemptStr}${cacheStr}` | ||
) | ||
} | ||
|
||
const WARNING_REGEXP = /^\s*(\d{3})\s+(\S+)\s+"(.*)"\s+"([^"]+)"/ | ||
const BAD_HOSTS = new LRU({ max: 50 }) | ||
|
||
function checkWarnings (res, registry, opts) { | ||
if (res.headers.has('warning') && !BAD_HOSTS.has(registry)) { | ||
const warnings = {} | ||
res.headers.raw()['warning'].forEach(w => { | ||
const match = w.match(WARNING_REGEXP) | ||
if (match) { | ||
warnings[match[1]] = { | ||
code: match[1], | ||
host: match[2], | ||
message: match[3], | ||
date: new Date(match[4]) | ||
} | ||
} | ||
}) | ||
BAD_HOSTS.set(registry, true) | ||
if (warnings['199']) { | ||
if (warnings['199'].message.match(/ENOTFOUND/)) { | ||
opts.log.warn('registry', `Using stale data from ${registry} because the host is inaccessible -- are you offline?`) | ||
} else { | ||
opts.log.warn('registry', `Unexpected warning for ${registry}: ${warnings['199'].message}`) | ||
} | ||
} | ||
if (warnings['111']) { | ||
// 111 Revalidation failed -- we're using stale data | ||
opts.log.warn( | ||
'registry', | ||
`Using stale data from ${registry} due to a request error during revalidation.` | ||
) | ||
} | ||
} | ||
} | ||
|
||
function checkErrors (method, res, startTime, opts) { | ||
return res.buffer() | ||
.catch(() => null) | ||
.then(body => { | ||
try { | ||
body = JSON.parse(body.toString('utf8')) | ||
} catch (e) {} | ||
if (res.status === 401 && res.headers.get('www-authenticate')) { | ||
const auth = res.headers.get('www-authenticate') | ||
.split(/,\s*/) | ||
.map(s => s.toLowerCase()) | ||
if (auth.indexOf('ipaddress') !== -1) { | ||
throw new errors.HttpErrorAuthIPAddress( | ||
method, res, body, opts.spec | ||
) | ||
} else if (auth.indexOf('otp') !== -1) { | ||
throw new errors.HttpErrorAuthOTP( | ||
method, res, body, opts.spec | ||
) | ||
} else { | ||
throw new errors.HttpErrorAuthUnknown( | ||
method, res, body, opts.spec | ||
) | ||
} | ||
} else { | ||
throw new errors.HttpErrorGeneral( | ||
method, res, body, opts.spec | ||
) | ||
} | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
'use strict' | ||
|
||
class HttpErrorBase extends Error { | ||
constructor (method, res, body, spec) { | ||
super() | ||
this.headers = res.headers.raw() | ||
this.statusCode = res.status | ||
this.code = `E${res.status}` | ||
this.method = method | ||
this.uri = res.url | ||
this.body = body | ||
} | ||
} | ||
module.exports.HttpErrorBase = HttpErrorBase | ||
|
||
class HttpErrorGeneral extends HttpErrorBase { | ||
constructor (method, res, body, spec) { | ||
super(method, res, body, spec) | ||
this.message = `${res.status} ${res.statusText} - ${ | ||
this.method.toUpperCase() | ||
} ${ | ||
this.spec || this.uri | ||
}${ | ||
(body && body.error) ? ' - ' + body.error : '' | ||
}` | ||
Error.captureStackTrace(this, HttpErrorGeneral) | ||
} | ||
} | ||
module.exports.HttpErrorGeneral = HttpErrorGeneral | ||
|
||
class HttpErrorAuthOTP extends HttpErrorBase { | ||
constructor (method, res, body, spec) { | ||
super(method, res, body, spec) | ||
this.message = 'OTP required for authentication' | ||
this.code = 'EOTP' | ||
Error.captureStackTrace(this, HttpErrorAuthOTP) | ||
} | ||
} | ||
module.exports.HttpErrorAuthOTP = HttpErrorAuthOTP | ||
|
||
class HttpErrorAuthIPAddress extends HttpErrorBase { | ||
constructor (method, res, body, spec) { | ||
super(method, res, body, spec) | ||
this.message = 'Login is not allowed from your IP address' | ||
this.code = 'EAUTHIP' | ||
Error.captureStackTrace(this, HttpErrorAuthIPAddress) | ||
} | ||
} | ||
module.exports.HttpErrorAuthIPAddress = HttpErrorAuthIPAddress | ||
|
||
class HttpErrorAuthUnknown extends HttpErrorBase { | ||
constructor (method, res, body, spec) { | ||
super(method, res, body, spec) | ||
this.message = 'Unable to authenticate, need: ' + res.headers.get('www-authenticate') | ||
this.code = 'EAUTHUNOWN' | ||
Error.captureStackTrace(this, HttpErrorAuthUnknown) | ||
} | ||
} | ||
module.exports.HttpErrorAuthUnknown = HttpErrorAuthUnknown |
Oops, something went wrong.