Skip to content

Commit a5e306f

Browse files
authored
feat: show hosts in cert in CLI (#19317)
1 parent 4d88f6c commit a5e306f

File tree

4 files changed

+112
-5
lines changed

4 files changed

+112
-5
lines changed

packages/vite/src/node/__tests__/utils.spec.ts

+55
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import fs from 'node:fs'
22
import path from 'node:path'
3+
import crypto from 'node:crypto'
34
import { describe, expect, test } from 'vitest'
45
import {
56
asyncFlatten,
67
bareImportRE,
8+
extractHostnamesFromSubjectAltName,
79
flattenId,
810
generateCodeFrame,
911
getHash,
@@ -165,6 +167,59 @@ describe('resolveHostname', () => {
165167
})
166168
})
167169

170+
describe('extractHostnamesFromSubjectAltName', () => {
171+
const testCases = [
172+
['DNS:localhost', ['localhost']],
173+
['DNS:localhost, DNS:foo.localhost', ['localhost', 'foo.localhost']],
174+
['DNS:*.localhost', ['vite.localhost']],
175+
['DNS:[::1]', []], // [::1] is skipped
176+
['othername:"foo,bar", DNS:localhost', ['localhost']], // handle quoted correctly
177+
] as const
178+
179+
for (const [input, expected] of testCases) {
180+
test(`should extract names from subjectAltName: ${input}`, () => {
181+
expect(extractHostnamesFromSubjectAltName(input)).toStrictEqual(expected)
182+
})
183+
}
184+
185+
test('should extract names from actual certificate', () => {
186+
const certText = `
187+
-----BEGIN CERTIFICATE-----
188+
MIID7zCCAtegAwIBAgIJS9D2rIN7tA8mMA0GCSqGSIb3DQEBCwUAMGkxFDASBgNV
189+
BAMTC2V4YW1wbGUub3JnMQswCQYDVQQGEwJVUzERMA8GA1UECBMIVmlyZ2luaWEx
190+
EzARBgNVBAcTCkJsYWNrc2J1cmcxDTALBgNVBAoTBFRlc3QxDTALBgNVBAsTBFRl
191+
c3QwHhcNMjUwMTMwMDQxNTI1WhcNMjUwMzAxMDQxNTI1WjBpMRQwEgYDVQQDEwtl
192+
eGFtcGxlLm9yZzELMAkGA1UEBhMCVVMxETAPBgNVBAgTCFZpcmdpbmlhMRMwEQYD
193+
VQQHEwpCbGFja3NidXJnMQ0wCwYDVQQKEwRUZXN0MQ0wCwYDVQQLEwRUZXN0MIIB
194+
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxNPlCqTmUZ7/F7GyFWDopqZ6
195+
w19Y7/98B10JEeFGTAQIj/RP2UgZNcTABQDUvtkF7y+bOeoVJW7Zz8ozQYhRaDp8
196+
CN2gXMcYeTUku/pKLXyCzHHVrOPAXTeU7sMRgLvPCrrJtx5OjvndW+O/PhohPRi3
197+
iEpPvpM8gi7MVRGhnWVSx0/Ynx5c0+/vqyBTzrM2OX7Ufg8Nv7LaTXpCAnmIQp+f
198+
Sqq7HZ7t6Y7laS4RApityvlnFHZ4f2cEibAKv/vXLED7bgAlGb8R1viPRdMtAPuI
199+
MYvHBgGFjyX1fmq6Mz3aqlAscJILtbQlwty1oYyaENE0lq8+nZXQ+t6I+CIVLQID
200+
AQABo4GZMIGWMAsGA1UdDwQEAwIC9DAxBgNVHSUEKjAoBggrBgEFBQcDAQYIKwYB
201+
BQUHAwIGCCsGAQUFBwMDBggrBgEFBQcDCDBUBgNVHREETTBLgglsb2NhbGhvc3SC
202+
DWZvby5sb2NhbGhvc3SCECoudml0ZS5sb2NhbGhvc3SCBVs6OjFdhwR/AAABhxD+
203+
gAAAAAAAAAAAAAAAAAABMA0GCSqGSIb3DQEBCwUAA4IBAQBi302qLCgxWsUalgc2
204+
olFxVKob1xCciS8yUVX6HX0vza0WJ7oGW6qZsBbQtfgDwB/dHv7rwsfpjRWvFhmq
205+
gEUrewa1h0TIC+PPTYYz4M0LOwcLIWZLZr4am1eI7YP9NDgRdhfAfM4hw20vjf2a
206+
kYLKyRTC5+3/ly5opMq+CGLQ8/gnFxhP3ho8JYrRnqLeh3KCTGen3kmbAhD4IOJ9
207+
lxMwFPTTWLFFjxbXjXmt5cEiL2mpcq13VCF2HmheCen37CyYIkrwK9IfLhBd5QQh
208+
WEIBLwjKCAscrtyayXWp6zUTmgvb8PQf//3Mh2DiEngAi3WI/nL+8Y0RkqbvxBar
209+
X2JN
210+
-----END CERTIFICATE-----
211+
`.trim()
212+
const cert = new crypto.X509Certificate(certText)
213+
expect(
214+
extractHostnamesFromSubjectAltName(cert.subjectAltName ?? ''),
215+
).toStrictEqual([
216+
'localhost',
217+
'foo.localhost',
218+
'vite.vite.localhost', // *.vite.localhost
219+
])
220+
})
221+
})
222+
168223
describe('posToNumber', () => {
169224
test('simple', () => {
170225
const actual = posToNumber('a\nb', { line: 2, column: 0 })

packages/vite/src/node/preview.ts

+3-5
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,9 @@ export async function preview(
137137
)
138138
}
139139

140+
const httpsOptions = await resolveHttpsConfig(config.server.https)
140141
const app = connect() as Connect.Server
141-
const httpServer = await resolveHttpServer(
142-
config.preview,
143-
app,
144-
await resolveHttpsConfig(config.preview.https),
145-
)
142+
const httpServer = await resolveHttpServer(config.preview, app, httpsOptions)
146143
setClientErrorHandler(httpServer, config.logger)
147144

148145
const options = config.preview
@@ -274,6 +271,7 @@ export async function preview(
274271
server.resolvedUrls = await resolveServerUrls(
275272
httpServer,
276273
config.preview,
274+
httpsOptions,
277275
config,
278276
)
279277

packages/vite/src/node/server/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,7 @@ export async function _createServer(
649649
server.resolvedUrls = await resolveServerUrls(
650650
httpServer,
651651
config.server,
652+
httpsOptions,
652653
config,
653654
)
654655
if (!isRestart && config.server.open) server.openBrowser()

packages/vite/src/node/utils.ts

+53
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import path from 'node:path'
44
import { exec } from 'node:child_process'
55
import crypto from 'node:crypto'
66
import { URL, fileURLToPath } from 'node:url'
7+
import type { ServerOptions as HttpsServerOptions } from 'node:https'
78
import { builtinModules, createRequire } from 'node:module'
89
import { promises as dns } from 'node:dns'
910
import { performance } from 'node:perf_hooks'
@@ -982,6 +983,7 @@ export async function resolveHostname(
982983
export async function resolveServerUrls(
983984
server: Server,
984985
options: CommonServerOptions,
986+
httpsOptions: HttpsServerOptions | undefined,
985987
config: ResolvedConfig,
986988
): Promise<ResolvedServerUrls> {
987989
const address = server.address()
@@ -1035,9 +1037,60 @@ export async function resolveServerUrls(
10351037
}
10361038
})
10371039
}
1040+
1041+
const cert =
1042+
httpsOptions?.cert && !Array.isArray(httpsOptions.cert)
1043+
? new crypto.X509Certificate(httpsOptions.cert)
1044+
: undefined
1045+
const hostnameFromCert = cert?.subjectAltName
1046+
? extractHostnamesFromSubjectAltName(cert.subjectAltName)
1047+
: []
1048+
1049+
if (hostnameFromCert.length > 0) {
1050+
const existings = new Set([...local, ...network])
1051+
local.push(
1052+
...hostnameFromCert
1053+
.map((hostname) => `https://${hostname}:${port}${base}`)
1054+
.filter((url) => !existings.has(url)),
1055+
)
1056+
}
1057+
10381058
return { local, network }
10391059
}
10401060

1061+
export function extractHostnamesFromSubjectAltName(
1062+
subjectAltName: string,
1063+
): string[] {
1064+
const hostnames: string[] = []
1065+
let remaining = subjectAltName
1066+
while (remaining) {
1067+
const nameEndIndex = remaining.indexOf(':')
1068+
const name = remaining.slice(0, nameEndIndex)
1069+
remaining = remaining.slice(nameEndIndex + 1)
1070+
if (!remaining) break
1071+
1072+
const isQuoted = remaining[0] === '"'
1073+
let value: string
1074+
if (isQuoted) {
1075+
const endQuoteIndex = remaining.indexOf('"', 1)
1076+
value = JSON.parse(remaining.slice(0, endQuoteIndex + 1))
1077+
remaining = remaining.slice(endQuoteIndex + 1)
1078+
} else {
1079+
const maybeEndIndex = remaining.indexOf(',')
1080+
const endIndex = maybeEndIndex === -1 ? remaining.length : maybeEndIndex
1081+
value = remaining.slice(0, endIndex)
1082+
remaining = remaining.slice(endIndex)
1083+
}
1084+
remaining = remaining.slice(/* for , */ 1).trimStart()
1085+
1086+
// [::1] might be included but skip it as it's already included as a local address
1087+
if (name === 'DNS' && value !== '[::1]') {
1088+
hostnames.push(value.replace('*', 'vite'))
1089+
}
1090+
}
1091+
return hostnames
1092+
}
1093+
10411094
export function arraify<T>(target: T | T[]): T[] {
10421095
return Array.isArray(target) ? target : [target]
10431096
}

0 commit comments

Comments
 (0)