mirror of
https://github.com/titanscouting/tra-analysis.git
synced 2025-03-01 04:15:47 +00:00
400 lines
12 KiB
JavaScript
400 lines
12 KiB
JavaScript
'use strict'
|
|
const fetch = require('make-fetch-happen').defaults({retry: false})
|
|
const validate = require('aproba')
|
|
const url = require('url')
|
|
const os = require('os')
|
|
|
|
exports.adduserCouch = adduserCouch
|
|
exports.loginCouch = loginCouch
|
|
exports.adduserWeb = adduserWeb
|
|
exports.loginWeb = loginWeb
|
|
exports.login = login
|
|
exports.adduser = adduser
|
|
exports.get = get
|
|
exports.set = set
|
|
exports.listTokens = listTokens
|
|
exports.removeToken = removeToken
|
|
exports.createToken = createToken
|
|
|
|
// try loginWeb, catch the "not supported" message and fall back to couch
|
|
function login (opener, prompter, conf) {
|
|
validate('FFO', arguments)
|
|
return loginWeb(opener, conf).catch(er => {
|
|
if (er instanceof WebLoginNotSupported) {
|
|
process.emit('log', 'verbose', 'web login not supported, trying couch')
|
|
return prompter(conf.creds)
|
|
.then(data => loginCouch(data.username, data.password, conf))
|
|
} else {
|
|
throw er
|
|
}
|
|
})
|
|
}
|
|
|
|
function adduser (opener, prompter, conf) {
|
|
validate('FFO', arguments)
|
|
return adduserWeb(opener, conf).catch(er => {
|
|
if (er instanceof WebLoginNotSupported) {
|
|
process.emit('log', 'verbose', 'web adduser not supported, trying couch')
|
|
return prompter(conf.creds)
|
|
.then(data => adduserCouch(data.username, data.email, data.password, conf))
|
|
} else {
|
|
throw er
|
|
}
|
|
})
|
|
}
|
|
|
|
function adduserWeb (opener, conf) {
|
|
validate('FO', arguments)
|
|
const body = { create: true }
|
|
process.emit('log', 'verbose', 'web adduser', 'before first POST')
|
|
return webAuth(opener, conf, body)
|
|
}
|
|
|
|
function loginWeb (opener, conf) {
|
|
validate('FO', arguments)
|
|
process.emit('log', 'verbose', 'web login', 'before first POST')
|
|
return webAuth(opener, conf, {})
|
|
}
|
|
|
|
function webAuth (opener, conf, body) {
|
|
if (!conf.opts) conf.opts = {}
|
|
const target = url.resolve(conf.registry, '-/v1/login')
|
|
body.hostname = conf.hostname || os.hostname()
|
|
return fetchJSON({
|
|
target: target,
|
|
method: 'POST',
|
|
body: body,
|
|
opts: conf.opts,
|
|
saveResponse: true
|
|
}).then(result => {
|
|
const res = result[0]
|
|
const content = result[1]
|
|
process.emit('log', 'verbose', 'web auth', 'got response', content)
|
|
const doneUrl = content.doneUrl
|
|
const loginUrl = content.loginUrl
|
|
if (typeof doneUrl !== 'string' ||
|
|
typeof loginUrl !== 'string' ||
|
|
!doneUrl || !loginUrl) {
|
|
throw new WebLoginInvalidResponse('POST', target, res, content)
|
|
}
|
|
process.emit('log', 'verbose', 'web auth', 'opening url pair')
|
|
const doneConf = {
|
|
target: doneUrl,
|
|
method: 'GET',
|
|
opts: conf.opts,
|
|
saveResponse: true
|
|
}
|
|
return opener(loginUrl).then(() => fetchJSON(doneConf)).then(onDone)
|
|
function onDone (result) {
|
|
const res = result[0]
|
|
const content = result[1]
|
|
if (res.status === 200) {
|
|
if (!content.token) {
|
|
throw new WebLoginInvalidResponse('GET', doneUrl, res, content)
|
|
} else {
|
|
return content
|
|
}
|
|
} else if (res.status === 202) {
|
|
const retry = +res.headers.get('retry-after')
|
|
if (retry > 0) {
|
|
return new Promise(resolve => setTimeout(resolve, 1000 * retry))
|
|
.then(() => fetchJSON(doneConf)).then(onDone)
|
|
} else {
|
|
return fetchJSON(doneConf).then(onDone)
|
|
}
|
|
} else {
|
|
throw new WebLoginInvalidResponse('GET', doneUrl, res, content)
|
|
}
|
|
}
|
|
}).catch(er => {
|
|
if ((er.statusCode >= 400 && er.statusCode <= 499) || er.statusCode === 500) {
|
|
throw new WebLoginNotSupported('POST', target, {
|
|
status: er.statusCode,
|
|
headers: { raw: () => er.headers }
|
|
}, er.body)
|
|
} else {
|
|
throw er
|
|
}
|
|
})
|
|
}
|
|
|
|
function adduserCouch (username, email, password, conf) {
|
|
validate('SSSO', arguments)
|
|
if (!conf.opts) conf.opts = {}
|
|
const userobj = {
|
|
_id: 'org.couchdb.user:' + username,
|
|
name: username,
|
|
password: password,
|
|
email: email,
|
|
type: 'user',
|
|
roles: [],
|
|
date: new Date().toISOString()
|
|
}
|
|
const logObj = {}
|
|
Object.keys(userobj).forEach(k => {
|
|
logObj[k] = k === 'password' ? 'XXXXX' : userobj[k]
|
|
})
|
|
process.emit('log', 'verbose', 'adduser', 'before first PUT', logObj)
|
|
|
|
const target = url.resolve(conf.registry, '-/user/org.couchdb.user:' + encodeURIComponent(username))
|
|
|
|
return fetchJSON({target: target, method: 'PUT', body: userobj, opts: conf.opts})
|
|
.then(result => {
|
|
result.username = username
|
|
return result
|
|
})
|
|
}
|
|
|
|
function loginCouch (username, password, conf) {
|
|
validate('SSO', arguments)
|
|
const userobj = {
|
|
_id: 'org.couchdb.user:' + username,
|
|
name: username,
|
|
password: password,
|
|
type: 'user',
|
|
roles: [],
|
|
date: new Date().toISOString()
|
|
}
|
|
const logObj = {}
|
|
Object.keys(userobj).forEach(k => {
|
|
logObj[k] = k === 'password' ? 'XXXXX' : userobj[k]
|
|
})
|
|
process.emit('log', 'verbose', 'login', 'before first PUT', logObj)
|
|
|
|
const target = url.resolve(conf.registry, '-/user/org.couchdb.user:' + encodeURIComponent(username))
|
|
return fetchJSON(Object.assign({method: 'PUT', target: target, body: userobj}, conf)).catch(err => {
|
|
if (err.code === 'E400') {
|
|
err.message = `There is no user with the username "${username}".`
|
|
throw err
|
|
}
|
|
if (err.code !== 'E409') throw err
|
|
return fetchJSON(Object.assign({method: 'GET', target: target + '?write=true'}, conf)).then(result => {
|
|
Object.keys(result).forEach(function (k) {
|
|
if (!userobj[k] || k === 'roles') {
|
|
userobj[k] = result[k]
|
|
}
|
|
})
|
|
const req = {
|
|
method: 'PUT',
|
|
target: target + '/-rev/' + userobj._rev,
|
|
body: userobj,
|
|
auth: {
|
|
basic: {
|
|
username: username,
|
|
password: password
|
|
}
|
|
}
|
|
}
|
|
return fetchJSON(Object.assign({}, conf, req))
|
|
})
|
|
}).then(result => {
|
|
result.username = username
|
|
return result
|
|
})
|
|
}
|
|
|
|
function get (conf) {
|
|
validate('O', arguments)
|
|
const target = url.resolve(conf.registry, '-/npm/v1/user')
|
|
return fetchJSON(Object.assign({target: target}, conf))
|
|
}
|
|
|
|
function set (profile, conf) {
|
|
validate('OO', arguments)
|
|
const target = url.resolve(conf.registry, '-/npm/v1/user')
|
|
Object.keys(profile).forEach(key => {
|
|
// profile keys can't be empty strings, but they CAN be null
|
|
if (profile[key] === '') profile[key] = null
|
|
})
|
|
return fetchJSON(Object.assign({target: target, method: 'POST', body: profile}, conf))
|
|
}
|
|
|
|
function listTokens (conf) {
|
|
validate('O', arguments)
|
|
|
|
return untilLastPage(`-/npm/v1/tokens`)
|
|
|
|
function untilLastPage (href, objects) {
|
|
return fetchJSON(Object.assign({target: url.resolve(conf.registry, href)}, conf)).then(result => {
|
|
objects = objects ? objects.concat(result.objects) : result.objects
|
|
if (result.urls.next) {
|
|
return untilLastPage(result.urls.next, objects)
|
|
} else {
|
|
return objects
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
function removeToken (tokenKey, conf) {
|
|
validate('SO', arguments)
|
|
const target = url.resolve(conf.registry, `-/npm/v1/tokens/token/${tokenKey}`)
|
|
return fetchJSON(Object.assign({target: target, method: 'DELETE'}, conf))
|
|
}
|
|
|
|
function createToken (password, readonly, cidrs, conf) {
|
|
validate('SBAO', arguments)
|
|
const target = url.resolve(conf.registry, '-/npm/v1/tokens')
|
|
const props = {
|
|
password: password,
|
|
readonly: readonly,
|
|
cidr_whitelist: cidrs
|
|
}
|
|
return fetchJSON(Object.assign({target: target, method: 'POST', body: props}, conf))
|
|
}
|
|
|
|
function FetchError (err, method, target) {
|
|
err.method = method
|
|
err.href = target
|
|
return err
|
|
}
|
|
|
|
class HttpErrorBase extends Error {
|
|
constructor (method, target, res, body) {
|
|
super()
|
|
this.headers = res.headers.raw()
|
|
this.statusCode = res.status
|
|
this.code = 'E' + res.status
|
|
this.method = method
|
|
this.target = target
|
|
this.body = body
|
|
this.pkgid = packageName(target)
|
|
}
|
|
}
|
|
|
|
class HttpErrorGeneral extends HttpErrorBase {
|
|
constructor (method, target, res, body) {
|
|
super(method, target, res, body)
|
|
if (body && body.error) {
|
|
this.message = `Registry returned ${this.statusCode} for ${this.method} on ${this.target}: ${body.error}`
|
|
} else {
|
|
this.message = `Registry returned ${this.statusCode} for ${this.method} on ${this.target}`
|
|
}
|
|
Error.captureStackTrace(this, HttpErrorGeneral)
|
|
}
|
|
}
|
|
|
|
class WebLoginInvalidResponse extends HttpErrorBase {
|
|
constructor (method, target, res, body) {
|
|
super(method, target, res, body)
|
|
this.message = 'Invalid response from web login endpoint'
|
|
Error.captureStackTrace(this, WebLoginInvalidResponse)
|
|
}
|
|
}
|
|
|
|
class WebLoginNotSupported extends HttpErrorBase {
|
|
constructor (method, target, res, body) {
|
|
super(method, target, res, body)
|
|
this.message = 'Web login not supported'
|
|
this.code = 'ENYI'
|
|
Error.captureStackTrace(this, WebLoginNotSupported)
|
|
}
|
|
}
|
|
|
|
class HttpErrorAuthOTP extends HttpErrorBase {
|
|
constructor (method, target, res, body) {
|
|
super(method, target, res, body)
|
|
this.message = 'OTP required for authentication'
|
|
this.code = 'EOTP'
|
|
Error.captureStackTrace(this, HttpErrorAuthOTP)
|
|
}
|
|
}
|
|
|
|
class HttpErrorAuthIPAddress extends HttpErrorBase {
|
|
constructor (method, target, res, body) {
|
|
super(method, target, res, body)
|
|
this.message = 'Login is not allowed from your IP address'
|
|
this.code = 'EAUTHIP'
|
|
Error.captureStackTrace(this, HttpErrorAuthIPAddress)
|
|
}
|
|
}
|
|
|
|
class HttpErrorAuthUnknown extends HttpErrorBase {
|
|
constructor (method, target, res, body) {
|
|
super(method, target, res, body)
|
|
this.message = 'Unable to authenticate, need: ' + res.headers.get('www-authenticate')
|
|
this.code = 'EAUTHUNKNOWN'
|
|
Error.captureStackTrace(this, HttpErrorAuthUnknown)
|
|
}
|
|
}
|
|
|
|
function authHeaders (auth) {
|
|
const headers = {}
|
|
if (!auth) return headers
|
|
if (auth.otp) headers['npm-otp'] = auth.otp
|
|
if (auth.token) {
|
|
headers['Authorization'] = 'Bearer ' + auth.token
|
|
} else if (auth.basic) {
|
|
const basic = auth.basic.username + ':' + auth.basic.password
|
|
headers['Authorization'] = 'Basic ' + Buffer.from(basic).toString('base64')
|
|
}
|
|
return headers
|
|
}
|
|
|
|
function fetchJSON (conf) {
|
|
const fetchOpts = {
|
|
method: conf.method,
|
|
headers: Object.assign({}, conf.headers || (conf.auth && authHeaders(conf.auth)) || {})
|
|
}
|
|
if (conf.body != null) {
|
|
fetchOpts.headers['Content-Type'] = 'application/json'
|
|
fetchOpts.body = JSON.stringify(conf.body)
|
|
}
|
|
process.emit('log', 'http', 'request', '→', conf.method || 'GET', conf.target)
|
|
return fetch.defaults(conf.opts || {})(conf.target, fetchOpts).catch(err => {
|
|
throw new FetchError(err, conf.method, conf.target)
|
|
}).then(res => {
|
|
if (res.headers.has('npm-notice')) {
|
|
process.emit('warn', 'notice', res.headers.get('npm-notice'))
|
|
}
|
|
if (res.headers.get('content-type') === 'application/json') {
|
|
return res.json().then(content => [res, content])
|
|
} else {
|
|
return res.buffer().then(content => {
|
|
try {
|
|
return [res, JSON.parse(content)]
|
|
} catch (_) {
|
|
return [res, content]
|
|
}
|
|
})
|
|
}
|
|
}).then(result => {
|
|
const res = result[0]
|
|
const content = result[1]
|
|
const retVal = conf.saveResponse ? result : content
|
|
process.emit('log', 'http', res.status, `← ${res.statusText} (${conf.target})`)
|
|
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 HttpErrorAuthIPAddress(conf.method, conf.target, res, content)
|
|
} else if (auth.indexOf('otp') !== -1) {
|
|
throw new HttpErrorAuthOTP(conf.method, conf.target, res, content)
|
|
} else {
|
|
throw new HttpErrorAuthUnknown(conf.method, conf.target, res, content)
|
|
}
|
|
} else if (res.status < 200 || res.status >= 300) {
|
|
throw new HttpErrorGeneral(conf.method, conf.target, res, content)
|
|
} else {
|
|
return retVal
|
|
}
|
|
})
|
|
}
|
|
|
|
function packageName (href) {
|
|
try {
|
|
let basePath = url.parse(href).pathname.substr(1)
|
|
if (!basePath.match(/^-/)) {
|
|
basePath = basePath.split('/')
|
|
var index = basePath.indexOf('_rewrite')
|
|
if (index === -1) {
|
|
index = basePath.length - 1
|
|
} else {
|
|
index++
|
|
}
|
|
return decodeURIComponent(basePath[index])
|
|
}
|
|
} catch (_) {
|
|
// this is ok
|
|
}
|
|
}
|