codekingpro/portable-devtools
114k
1'use strict'
2const fs = require('fs')
3const npa = require('npm-package-arg')
4const { URL } = require('url')
5
6// Find the longest registry key that is used for some kind of auth
7// in the options. Returns the registry key and the auth config.
8const regFromURI = (uri, opts) => {
9 const parsed = new URL(uri)
10 // try to find a config key indicating we have auth for this registry
11 // can be one of :_authToken, :_auth, :_password and :username, or
12 // :certfile and :keyfile
13 // We walk up the "path" until we're left with just //<host>[:<port>],
14 // stopping when we reach '//'.
15 let regKey = `//${parsed.host}${parsed.pathname}`
16 while (regKey.length > '//'.length) {
17 const authKey = hasAuth(regKey, opts)
18 // got some auth for this URI
19 if (authKey) {
20 return { regKey, authKey }
21 }
22
23 // can be either //host/some/path/:_auth or //host/some/path:_auth
24 // walk up by removing EITHER what's after the slash OR the slash itself
25 regKey = regKey.replace(/([^/]+|\/)$/, '')
26 }
27 return { regKey: false, authKey: null }
28}
29
30// Not only do we want to know if there is auth, but if we are calling `npm
31// logout` we want to know what config value specifically provided it. This is
32// so we can look up where the config came from to delete it (i.e. user vs
33// project)
34const hasAuth = (regKey, opts) => {
35 if (opts[`${regKey}:_authToken`]) {
36 return '_authToken'
37 }
38 if (opts[`${regKey}:_auth`]) {
39 return '_auth'
40 }
41 if (opts[`${regKey}:username`] && opts[`${regKey}:_password`]) {
42 // 'password' can be inferred to also be present
43 return 'username'
44 }
45 if (opts[`${regKey}:certfile`] && opts[`${regKey}:keyfile`]) {
46 // 'keyfile' can be inferred to also be present
47 return 'certfile'
48 }
49 return false
50}
51
52const sameHost = (a, b) => {
53 const parsedA = new URL(a)
54 const parsedB = new URL(b)
55 return parsedA.host === parsedB.host
56}
57
58const getRegistry = opts => {
59 const { spec } = opts
60 const { scope: specScope, subSpec } = spec ? npa(spec) : {}
61 const subSpecScope = subSpec && subSpec.scope
62 const scope = subSpec ? subSpecScope : specScope
63 const scopeReg = scope && opts[`${scope}:registry`]
64 return scopeReg || opts.registry
65}
66
67const maybeReadFile = file => {
68 try {
69 return fs.readFileSync(file, 'utf8')
70 } catch (er) {
71 if (er.code !== 'ENOENT') {
72 throw er
73 }
74 return null
75 }
76}
77
78const getAuth = (uri, opts = {}) => {
79 const { forceAuth } = opts
80 if (!uri) {
81 throw new Error('URI is required')
82 }
83 const { regKey, authKey } = regFromURI(uri, forceAuth || opts)
84
85 // we are only allowed to use what's in forceAuth if specified
86 if (forceAuth && !regKey) {
87 return new Auth({
88 // if we force auth we don't want to refer back to anything in config
89 regKey: false,
90 authKey: null,
91 scopeAuthKey: null,
92 token: forceAuth._authToken || forceAuth.token,
93 username: forceAuth.username,
94 password: forceAuth._password || forceAuth.password,
95 auth: forceAuth._auth || forceAuth.auth,
96 certfile: forceAuth.certfile,
97 keyfile: forceAuth.keyfile,
98 })
99 }
100
101 // no auth for this URI, but might have it for the registry
102 if (!regKey) {
103 const registry = getRegistry(opts)
104 if (registry && uri !== registry && sameHost(uri, registry)) {
105 return getAuth(registry, opts)
106 } else if (registry !== opts.registry) {
107 // If making a tarball request to a different base URI than the
108 // registry where we logged in, but the same auth SHOULD be sent
109 // to that artifact host, then we track where it was coming in from,
110 // and warn the user if we get a 4xx error on it.
111 const { regKey: scopeAuthKey, authKey: _authKey } = regFromURI(registry, opts)
112 return new Auth({ scopeAuthKey, regKey: scopeAuthKey, authKey: _authKey })
113 }
114 }
115
116 const {
117 [`${regKey}:_authToken`]: token,
118 [`${regKey}:username`]: username,
119 [`${regKey}:_password`]: password,
120 [`${regKey}:_auth`]: auth,
121 [`${regKey}:certfile`]: certfile,
122 [`${regKey}:keyfile`]: keyfile,
123 } = opts
124
125 return new Auth({
126 scopeAuthKey: null,
127 regKey,
128 authKey,
129 token,
130 auth,
131 username,
132 password,
133 certfile,
134 keyfile,
135 })
136}
137
138class Auth {
139 constructor ({
140 token,
141 auth,
142 username,
143 password,
144 scopeAuthKey,
145 certfile,
146 keyfile,
147 regKey,
148 authKey,
149 }) {
150 // same as regKey but only present for scoped auth. Should have been named scopeRegKey
151 this.scopeAuthKey = scopeAuthKey
152 // `${regKey}:${authKey}` will get you back to the auth config that gave us auth
153 this.regKey = regKey
154 this.authKey = authKey
155 this.token = null
156 this.auth = null
157 this.isBasicAuth = false
158 this.cert = null
159 this.key = null
160 if (token) {
161 this.token = token
162 } else if (auth) {
163 this.auth = auth
164 } else if (username && password) {
165 const p = Buffer.from(password, 'base64').toString('utf8')
166 this.auth = Buffer.from(`${username}:${p}`, 'utf8').toString('base64')
167 this.isBasicAuth = true
168 }
169 // mTLS may be used in conjunction with another auth method above
170 if (certfile && keyfile) {
171 const cert = maybeReadFile(certfile, 'utf-8')
172 const key = maybeReadFile(keyfile, 'utf-8')
173 if (cert && key) {
174 this.cert = cert
175 this.key = key
176 }
177 }
178 }
179}
180
181module.exports = getAuth
182 