mirror of
https://github.com/pnpm/pnpm.git
synced 2026-01-15 02:18:31 -05:00
feat: allow loading certificates from scoped cert, ca and key (#10230)
* feat: allow loading certificates from `cert`, `ca` and `key` These properties are supported in .npmrc, but get ignored by pnpm, this will make pnpm read and use them as well. * refactor: getNetworkConfigs.ts * docs: update changesets * fix: issues * docs: update changesets --------- Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
committed by
Zoltan Kochan
parent
82e2c30484
commit
b0ec709fa5
10
.changeset/all-tables-speak.md
Normal file
10
.changeset/all-tables-speak.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
"@pnpm/config": minor
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
Allow loading certificates from `cert`, `ca`, and `key` for specific registry URLs. E.g., `//registry.example.com/:ca=-----BEGIN CERTIFICATE-----...`. Previously this was only working via `certfile`, `cafile`, and `keyfile`.
|
||||
|
||||
These properties are supported in `.npmrc`, but were ignored by pnpm, this will make pnpm read and use them as well.
|
||||
|
||||
Related PR: [#10230](https://github.com/pnpm/pnpm/pull/10230).
|
||||
@@ -17,8 +17,11 @@ const RAW_AUTH_CFG_KEYS = [
|
||||
] satisfies Array<keyof typeof types>
|
||||
|
||||
const RAW_AUTH_CFG_KEY_SUFFIXES = [
|
||||
':ca',
|
||||
':cafile',
|
||||
':cert',
|
||||
':certfile',
|
||||
':key',
|
||||
':keyfile',
|
||||
':registry',
|
||||
':tokenHelper',
|
||||
|
||||
@@ -8,29 +8,47 @@ export interface GetNetworkConfigsResult {
|
||||
}
|
||||
|
||||
export function getNetworkConfigs (rawConfig: Record<string, object>): GetNetworkConfigsResult {
|
||||
// Get all the auth options that have :certfile or :keyfile in their name
|
||||
// Get all the auth options that have SSL certificate data or file references
|
||||
const sslConfigs: Record<string, SslConfig> = {}
|
||||
const registries: Record<string, string> = {}
|
||||
for (const [configKey, value] of Object.entries(rawConfig)) {
|
||||
if (configKey[0] === '@' && configKey.endsWith(':registry')) {
|
||||
registries[configKey.slice(0, configKey.indexOf(':'))] = normalizeRegistryUrl(value as unknown as string)
|
||||
} else if (configKey.includes(':certfile') || configKey.includes(':keyfile') || configKey.includes(':cafile')) {
|
||||
// Split by '/:' because the registry may contain a port
|
||||
const registry = configKey.split('/:')[0] + '/'
|
||||
if (!sslConfigs[registry]) {
|
||||
sslConfigs[registry] = { cert: '', key: '' }
|
||||
}
|
||||
if (configKey.includes(':certfile')) {
|
||||
sslConfigs[registry].cert = fs.readFileSync(value as unknown as string, 'utf8')
|
||||
} else if (configKey.includes(':keyfile')) {
|
||||
sslConfigs[registry].key = fs.readFileSync(value as unknown as string, 'utf8')
|
||||
} else if (configKey.includes(':cafile')) {
|
||||
sslConfigs[registry].ca = fs.readFileSync(value as unknown as string, 'utf8')
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const parsed = tryParseSslSetting(configKey)
|
||||
if (!parsed) continue
|
||||
|
||||
const { registry, sslConfigKey, isFile } = parsed
|
||||
if (!sslConfigs[registry]) {
|
||||
sslConfigs[registry] = { cert: '', key: '' }
|
||||
}
|
||||
sslConfigs[registry][sslConfigKey] = isFile
|
||||
? fs.readFileSync(value as unknown as string, 'utf8')
|
||||
: (value as unknown as string).replace(/\\n/g, '\n')
|
||||
}
|
||||
return {
|
||||
registries,
|
||||
sslConfigs,
|
||||
}
|
||||
}
|
||||
|
||||
const SSL_SUFFIX_RE = /:(?<id>cert|key|ca)(?<kind>file)?$/
|
||||
|
||||
interface ParsedSslSetting {
|
||||
registry: string
|
||||
sslConfigKey: keyof SslConfig
|
||||
isFile: boolean
|
||||
}
|
||||
|
||||
function tryParseSslSetting (key: string): ParsedSslSetting | null {
|
||||
const match = key.match(SSL_SUFFIX_RE)
|
||||
if (!match?.groups) {
|
||||
return null
|
||||
}
|
||||
const registry = key.slice(0, match.index!) // already includes the trailing slash
|
||||
const sslConfigKey = match.groups.id as keyof SslConfig
|
||||
const isFile = Boolean(match.groups.kind)
|
||||
return { registry, sslConfigKey, isFile }
|
||||
}
|
||||
|
||||
@@ -842,6 +842,38 @@ test('getConfig() should read cafile', async () => {
|
||||
-----END CERTIFICATE-----`])
|
||||
})
|
||||
|
||||
test('getConfig() should read inline SSL certificates from .npmrc', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
// These are written to .npmrc with literal \n strings
|
||||
const inlineCa = '-----BEGIN CERTIFICATE-----\\nMIIFNzCCAx+gAwIBAgIQNB613yRzpKtDztlXiHmOGDANBgkqhkiG9w0BAQsFADAR\\n-----END CERTIFICATE-----'
|
||||
const inlineCert = '-----BEGIN CERTIFICATE-----\\nMIIClientCert\\n-----END CERTIFICATE-----'
|
||||
const inlineKey = '-----BEGIN PRIVATE KEY-----\\nMIIClientKey\\n-----END PRIVATE KEY-----'
|
||||
|
||||
const npmrc = [
|
||||
'//registry.example.com/:ca=' + inlineCa,
|
||||
'//registry.example.com/:cert=' + inlineCert,
|
||||
'//registry.example.com/:key=' + inlineKey,
|
||||
].join('\n')
|
||||
fs.writeFileSync('.npmrc', npmrc, 'utf8')
|
||||
|
||||
const { config } = await getConfig({
|
||||
cliOptions: {},
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '1.0.0',
|
||||
},
|
||||
})
|
||||
|
||||
// After processing, \n should be converted to actual newlines
|
||||
expect(config.sslConfigs).toBeDefined()
|
||||
expect(config.sslConfigs['//registry.example.com/']).toStrictEqual({
|
||||
ca: inlineCa.replace(/\\n/g, '\n'),
|
||||
cert: inlineCert.replace(/\\n/g, '\n'),
|
||||
key: inlineKey.replace(/\\n/g, '\n'),
|
||||
})
|
||||
})
|
||||
|
||||
test('respect mergeGitBranchLockfilesBranchPattern', async () => {
|
||||
{
|
||||
const { config } = await getConfig({
|
||||
|
||||
Reference in New Issue
Block a user