UNPKG

7.93 kBJavaScriptView Raw
1'use strict'
2
3const fetch = require('npm-registry-fetch')
4const { HttpErrorBase } = require('npm-registry-fetch/errors.js')
5const os = require('os')
6const pudding = require('figgy-pudding')
7const validate = require('aproba')
8
9exports.adduserCouch = adduserCouch
10exports.loginCouch = loginCouch
11exports.adduserWeb = adduserWeb
12exports.loginWeb = loginWeb
13exports.login = login
14exports.adduser = adduser
15exports.get = get
16exports.set = set
17exports.listTokens = listTokens
18exports.removeToken = removeToken
19exports.createToken = createToken
20
21const url = require('url')
22
23const isValidUrl = u => {
24 if (u && typeof u === 'string') {
25 const p = url.parse(u)
26 return p.slashes && p.host && p.path && /^https?:$/.test(p.protocol)
27 }
28 return false
29}
30
31const ProfileConfig = pudding({
32 creds: {},
33 hostname: {},
34 otp: {}
35})
36
37// try loginWeb, catch the "not supported" message and fall back to couch
38function login (opener, prompter, opts) {
39 validate('FFO', arguments)
40 opts = ProfileConfig(opts)
41 return loginWeb(opener, opts).catch(er => {
42 if (er instanceof WebLoginNotSupported) {
43 process.emit('log', 'verbose', 'web login not supported, trying couch')
44 return prompter(opts.creds)
45 .then(data => loginCouch(data.username, data.password, opts))
46 } else {
47 throw er
48 }
49 })
50}
51
52function adduser (opener, prompter, opts) {
53 validate('FFO', arguments)
54 opts = ProfileConfig(opts)
55 return adduserWeb(opener, opts).catch(er => {
56 if (er instanceof WebLoginNotSupported) {
57 process.emit('log', 'verbose', 'web adduser not supported, trying couch')
58 return prompter(opts.creds)
59 .then(data => adduserCouch(data.username, data.email, data.password, opts))
60 } else {
61 throw er
62 }
63 })
64}
65
66function adduserWeb (opener, opts) {
67 validate('FO', arguments)
68 const body = { create: true }
69 process.emit('log', 'verbose', 'web adduser', 'before first POST')
70 return webAuth(opener, opts, body)
71}
72
73function loginWeb (opener, opts) {
74 validate('FO', arguments)
75 process.emit('log', 'verbose', 'web login', 'before first POST')
76 return webAuth(opener, opts, {})
77}
78
79function webAuth (opener, opts, body) {
80 opts = ProfileConfig(opts)
81 body.hostname = opts.hostname || os.hostname()
82 const target = '/-/v1/login'
83 return fetch(target, opts.concat({
84 method: 'POST',
85 body
86 })).then(res => {
87 return Promise.all([res, res.json()])
88 }).then(([res, content]) => {
89 const { doneUrl, loginUrl } = content
90 process.emit('log', 'verbose', 'web auth', 'got response', content)
91 if (!isValidUrl(doneUrl) || !isValidUrl(loginUrl)) {
92 throw new WebLoginInvalidResponse('POST', res, content)
93 }
94 return content
95 }).then(({ doneUrl, loginUrl }) => {
96 process.emit('log', 'verbose', 'web auth', 'opening url pair')
97 return opener(loginUrl).then(
98 () => webAuthCheckLogin(doneUrl, opts.concat({ cache: false }))
99 )
100 }).catch(er => {
101 if ((er.statusCode >= 400 && er.statusCode <= 499) || er.statusCode === 500) {
102 throw new WebLoginNotSupported('POST', {
103 status: er.statusCode,
104 headers: { raw: () => er.headers }
105 }, er.body)
106 } else {
107 throw er
108 }
109 })
110}
111
112function webAuthCheckLogin (doneUrl, opts) {
113 return fetch(doneUrl, opts).then(res => {
114 return Promise.all([res, res.json()])
115 }).then(([res, content]) => {
116 if (res.status === 200) {
117 if (!content.token) {
118 throw new WebLoginInvalidResponse('GET', res, content)
119 } else {
120 return content
121 }
122 } else if (res.status === 202) {
123 const retry = +res.headers.get('retry-after') * 1000
124 if (retry > 0) {
125 return sleep(retry).then(() => webAuthCheckLogin(doneUrl, opts))
126 } else {
127 return webAuthCheckLogin(doneUrl, opts)
128 }
129 } else {
130 throw new WebLoginInvalidResponse('GET', res, content)
131 }
132 })
133}
134
135function adduserCouch (username, email, password, opts) {
136 validate('SSSO', arguments)
137 opts = ProfileConfig(opts)
138 const body = {
139 _id: 'org.couchdb.user:' + username,
140 name: username,
141 password: password,
142 email: email,
143 type: 'user',
144 roles: [],
145 date: new Date().toISOString()
146 }
147 const logObj = {}
148 Object.keys(body).forEach(k => {
149 logObj[k] = k === 'password' ? 'XXXXX' : body[k]
150 })
151 process.emit('log', 'verbose', 'adduser', 'before first PUT', logObj)
152
153 const target = '/-/user/org.couchdb.user:' + encodeURIComponent(username)
154 return fetch.json(target, opts.concat({
155 method: 'PUT',
156 body
157 })).then(result => {
158 result.username = username
159 return result
160 })
161}
162
163function loginCouch (username, password, opts) {
164 validate('SSO', arguments)
165 opts = ProfileConfig(opts)
166 const body = {
167 _id: 'org.couchdb.user:' + username,
168 name: username,
169 password: password,
170 type: 'user',
171 roles: [],
172 date: new Date().toISOString()
173 }
174 const logObj = {}
175 Object.keys(body).forEach(k => {
176 logObj[k] = k === 'password' ? 'XXXXX' : body[k]
177 })
178 process.emit('log', 'verbose', 'login', 'before first PUT', logObj)
179
180 const target = '-/user/org.couchdb.user:' + encodeURIComponent(username)
181 return fetch.json(target, opts.concat({
182 method: 'PUT',
183 body
184 })).catch(err => {
185 if (err.code === 'E400') {
186 err.message = `There is no user with the username "${username}".`
187 throw err
188 }
189 if (err.code !== 'E409') throw err
190 return fetch.json(target, opts.concat({
191 query: { write: true }
192 })).then(result => {
193 Object.keys(result).forEach(function (k) {
194 if (!body[k] || k === 'roles') {
195 body[k] = result[k]
196 }
197 })
198 return fetch.json(`${target}/-rev/${body._rev}`, opts.concat({
199 method: 'PUT',
200 body,
201 forceAuth: {
202 username,
203 password: Buffer.from(password, 'utf8').toString('base64'),
204 otp: opts.otp
205 }
206 }))
207 })
208 }).then(result => {
209 result.username = username
210 return result
211 })
212}
213
214function get (opts) {
215 validate('O', arguments)
216 return fetch.json('/-/npm/v1/user', opts)
217}
218
219function set (profile, opts) {
220 validate('OO', arguments)
221 Object.keys(profile).forEach(key => {
222 // profile keys can't be empty strings, but they CAN be null
223 if (profile[key] === '') profile[key] = null
224 })
225 return fetch.json('/-/npm/v1/user', ProfileConfig(opts, {
226 method: 'POST',
227 body: profile
228 }))
229}
230
231function listTokens (opts) {
232 validate('O', arguments)
233 opts = ProfileConfig(opts)
234
235 return untilLastPage('/-/npm/v1/tokens')
236
237 function untilLastPage (href, objects) {
238 return fetch.json(href, opts).then(result => {
239 objects = objects ? objects.concat(result.objects) : result.objects
240 if (result.urls.next) {
241 return untilLastPage(result.urls.next, objects)
242 } else {
243 return objects
244 }
245 })
246 }
247}
248
249function removeToken (tokenKey, opts) {
250 validate('SO', arguments)
251 const target = `/-/npm/v1/tokens/token/${tokenKey}`
252 return fetch(target, ProfileConfig(opts, {
253 method: 'DELETE',
254 ignoreBody: true
255 })).then(() => null)
256}
257
258function createToken (password, readonly, cidrs, opts) {
259 validate('SBAO', arguments)
260 return fetch.json('/-/npm/v1/tokens', ProfileConfig(opts, {
261 method: 'POST',
262 body: {
263 password: password,
264 readonly: readonly,
265 cidr_whitelist: cidrs
266 }
267 }))
268}
269
270class WebLoginInvalidResponse extends HttpErrorBase {
271 constructor (method, res, body) {
272 super(method, res, body)
273 this.message = 'Invalid response from web login endpoint'
274 Error.captureStackTrace(this, WebLoginInvalidResponse)
275 }
276}
277
278class WebLoginNotSupported extends HttpErrorBase {
279 constructor (method, res, body) {
280 super(method, res, body)
281 this.message = 'Web login not supported'
282 this.code = 'ENYI'
283 Error.captureStackTrace(this, WebLoginNotSupported)
284 }
285}
286
287function sleep (ms) {
288 return new Promise((resolve, reject) => setTimeout(resolve, ms))
289}