Skip to content

Commit bf91f9f

Browse files
committed
feat(api): baseline kinda-working API impl
1 parent 9620a0a commit bf91f9f

File tree

4 files changed

+312
-114
lines changed

4 files changed

+312
-114
lines changed

auth.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
'use strict'
2+
3+
module.exports = getAuth
4+
function getAuth (conf) {
5+
const AUTH = {}
6+
const iterator = typeof conf.forEach === 'function'
7+
? conf
8+
: conf.keys
9+
iterator.forEach((k) => {
10+
const authMatchGlobal = k.match(
11+
/^(_authToken|username|_password|password|email|always-auth|_auth)$/
12+
)
13+
const authMatchScoped = k[0] === '/' && k.match(
14+
/(.*):(_authToken|username|_password|password|email|always-auth|_auth)$/
15+
)
16+
17+
// if it matches scoped it will also match global
18+
if (authMatchGlobal || authMatchScoped) {
19+
let nerfDart = null
20+
let key = null
21+
let val = null
22+
23+
if (authMatchScoped) {
24+
nerfDart = authMatchScoped[1]
25+
key = authMatchScoped[2]
26+
val = conf.get(k)
27+
if (!AUTH[nerfDart]) {
28+
AUTH[nerfDart] = {
29+
alwaysAuth: !!conf.get('always-auth')
30+
}
31+
}
32+
} else {
33+
key = authMatchGlobal[1]
34+
val = conf.get(k)
35+
AUTH.alwaysAuth = !!conf.get('always-auth')
36+
}
37+
38+
const auth = authMatchScoped ? AUTH[nerfDart] : AUTH
39+
if (key === '_authToken') {
40+
auth.token = val
41+
} else if (key.match(/password$/i)) {
42+
auth.password =
43+
// the config file stores password auth already-encoded. pacote expects
44+
// the actual username/password pair.
45+
Buffer.from(val, 'base64').toString('utf8')
46+
} else if (key === 'always-auth') {
47+
auth.alwaysAuth = val === 'false' ? false : !!val
48+
} else {
49+
auth[key] = val
50+
}
51+
}
52+
})
53+
return AUTH
54+
}

check-response.js

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

errors.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
'use strict'
2+
3+
class HttpErrorBase extends Error {
4+
constructor (method, res, body, spec) {
5+
super()
6+
this.headers = res.headers.raw()
7+
this.statusCode = res.status
8+
this.code = `E${res.status}`
9+
this.method = method
10+
this.uri = res.url
11+
this.body = body
12+
}
13+
}
14+
module.exports.HttpErrorBase = HttpErrorBase
15+
16+
class HttpErrorGeneral extends HttpErrorBase {
17+
constructor (method, res, body, spec) {
18+
super(method, res, body, spec)
19+
this.message = `${res.status} ${res.statusText} - ${
20+
this.method.toUpperCase()
21+
} ${
22+
this.spec || this.uri
23+
}${
24+
(body && body.error) ? ' - ' + body.error : ''
25+
}`
26+
Error.captureStackTrace(this, HttpErrorGeneral)
27+
}
28+
}
29+
module.exports.HttpErrorGeneral = HttpErrorGeneral
30+
31+
class HttpErrorAuthOTP extends HttpErrorBase {
32+
constructor (method, res, body, spec) {
33+
super(method, res, body, spec)
34+
this.message = 'OTP required for authentication'
35+
this.code = 'EOTP'
36+
Error.captureStackTrace(this, HttpErrorAuthOTP)
37+
}
38+
}
39+
module.exports.HttpErrorAuthOTP = HttpErrorAuthOTP
40+
41+
class HttpErrorAuthIPAddress extends HttpErrorBase {
42+
constructor (method, res, body, spec) {
43+
super(method, res, body, spec)
44+
this.message = 'Login is not allowed from your IP address'
45+
this.code = 'EAUTHIP'
46+
Error.captureStackTrace(this, HttpErrorAuthIPAddress)
47+
}
48+
}
49+
module.exports.HttpErrorAuthIPAddress = HttpErrorAuthIPAddress
50+
51+
class HttpErrorAuthUnknown extends HttpErrorBase {
52+
constructor (method, res, body, spec) {
53+
super(method, res, body, spec)
54+
this.message = 'Unable to authenticate, need: ' + res.headers.get('www-authenticate')
55+
this.code = 'EAUTHUNOWN'
56+
Error.captureStackTrace(this, HttpErrorAuthUnknown)
57+
}
58+
}
59+
module.exports.HttpErrorAuthUnknown = HttpErrorAuthUnknown

0 commit comments

Comments
 (0)