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