Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: show hosts in cert in CLI #19317

Merged
merged 1 commit into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions packages/vite/src/node/__tests__/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import fs from 'node:fs'
import path from 'node:path'
import crypto from 'node:crypto'
import { describe, expect, test } from 'vitest'
import {
asyncFlatten,
bareImportRE,
extractHostnamesFromSubjectAltName,
flattenId,
generateCodeFrame,
getHash,
Expand Down Expand Up @@ -165,6 +167,59 @@ describe('resolveHostname', () => {
})
})

describe('extractHostnamesFromSubjectAltName', () => {
const testCases = [
['DNS:localhost', ['localhost']],
['DNS:localhost, DNS:foo.localhost', ['localhost', 'foo.localhost']],
['DNS:*.localhost', ['vite.localhost']],
['DNS:[::1]', []], // [::1] is skipped
['othername:"foo,bar", DNS:localhost', ['localhost']], // handle quoted correctly
] as const

for (const [input, expected] of testCases) {
test(`should extract names from subjectAltName: ${input}`, () => {
expect(extractHostnamesFromSubjectAltName(input)).toStrictEqual(expected)
})
}

test('should extract names from actual certificate', () => {
const certText = `
-----BEGIN CERTIFICATE-----
MIID7zCCAtegAwIBAgIJS9D2rIN7tA8mMA0GCSqGSIb3DQEBCwUAMGkxFDASBgNV
BAMTC2V4YW1wbGUub3JnMQswCQYDVQQGEwJVUzERMA8GA1UECBMIVmlyZ2luaWEx
EzARBgNVBAcTCkJsYWNrc2J1cmcxDTALBgNVBAoTBFRlc3QxDTALBgNVBAsTBFRl
c3QwHhcNMjUwMTMwMDQxNTI1WhcNMjUwMzAxMDQxNTI1WjBpMRQwEgYDVQQDEwtl
eGFtcGxlLm9yZzELMAkGA1UEBhMCVVMxETAPBgNVBAgTCFZpcmdpbmlhMRMwEQYD
VQQHEwpCbGFja3NidXJnMQ0wCwYDVQQKEwRUZXN0MQ0wCwYDVQQLEwRUZXN0MIIB
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxNPlCqTmUZ7/F7GyFWDopqZ6
w19Y7/98B10JEeFGTAQIj/RP2UgZNcTABQDUvtkF7y+bOeoVJW7Zz8ozQYhRaDp8
CN2gXMcYeTUku/pKLXyCzHHVrOPAXTeU7sMRgLvPCrrJtx5OjvndW+O/PhohPRi3
iEpPvpM8gi7MVRGhnWVSx0/Ynx5c0+/vqyBTzrM2OX7Ufg8Nv7LaTXpCAnmIQp+f
Sqq7HZ7t6Y7laS4RApityvlnFHZ4f2cEibAKv/vXLED7bgAlGb8R1viPRdMtAPuI
MYvHBgGFjyX1fmq6Mz3aqlAscJILtbQlwty1oYyaENE0lq8+nZXQ+t6I+CIVLQID
AQABo4GZMIGWMAsGA1UdDwQEAwIC9DAxBgNVHSUEKjAoBggrBgEFBQcDAQYIKwYB
BQUHAwIGCCsGAQUFBwMDBggrBgEFBQcDCDBUBgNVHREETTBLgglsb2NhbGhvc3SC
DWZvby5sb2NhbGhvc3SCECoudml0ZS5sb2NhbGhvc3SCBVs6OjFdhwR/AAABhxD+
gAAAAAAAAAAAAAAAAAABMA0GCSqGSIb3DQEBCwUAA4IBAQBi302qLCgxWsUalgc2
olFxVKob1xCciS8yUVX6HX0vza0WJ7oGW6qZsBbQtfgDwB/dHv7rwsfpjRWvFhmq
gEUrewa1h0TIC+PPTYYz4M0LOwcLIWZLZr4am1eI7YP9NDgRdhfAfM4hw20vjf2a
kYLKyRTC5+3/ly5opMq+CGLQ8/gnFxhP3ho8JYrRnqLeh3KCTGen3kmbAhD4IOJ9
lxMwFPTTWLFFjxbXjXmt5cEiL2mpcq13VCF2HmheCen37CyYIkrwK9IfLhBd5QQh
WEIBLwjKCAscrtyayXWp6zUTmgvb8PQf//3Mh2DiEngAi3WI/nL+8Y0RkqbvxBar
X2JN
-----END CERTIFICATE-----
`.trim()
const cert = new crypto.X509Certificate(certText)
expect(
extractHostnamesFromSubjectAltName(cert.subjectAltName ?? ''),
).toStrictEqual([
'localhost',
'foo.localhost',
'vite.vite.localhost', // *.vite.localhost
])
})
})

describe('posToNumber', () => {
test('simple', () => {
const actual = posToNumber('a\nb', { line: 2, column: 0 })
Expand Down
8 changes: 3 additions & 5 deletions packages/vite/src/node/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,9 @@ export async function preview(
)
}

const httpsOptions = await resolveHttpsConfig(config.server.https)
const app = connect() as Connect.Server
const httpServer = await resolveHttpServer(
config.preview,
app,
await resolveHttpsConfig(config.preview.https),
)
const httpServer = await resolveHttpServer(config.preview, app, httpsOptions)
setClientErrorHandler(httpServer, config.logger)

const options = config.preview
Expand Down Expand Up @@ -274,6 +271,7 @@ export async function preview(
server.resolvedUrls = await resolveServerUrls(
httpServer,
config.preview,
httpsOptions,
config,
)

Expand Down
1 change: 1 addition & 0 deletions packages/vite/src/node/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,7 @@ export async function _createServer(
server.resolvedUrls = await resolveServerUrls(
httpServer,
config.server,
httpsOptions,
config,
)
if (!isRestart && config.server.open) server.openBrowser()
Expand Down
53 changes: 53 additions & 0 deletions packages/vite/src/node/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import path from 'node:path'
import { exec } from 'node:child_process'
import crypto from 'node:crypto'
import { URL, fileURLToPath } from 'node:url'
import type { ServerOptions as HttpsServerOptions } from 'node:https'
import { builtinModules, createRequire } from 'node:module'
import { promises as dns } from 'node:dns'
import { performance } from 'node:perf_hooks'
Expand Down Expand Up @@ -982,6 +983,7 @@ export async function resolveHostname(
export async function resolveServerUrls(
server: Server,
options: CommonServerOptions,
httpsOptions: HttpsServerOptions | undefined,
config: ResolvedConfig,
): Promise<ResolvedServerUrls> {
const address = server.address()
Expand Down Expand Up @@ -1035,9 +1037,60 @@ export async function resolveServerUrls(
}
})
}

const cert =
httpsOptions?.cert && !Array.isArray(httpsOptions.cert)
? new crypto.X509Certificate(httpsOptions.cert)
: undefined
const hostnameFromCert = cert?.subjectAltName
? extractHostnamesFromSubjectAltName(cert.subjectAltName)
: []

if (hostnameFromCert.length > 0) {
const existings = new Set([...local, ...network])
local.push(
Copy link
Member Author

@sapphi-red sapphi-red Jan 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with adding all of these to local so that Network: use --host to expose is shown when --host is not passed.
We can also hide hosts that resolves to addresses that are not loopback addresses when --host is not passed, but I felt that confusing.

...hostnameFromCert
.map((hostname) => `https://${hostname}:${port}${base}`)
.filter((url) => !existings.has(url)),
)
}

return { local, network }
}

export function extractHostnamesFromSubjectAltName(
subjectAltName: string,
): string[] {
const hostnames: string[] = []
let remaining = subjectAltName
while (remaining) {
const nameEndIndex = remaining.indexOf(':')
const name = remaining.slice(0, nameEndIndex)
remaining = remaining.slice(nameEndIndex + 1)
if (!remaining) break

const isQuoted = remaining[0] === '"'
let value: string
if (isQuoted) {
const endQuoteIndex = remaining.indexOf('"', 1)
value = JSON.parse(remaining.slice(0, endQuoteIndex + 1))
remaining = remaining.slice(endQuoteIndex + 1)
} else {
const maybeEndIndex = remaining.indexOf(',')
const endIndex = maybeEndIndex === -1 ? remaining.length : maybeEndIndex
value = remaining.slice(0, endIndex)
remaining = remaining.slice(endIndex)
}
remaining = remaining.slice(/* for , */ 1).trimStart()

// [::1] might be included but skip it as it's already included as a local address
if (name === 'DNS' && value !== '[::1]') {
hostnames.push(value.replace('*', 'vite'))
}
}
return hostnames
}

export function arraify<T>(target: T | T[]): T[] {
return Array.isArray(target) ? target : [target]
}
Expand Down