Skip to content

Commit

Permalink
fix: ensure /etc/hosts is cleaned if configured
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisbbreuer committed Nov 22, 2024
1 parent c47d212 commit 501027c
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 58 deletions.
16 changes: 2 additions & 14 deletions src/https.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,14 @@ import path from 'node:path'
import { log } from '@stacksjs/cli'
import { addCertToSystemTrustStoreAndSaveCert, createRootCA, generateCertificate as generateCert } from '@stacksjs/tlsx'
import { config } from './config'
import { debugLog } from './utils'
import { debugLog, extractDomains } from './utils'

let cachedSSLConfig: { key: string, cert: string, ca?: string } | null = null

function isMultiProxyConfig(options: ReverseProxyConfigs): options is MultiReverseProxyConfig {
export function isMultiProxyConfig(options: ReverseProxyConfigs): options is MultiReverseProxyConfig {
return 'proxies' in options
}

function extractDomains(options: ReverseProxyConfigs): string[] {
if (isMultiProxyConfig(options)) {
return options.proxies.map((proxy) => {
const domain = proxy.to || 'stacks.localhost'
return domain.startsWith('http') ? new URL(domain).hostname : domain
})
}

const domain = options.to || 'stacks.localhost'
return [domain.startsWith('http') ? new URL(domain).hostname : domain]
}

// Generate wildcard patterns for a domain
function generateWildcardPatterns(domain: string): string[] {
const patterns = new Set<string>()
Expand Down
85 changes: 41 additions & 44 deletions src/start.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable no-console */
import type { SecureServerOptions } from 'node:http2'
import type { ServerOptions } from 'node:https'
import type { MultiReverseProxyConfig, ProxySetupOptions, ReverseProxyConfigs, ReverseProxyOption, ReverseProxyOptions, SingleReverseProxyConfig, SSLConfig } from './types'
import type { CleanupOptions, MultiReverseProxyConfig, ProxySetupOptions, ReverseProxyConfigs, ReverseProxyOption, ReverseProxyOptions, SingleReverseProxyConfig, SSLConfig } from './types'
import * as fs from 'node:fs'
import * as http from 'node:http'
import * as https from 'node:https'
Expand All @@ -11,17 +11,11 @@ import { bold, dim, green, log } from '@stacksjs/cli'
import { version } from '../package.json'
import { addHosts, checkHosts, removeHosts } from './hosts'
import { generateCertificate, getSSLConfig, httpsConfig } from './https'
import { debugLog } from './utils'
import { debugLog, extractDomains } from './utils'

// Keep track of all running servers for cleanup
const activeServers: Set<http.Server | https.Server> = new Set()

interface CleanupOptions {
domain?: string
etcHostsCleanup?: boolean
verbose?: boolean
}

/**
* Cleanup function to close all servers and cleanup hosts file if configured
*/
Expand All @@ -45,34 +39,23 @@ export async function cleanup(options?: CleanupOptions): Promise<void> {
cleanupPromises.push(...serverClosePromises)

// Add hosts file cleanup if configured
if (options?.etcHostsCleanup) {
if (options?.etcHostsCleanup && options.domains?.length) {
debugLog('cleanup', 'Cleaning up hosts file entries', options?.verbose)

// Parse the URL to get the hostname
try {
const domain = options.domain || 'stacks.localhost'
const toUrl = new URL(domain.startsWith('http') ? domain : `http://${domain}`)
const hostname = toUrl.hostname

// Only clean up if it's not localhost
if (!hostname.includes('localhost') && !hostname.includes('127.0.0.1')) {
log.info('Cleaning up hosts file entries...')
cleanupPromises.push(
removeHosts([hostname], options?.verbose)
.then(() => {
debugLog('cleanup', `Removed hosts entry for ${hostname}`, options?.verbose)
})
.catch((err) => {
debugLog('cleanup', `Failed to remove hosts entry: ${err}`, options?.verbose)
log.warn(`Failed to clean up hosts file entry for ${hostname}:`, err)
// Don't throw here to allow the rest of cleanup to continue
}),
)
}
}
catch (err) {
debugLog('cleanup', `Error parsing URL during hosts cleanup: ${err}`, options?.verbose)
log.warn('Failed to parse URL for hosts cleanup:', err)
const domainsToClean = options.domains.filter(domain => !domain.includes('localhost'))

if (domainsToClean.length > 0) {
log.info('Cleaning up hosts file entries...')
cleanupPromises.push(
removeHosts(domainsToClean, options?.verbose)
.then(() => {
debugLog('cleanup', `Removed hosts entries for ${domainsToClean.join(', ')}`, options?.verbose)
})
.catch((err) => {
debugLog('cleanup', `Failed to remove hosts entries: ${err}`, options?.verbose)
log.warn(`Failed to clean up hosts file entries for ${domainsToClean.join(', ')}:`, err)
}),
)
}
}

Expand Down Expand Up @@ -511,7 +494,7 @@ export async function setupReverseProxy(options: ProxySetupOptions): Promise<voi
debugLog('setup', `Setup failed: ${err}`, verbose)
log.error(`Failed to setup reverse proxy: ${(err as Error).message}`)
cleanup({
domain: to,
domains: [to],
etcHostsCleanup,
verbose,
})
Expand Down Expand Up @@ -552,7 +535,7 @@ export function startProxy(options: ReverseProxyOption): void {
debugLog('proxy', `Failed to start proxy: ${err}`, options.verbose)
log.error(`Failed to start proxy: ${err.message}`)
cleanup({
domain: options.to,
domains: [options.to],
etcHostsCleanup: options.etcHostsCleanup,
verbose: options.verbose,
})
Expand Down Expand Up @@ -580,18 +563,36 @@ export async function startProxies(options?: ReverseProxyOptions): Promise<void>
}))
: [options]

// Extract all domains for cleanup
const domains = extractDomains(options as ReverseProxyConfigs)
const sslConfig = options.https ? getSSLConfig() : null

// Setup cleanup handler with all domains
const cleanupHandler = () => cleanup({
domains,
etcHostsCleanup: isMultiProxyConfig(options) ? options.etcHostsCleanup : options.etcHostsCleanup || false,
verbose: isMultiProxyConfig(options) ? options.verbose : options.verbose || false,
})

// Register cleanup handlers
process.on('SIGINT', cleanupHandler)
process.on('SIGTERM', cleanupHandler)
process.on('uncaughtException', (err) => {
debugLog('process', `Uncaught exception: ${err}`, true)
log.error('Uncaught exception:', err)
cleanupHandler()
})

// Now start all proxies with the cached SSL config
for (const option of proxyOptions) {
try {
const domain = option.to || 'stacks.localhost'
const sslConfig = option.https ? getSSLConfig() : null

debugLog('proxy', `Starting proxy for ${domain} with SSL config: ${!!sslConfig}`, option.verbose)

await startServer({
from: option.from || 'localhost:5173',
to: domain,
https: option.https ?? false, // Ensure https is always boolean or TlsConfig
https: option.https ?? false,
etcHostsCleanup: option.etcHostsCleanup || false,
verbose: option.verbose || false,
_cachedSSLConfig: sslConfig,
Expand All @@ -600,11 +601,7 @@ export async function startProxies(options?: ReverseProxyOptions): Promise<void>
catch (err) {
debugLog('proxies', `Failed to start proxy for ${option.to}: ${err}`, option.verbose)
log.error(`Failed to start proxy for ${option.to}:`, err)
cleanup({
domain: option.to,
etcHostsCleanup: option.etcHostsCleanup,
verbose: option.verbose,
})
cleanupHandler()
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ export interface BaseReverseProxyConfig {
to: string // stacks.localhost
}

export interface CleanupOptions {
domains?: string[]
etcHostsCleanup?: boolean
verbose?: boolean
}

export interface SharedProxySettings {
https: boolean | CustomTlsConfig
etcHostsCleanup: boolean
Expand Down
15 changes: 15 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
import type { ReverseProxyConfigs } from './types'
import { isMultiProxyConfig } from './https'

export function debugLog(category: string, message: string, verbose?: boolean): void {
if (verbose) {
// eslint-disable-next-line no-console
console.debug(`[rpx:${category}] ${message}`)
}
}

export function extractDomains(options: ReverseProxyConfigs): string[] {
if (isMultiProxyConfig(options)) {
return options.proxies.map((proxy) => {
const domain = proxy.to || 'stacks.localhost'
return domain.startsWith('http') ? new URL(domain).hostname : domain
})
}

const domain = options.to || 'stacks.localhost'
return [domain.startsWith('http') ? new URL(domain).hostname : domain]
}

0 comments on commit 501027c

Please sign in to comment.