Skip to content

Commit

Permalink
feat: allow for cleaning up certificates
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisbbreuer committed Dec 13, 2024
1 parent 434a163 commit a22b7d7
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 23 deletions.
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,17 @@ Given the npm package is installed:
import type { TlsConfig } from '@stacksjs/rpx'
import { startProxy } from '@stacksjs/rpx'

export interface CleanupConfig {
hosts: boolean // clean up /etc/hosts, defaults to false
certs: boolean // clean up certificates, defaults to false
}

export interface ReverseProxyConfig {
from: string // domain to proxy from, defaults to localhost:3000
to: string // domain to proxy to, defaults to stacks.localhost
cleanUrls?: boolean // removes the .html extension from URLs, defaults to false
https: boolean | TlsConfig // automatically uses https, defaults to true, also redirects http to https
etcHostsCleanup?: boolean // automatically cleans up /etc/hosts, defaults to false
cleanup?: boolean | CleanupConfig // automatically cleans up /etc/hosts, defaults to false
verbose: boolean // log verbose output, defaults to false
}

Expand All @@ -58,7 +63,7 @@ const config: ReverseProxyOptions = {
to: 'my-docs.localhost',
cleanUrls: true,
https: true,
etcHostsCleanup: true,
cleanup: false,
}

startProxy(config)
Expand All @@ -79,7 +84,10 @@ const config: ReverseProxyOptions = {
keyPath: path.join(os.homedir(), '.stacks', 'ssl', `stacks.localhost.crt.key`),
},

etcHostsCleanup: true,
cleanup: {
hosts: true,
certs: false,
},

proxies: [
{
Expand Down Expand Up @@ -121,6 +129,7 @@ import path from 'node:path'
const config: ReverseProxyOptions = {
from: 'localhost:5173',
to: 'stacks.localhost',

https: {
domain: 'stacks.localhost',
hostCertCN: 'stacks.localhost',
Expand All @@ -137,6 +146,7 @@ const config: ReverseProxyOptions = {
validityDays: 180,
verbose: false,
},

verbose: false,
}

Expand Down
11 changes: 9 additions & 2 deletions reverse-proxy.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,26 @@ const config: ReverseProxyOptions = {
// keyPath: path.join(os.homedir(), '.stacks', 'ssl', `stacks.localhost.crt.key`),
// },
https: true,
etcHostsCleanup: true,

cleanup: {
hosts: true,
certs: false,
},

proxies: [
{
from: 'localhost:5173',
to: 'docs.localhost',
cleanUrls: true,
},

// {
// from: 'localhost:5174',
// to: 'test.local',
// },
],
vitePluginUsage: true,

vitePluginUsage: false,
verbose: false,
}

Expand Down
21 changes: 20 additions & 1 deletion src/https.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { join } from 'node:path'
import { log } from '@stacksjs/cli'
import { addCertToSystemTrustStoreAndSaveCert, createRootCA, generateCertificate as generateCert } from '@stacksjs/tlsx'
import { config } from './config'
import { debugLog, getPrimaryDomain, isMultiProxyConfig, isMultiProxyOptions, isSingleProxyOptions, isValidRootCA } from './utils'
import { debugLog, getPrimaryDomain, isMultiProxyConfig, isMultiProxyOptions, isSingleProxyOptions, isValidRootCA, safeDeleteFile } from './utils'

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

Expand Down Expand Up @@ -314,3 +314,22 @@ export function httpsConfig(options: ReverseProxyOption | ReverseProxyOptions, v
})),
}
}

/**
* Clean up SSL certificates for a specific domain
*/
export async function cleanupCertificates(domain: string, verbose?: boolean): Promise<void> {
const certPaths = generateSSLPaths({ to: domain, verbose })

// Define all possible certificate files
const filesToDelete = [
certPaths.caCertPath,
certPaths.certPath,
certPaths.keyPath,
]

debugLog('certificates', `Attempting to clean up relating certificates`, verbose)

// Delete all files concurrently
await Promise.all(filesToDelete.map(file => safeDeleteFile(file, verbose)))
}
47 changes: 35 additions & 12 deletions src/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ import { bold, dim, green, log } from '@stacksjs/cli'
import { version } from '../package.json'
import { config } from './config'
import { addHosts, checkHosts, removeHosts } from './hosts'
import { checkExistingCertificates, generateCertificate, httpsConfig, loadSSLConfig } from './https'
import { checkExistingCertificates, cleanupCertificates, generateCertificate, httpsConfig, loadSSLConfig } from './https'
import { debugLog, isMultiProxyConfig } from './utils'

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

/**
* Cleanup function to close all servers and cleanup hosts file if configured
* Cleanup function to close all servers, cleanup hosts file, and remove certificates if configured
*/
export async function cleanup(options?: CleanupOptions): Promise<void> {
debugLog('cleanup', 'Starting cleanup process', options?.verbose)
Expand All @@ -38,8 +38,8 @@ export async function cleanup(options?: CleanupOptions): Promise<void> {
)
cleanupPromises.push(...serverClosePromises)

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

const domainsToClean = options.domains.filter(domain => !domain.includes('localhost'))
Expand All @@ -59,6 +59,26 @@ export async function cleanup(options?: CleanupOptions): Promise<void> {
}
}

// certificate cleanup if configured
if (options?.certs && options.domains?.length) {
debugLog('cleanup', 'Cleaning up SSL certificates', options?.verbose)
log.info('Cleaning up SSL certificates...')

const certCleanupPromises = options.domains.map(async (domain) => {
try {
await cleanupCertificates(domain, options?.verbose)
debugLog('cleanup', `Removed certificates for ${domain}`, options?.verbose)
}
catch (err) {
console.log('checkError', err)
debugLog('cleanup', `Failed to remove certificates for ${domain}: ${err}`, options?.verbose)
log.warn(`Failed to clean up certificates for ${domain}:`, err)
}
})

cleanupPromises.push(...certCleanupPromises)
}

try {
await Promise.all(cleanupPromises)
debugLog('cleanup', 'All cleanup tasks completed successfully', options?.verbose)
Expand Down Expand Up @@ -483,7 +503,7 @@ async function createProxyServer(
export async function setupReverseProxy(options: ProxySetupOptions): Promise<void> {
debugLog('setup', `Setting up reverse proxy: ${JSON.stringify(options)}`, options.verbose)

const { from, to, fromPort, sourceUrl, ssl, verbose, etcHostsCleanup, vitePluginUsage, portManager } = options
const { from, to, fromPort, sourceUrl, ssl, verbose, cleanup: cleanupOptions, vitePluginUsage, portManager } = options
const httpPort = 80
const httpsPort = 443
const hostname = '0.0.0.0'
Expand Down Expand Up @@ -528,7 +548,8 @@ export async function setupReverseProxy(options: ProxySetupOptions): Promise<voi
log.error(`Failed to setup reverse proxy: ${(err as Error).message}`)
cleanup({
domains: [to],
etcHostsCleanup,
hosts: typeof cleanupOptions === 'boolean' ? cleanupOptions : cleanupOptions?.hosts,
certs: typeof cleanupOptions === 'boolean' ? cleanupOptions : cleanupOptions?.certs,
verbose,
})
}
Expand Down Expand Up @@ -564,7 +585,7 @@ export function startProxy(options: ReverseProxyOption): void {
to: mergedOptions.to,
cleanUrls: mergedOptions.cleanUrls,
https: httpsConfig(mergedOptions),
etcHostsCleanup: mergedOptions.etcHostsCleanup,
cleanup: mergedOptions.cleanup,
vitePluginUsage: mergedOptions.vitePluginUsage,
verbose: mergedOptions.verbose,
}
Expand All @@ -576,7 +597,8 @@ export function startProxy(options: ReverseProxyOption): void {
log.error(`Failed to start proxy: ${err.message}`)
cleanup({
domains: [mergedOptions.to],
etcHostsCleanup: mergedOptions.etcHostsCleanup,
hosts: typeof mergedOptions.cleanup === 'boolean' ? mergedOptions.cleanup : mergedOptions.cleanup?.hosts,
certs: typeof mergedOptions.cleanup === 'boolean' ? mergedOptions.cleanup : mergedOptions.cleanup?.certs,
verbose: mergedOptions.verbose,
})
})
Expand Down Expand Up @@ -622,7 +644,7 @@ export async function startProxies(options?: ReverseProxyOptions): Promise<void>
? mergedOptions.proxies.map(proxy => ({
...proxy,
https: mergedOptions.https,
etcHostsCleanup: mergedOptions.etcHostsCleanup,
cleanup: mergedOptions.cleanup,
cleanUrls: mergedOptions.cleanUrls,
vitePluginUsage: mergedOptions.vitePluginUsage,
verbose: mergedOptions.verbose,
Expand All @@ -633,7 +655,7 @@ export async function startProxies(options?: ReverseProxyOptions): Promise<void>
to: mergedOptions.to || 'stacks.localhost',
cleanUrls: mergedOptions.cleanUrls || false,
https: mergedOptions.https,
etcHostsCleanup: mergedOptions.etcHostsCleanup,
cleanup: mergedOptions.cleanup,
vitePluginUsage: mergedOptions.vitePluginUsage,
verbose: mergedOptions.verbose,
_cachedSSLConfig: mergedOptions._cachedSSLConfig,
Expand All @@ -646,7 +668,8 @@ export async function startProxies(options?: ReverseProxyOptions): Promise<void>
// Setup cleanup handler
const cleanupHandler = () => cleanup({
domains,
etcHostsCleanup: mergedOptions.etcHostsCleanup || false,
hosts: typeof mergedOptions.cleanup === 'boolean' ? mergedOptions.cleanup : mergedOptions.cleanup?.hosts,
certs: typeof mergedOptions.cleanup === 'boolean' ? mergedOptions.cleanup : mergedOptions.cleanup?.certs,
verbose: mergedOptions.verbose || false,
})

Expand All @@ -670,7 +693,7 @@ export async function startProxies(options?: ReverseProxyOptions): Promise<void>
to: domain,
cleanUrls: option.cleanUrls || false,
https: option.https || false,
etcHostsCleanup: option.etcHostsCleanup || false,
cleanup: option.cleanup || false,
vitePluginUsage: option.vitePluginUsage || false,
verbose: option.verbose || false,
_cachedSSLConfig: sslConfig,
Expand Down
13 changes: 8 additions & 5 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@ export interface BaseReverseProxyConfig {
}
export type BaseReverseProxyOptions = Partial<BaseReverseProxyConfig>

export interface CleanupOptions {
domains?: string[]
etcHostsCleanup?: boolean
verbose?: boolean
export interface CleanupConfig {
domains: string[] // default: [], if only specific domain/s should be cleaned up
hosts: boolean // default: true, if hosts file should be cleaned up
certs: boolean // default: false, if certificates should be cleaned up
verbose: boolean // default: false
}

export type CleanupOptions = Partial<CleanupConfig>

export interface SharedProxyConfig {
https: boolean | TlsOption
etcHostsCleanup: boolean
cleanup: boolean | CleanupOptions
vitePluginUsage: boolean
verbose: boolean
_cachedSSLConfig?: SSLConfig | null
Expand Down
18 changes: 18 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { MultiReverseProxyConfig, ReverseProxyConfigs, ReverseProxyOption, ReverseProxyOptions, SingleReverseProxyConfig } from './types'
import * as fs from 'node:fs/promises'

export function debugLog(category: string, message: string, verbose?: boolean): void {
if (verbose) {
Expand Down Expand Up @@ -75,3 +76,20 @@ export function isMultiProxyOptions(options: ReverseProxyOption | ReverseProxyOp
export function isSingleProxyOptions(options: ReverseProxyOption | ReverseProxyOptions): options is SingleReverseProxyConfig {
return 'to' in options && typeof (options as SingleReverseProxyConfig).to === 'string'
}

/**
* Safely delete a file if it exists
*/
export async function safeDeleteFile(filePath: string, verbose?: boolean): Promise<void> {
try {
// Try to delete the file directly without checking existence first
await fs.unlink(filePath)
debugLog('certificates', `Successfully deleted: ${filePath}`, verbose)
}
catch (err) {
// Ignore errors where file doesn't exist
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
debugLog('certificates', `Warning: Could not delete ${filePath}: ${err}`, verbose)
}
}
}

0 comments on commit a22b7d7

Please sign in to comment.