Skip to content

Commit

Permalink
feat(api): baseline kinda-working API impl
Browse files Browse the repository at this point in the history
  • Loading branch information
zkat committed Mar 14, 2018
1 parent 9620a0a commit bf91f9f
Show file tree
Hide file tree
Showing 4 changed files with 312 additions and 114 deletions.
54 changes: 54 additions & 0 deletions auth.js
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
}
97 changes: 97 additions & 0 deletions check-response.js
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
)
}
})
}
59 changes: 59 additions & 0 deletions errors.js
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
Loading

0 comments on commit bf91f9f

Please sign in to comment.