index.js 6.9 KB

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