123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271 |
- 'use strict'
- const fetch = require('npm-registry-fetch')
- const { HttpErrorBase } = require('npm-registry-fetch/errors.js')
- const os = require('os')
- const { URL } = require('url')
- // try loginWeb, catch the "not supported" message and fall back to couch
- const login = (opener, prompter, opts = {}) => {
- const { creds } = opts
- return loginWeb(opener, opts).catch(er => {
- if (er instanceof WebLoginNotSupported) {
- process.emit('log', 'verbose', 'web login not supported, trying couch')
- return prompter(creds)
- .then(data => loginCouch(data.username, data.password, opts))
- } else {
- throw er
- }
- })
- }
- const adduser = (opener, prompter, opts = {}) => {
- const { creds } = opts
- return adduserWeb(opener, opts).catch(er => {
- if (er instanceof WebLoginNotSupported) {
- process.emit('log', 'verbose', 'web adduser not supported, trying couch')
- return prompter(creds)
- .then(data => adduserCouch(data.username, data.email, data.password, opts))
- } else {
- throw er
- }
- })
- }
- const adduserWeb = (opener, opts = {}) => {
- process.emit('log', 'verbose', 'web adduser', 'before first POST')
- return webAuth(opener, opts, { create: true })
- }
- const loginWeb = (opener, opts = {}) => {
- process.emit('log', 'verbose', 'web login', 'before first POST')
- return webAuth(opener, opts, {})
- }
- const isValidUrl = u => {
- try {
- return /^https?:$/.test(new URL(u).protocol)
- } catch (er) {
- return false
- }
- }
- const webAuth = (opener, opts, body) => {
- const { hostname } = opts
- body.hostname = hostname || os.hostname()
- const target = '/-/v1/login'
- return fetch(target, {
- ...opts,
- method: 'POST',
- body
- }).then(res => {
- return Promise.all([res, res.json()])
- }).then(([res, content]) => {
- const { doneUrl, loginUrl } = content
- process.emit('log', 'verbose', 'web auth', 'got response', content)
- if (!isValidUrl(doneUrl) || !isValidUrl(loginUrl)) {
- throw new WebLoginInvalidResponse('POST', res, content)
- }
- return content
- }).then(({ doneUrl, loginUrl }) => {
- process.emit('log', 'verbose', 'web auth', 'opening url pair')
- return opener(loginUrl).then(
- () => webAuthCheckLogin(doneUrl, { ...opts, cache: false })
- )
- }).catch(er => {
- if ((er.statusCode >= 400 && er.statusCode <= 499) || er.statusCode === 500) {
- throw new WebLoginNotSupported('POST', {
- status: er.statusCode,
- headers: { raw: () => er.headers }
- }, er.body)
- } else {
- throw er
- }
- })
- }
- const webAuthCheckLogin = (doneUrl, opts) => {
- return fetch(doneUrl, opts).then(res => {
- return Promise.all([res, res.json()])
- }).then(([res, content]) => {
- if (res.status === 200) {
- if (!content.token) {
- throw new WebLoginInvalidResponse('GET', res, content)
- } else {
- return content
- }
- } else if (res.status === 202) {
- const retry = +res.headers.get('retry-after') * 1000
- if (retry > 0) {
- return sleep(retry).then(() => webAuthCheckLogin(doneUrl, opts))
- } else {
- return webAuthCheckLogin(doneUrl, opts)
- }
- } else {
- throw new WebLoginInvalidResponse('GET', res, content)
- }
- })
- }
- const adduserCouch = (username, email, password, opts = {}) => {
- const body = {
- _id: 'org.couchdb.user:' + username,
- name: username,
- password: password,
- email: email,
- type: 'user',
- roles: [],
- date: new Date().toISOString()
- }
- const logObj = {
- ...body,
- password: 'XXXXX'
- }
- process.emit('log', 'verbose', 'adduser', 'before first PUT', logObj)
- const target = '/-/user/org.couchdb.user:' + encodeURIComponent(username)
- return fetch.json(target, {
- ...opts,
- method: 'PUT',
- body
- }).then(result => {
- result.username = username
- return result
- })
- }
- const loginCouch = (username, password, opts = {}) => {
- const body = {
- _id: 'org.couchdb.user:' + username,
- name: username,
- password: password,
- type: 'user',
- roles: [],
- date: new Date().toISOString()
- }
- const logObj = {
- ...body,
- password: 'XXXXX'
- }
- process.emit('log', 'verbose', 'login', 'before first PUT', logObj)
- const target = '-/user/org.couchdb.user:' + encodeURIComponent(username)
- return fetch.json(target, {
- ...opts,
- method: 'PUT',
- body
- }).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 fetch.json(target, {
- ...opts,
- query: { write: true }
- }).then(result => {
- Object.keys(result).forEach(k => {
- if (!body[k] || k === 'roles') {
- body[k] = result[k]
- }
- })
- const { otp } = opts
- return fetch.json(`${target}/-rev/${body._rev}`, {
- ...opts,
- method: 'PUT',
- body,
- forceAuth: {
- username,
- password: Buffer.from(password, 'utf8').toString('base64'),
- otp
- }
- })
- })
- }).then(result => {
- result.username = username
- return result
- })
- }
- const get = (opts = {}) => fetch.json('/-/npm/v1/user', opts)
- const set = (profile, opts = {}) => {
- Object.keys(profile).forEach(key => {
- // profile keys can't be empty strings, but they CAN be null
- if (profile[key] === '') profile[key] = null
- })
- return fetch.json('/-/npm/v1/user', {
- ...opts,
- method: 'POST',
- body: profile
- })
- }
- const listTokens = (opts = {}) => {
- const untilLastPage = (href, objects) => {
- return fetch.json(href, opts).then(result => {
- objects = objects ? objects.concat(result.objects) : result.objects
- if (result.urls.next) {
- return untilLastPage(result.urls.next, objects)
- } else {
- return objects
- }
- })
- }
- return untilLastPage('/-/npm/v1/tokens')
- }
- const removeToken = (tokenKey, opts = {}) => {
- const target = `/-/npm/v1/tokens/token/${tokenKey}`
- return fetch(target, {
- ...opts,
- method: 'DELETE',
- ignoreBody: true
- }).then(() => null)
- }
- const createToken = (password, readonly, cidrs, opts = {}) => {
- return fetch.json('/-/npm/v1/tokens', {
- ...opts,
- method: 'POST',
- body: {
- password: password,
- readonly: readonly,
- cidr_whitelist: cidrs
- }
- })
- }
- class WebLoginInvalidResponse extends HttpErrorBase {
- constructor (method, res, body) {
- super(method, res, body)
- this.message = 'Invalid response from web login endpoint'
- Error.captureStackTrace(this, WebLoginInvalidResponse)
- }
- }
- class WebLoginNotSupported extends HttpErrorBase {
- constructor (method, res, body) {
- super(method, res, body)
- this.message = 'Web login not supported'
- this.code = 'ENYI'
- Error.captureStackTrace(this, WebLoginNotSupported)
- }
- }
- const sleep = (ms) =>
- new Promise((resolve, reject) => setTimeout(resolve, ms))
- module.exports = {
- adduserCouch,
- loginCouch,
- adduserWeb,
- loginWeb,
- login,
- adduser,
- get,
- set,
- listTokens,
- removeToken,
- createToken
- }
|