From f40868efbb98413bbf9c6bb6871e5a853f1c9793 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Thu, 30 Jan 2025 13:45:54 +0900 Subject: [PATCH] feat: show hosts in cert in CLI --- .../vite/src/node/__tests__/utils.spec.ts | 55 +++++++++++++++++++ packages/vite/src/node/preview.ts | 8 +-- packages/vite/src/node/server/index.ts | 1 + packages/vite/src/node/utils.ts | 53 ++++++++++++++++++ 4 files changed, 112 insertions(+), 5 deletions(-) diff --git a/packages/vite/src/node/__tests__/utils.spec.ts b/packages/vite/src/node/__tests__/utils.spec.ts index 1e7ca99d88d653..f0875f28ba4711 100644 --- a/packages/vite/src/node/__tests__/utils.spec.ts +++ b/packages/vite/src/node/__tests__/utils.spec.ts @@ -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, @@ -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 }) diff --git a/packages/vite/src/node/preview.ts b/packages/vite/src/node/preview.ts index 5b92a8d7addc42..e13ed993cf7e03 100644 --- a/packages/vite/src/node/preview.ts +++ b/packages/vite/src/node/preview.ts @@ -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 @@ -274,6 +271,7 @@ export async function preview( server.resolvedUrls = await resolveServerUrls( httpServer, config.preview, + httpsOptions, config, ) diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index b3c10bccf92456..5fb183b635f3db 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -649,6 +649,7 @@ export async function _createServer( server.resolvedUrls = await resolveServerUrls( httpServer, config.server, + httpsOptions, config, ) if (!isRestart && config.server.open) server.openBrowser() diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index 4951d798768420..088702dfdd3693 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -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' @@ -982,6 +983,7 @@ export async function resolveHostname( export async function resolveServerUrls( server: Server, options: CommonServerOptions, + httpsOptions: HttpsServerOptions | undefined, config: ResolvedConfig, ): Promise { const address = server.address() @@ -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( + ...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(target: T | T[]): T[] { return Array.isArray(target) ? target : [target] }