From 52ab5f890e55e4660befc290b6c5735c52b45fb7 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 9 Dec 2024 00:40:03 +0100 Subject: [PATCH] feat: add cleanUrls option chore: wip chore: wip chore: wip --- docs/.vitepress/config.ts | 1 + reverse-proxy.config.ts | 16 +++---- src/config.ts | 1 + src/start.ts | 89 +++++++++++++++++++++++++++++++++++++-- src/types.ts | 1 + 5 files changed, 96 insertions(+), 12 deletions(-) diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 5047942..b54daf9 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -4,6 +4,7 @@ import { defineConfig } from 'vitepress' export default defineConfig({ title: 'Reverse Proxy', description: 'A better developer environment.', + cleanUrls: true, themeConfig: { // https://vitepress.dev/reference/default-theme-config nav: [ diff --git a/reverse-proxy.config.ts b/reverse-proxy.config.ts index 7bc7999..3cc82ab 100644 --- a/reverse-proxy.config.ts +++ b/reverse-proxy.config.ts @@ -1,18 +1,18 @@ import type { ReverseProxyOptions } from './src/types' -import os from 'node:os' -import path from 'node:path' const config: ReverseProxyOptions = { - https: { - caCertPath: path.join(os.homedir(), '.stacks', 'ssl', `stacks.localhost.ca.crt`), - certPath: path.join(os.homedir(), '.stacks', 'ssl', `stacks.localhost.crt`), - keyPath: path.join(os.homedir(), '.stacks', 'ssl', `stacks.localhost.crt.key`), - }, + // https: { + // caCertPath: path.join(os.homedir(), '.stacks', 'ssl', `stacks.localhost.ca.crt`), + // certPath: path.join(os.homedir(), '.stacks', 'ssl', `stacks.localhost.crt`), + // keyPath: path.join(os.homedir(), '.stacks', 'ssl', `stacks.localhost.crt.key`), + // }, + https: true, etcHostsCleanup: true, proxies: [ { from: 'localhost:5173', - to: 'test.local', + to: 'docs.localhost', + cleanUrls: true, }, // { // from: 'localhost:5174', diff --git a/src/config.ts b/src/config.ts index aa4f437..eb77797 100644 --- a/src/config.ts +++ b/src/config.ts @@ -6,6 +6,7 @@ import { loadConfig } from 'bun-config' export const defaultConfig: ReverseProxyConfig = { from: 'localhost:5173', to: 'stacks.localhost', + cleanUrls: false, https: { basePath: '', caCertPath: join(homedir(), '.stacks', 'ssl', `stacks.localhost.ca.crt`), diff --git a/src/start.ts b/src/start.ts index 161ccf7..5af0e8a 100644 --- a/src/start.ts +++ b/src/start.ts @@ -286,16 +286,34 @@ async function createProxyServer( sourceUrl: Pick, ssl: SSLConfig | null, verbose?: boolean, + cleanUrls?: boolean, ): Promise { - debugLog('proxy', `Creating proxy server ${from} -> ${to}`, verbose) + debugLog('proxy', `Creating proxy server ${from} -> ${to} with cleanUrls: ${cleanUrls}`, verbose) const requestHandler = (req: http.IncomingMessage, res: http.ServerResponse) => { debugLog('request', `Incoming request: ${req.method} ${req.url}`, verbose) + let path = req.url || '/' + + // Handle clean URLs + if (cleanUrls) { + // Don't modify URLs that already have an extension + if (!path.match(/\.[a-z0-9]+$/i)) { + // If path ends with trailing slash, look for index.html + if (path.endsWith('/')) { + path = `${path}index.html` + } + // Otherwise append .html + else { + path = `${path}.html` + } + } + } + const proxyOptions = { hostname: sourceUrl.hostname, port: fromPort, - path: req.url, + path, method: req.method, headers: { ...req.headers, @@ -308,6 +326,62 @@ async function createProxyServer( const proxyReq = http.request(proxyOptions, (proxyRes) => { debugLog('response', `Proxy response received with status ${proxyRes.statusCode}`, verbose) + // Handle 404s for clean URLs + if (cleanUrls && proxyRes.statusCode === 404) { + // Try alternative paths for clean URLs + const alternativePaths = [] + + // If the path ends with .html, try without it + if (path.endsWith('.html')) { + alternativePaths.push(path.slice(0, -5)) + } + // If path doesn't end with .html, try with it + else if (!path.match(/\.[a-z0-9]+$/i)) { + alternativePaths.push(`${path}.html`) + } + // If path doesn't end with /, try with /index.html + if (!path.endsWith('/')) { + alternativePaths.push(`${path}/index.html`) + } + + // Try alternative paths + if (alternativePaths.length > 0) { + debugLog('cleanUrls', `Trying alternative paths: ${alternativePaths.join(', ')}`, verbose) + + // Try each alternative path + const tryNextPath = (paths: string[]) => { + if (paths.length === 0) { + // If no alternatives work, send original 404 + res.writeHead(proxyRes.statusCode || 404, proxyRes.headers) + proxyRes.pipe(res) + return + } + + const altPath = paths[0] + const altOptions = { ...proxyOptions, path: altPath } + + const altReq = http.request(altOptions, (altRes) => { + if (altRes.statusCode === 200) { + // If we found a matching path, use it + debugLog('cleanUrls', `Found matching path: ${altPath}`, verbose) + res.writeHead(altRes.statusCode, altRes.headers) + altRes.pipe(res) + } + else { + // Try next alternative + tryNextPath(paths.slice(1)) + } + }) + + altReq.on('error', () => tryNextPath(paths.slice(1))) + altReq.end() + } + + tryNextPath(alternativePaths) + return + } + } + // Add security headers const headers = { ...proxyRes.headers, @@ -329,7 +403,7 @@ async function createProxyServer( req.pipe(proxyReq) } - // Complete SSL configuration + // SSL configuration const serverOptions: (ServerOptions & SecureServerOptions) | undefined = ssl ? { key: ssl.key, @@ -389,6 +463,9 @@ async function createProxyServer( console.log(` - HTTP/2 enabled`) console.log(` - HSTS enabled`) } + if (cleanUrls) { + console.log(` ${green('➜')} Clean URLs enabled`) + } resolve() }) @@ -482,6 +559,7 @@ export function startProxy(options: ReverseProxyOption): void { const serverOptions: SingleReverseProxyConfig = { from: mergedOptions.from, to: mergedOptions.to, + cleanUrls: mergedOptions.cleanUrls, https: httpsConfig(mergedOptions), etcHostsCleanup: mergedOptions.etcHostsCleanup, verbose: mergedOptions.verbose, @@ -541,12 +619,14 @@ export async function startProxies(options?: ReverseProxyOptions): Promise ...proxy, https: mergedOptions.https, etcHostsCleanup: mergedOptions.etcHostsCleanup, + cleanUrls: mergedOptions.cleanUrls, verbose: mergedOptions.verbose, _cachedSSLConfig: mergedOptions._cachedSSLConfig, })) : [{ from: mergedOptions.from || 'localhost:5173', to: mergedOptions.to || 'stacks.localhost', + cleanUrls: mergedOptions.cleanUrls || false, https: mergedOptions.https, etcHostsCleanup: mergedOptions.etcHostsCleanup, verbose: mergedOptions.verbose, @@ -582,7 +662,8 @@ export async function startProxies(options?: ReverseProxyOptions): Promise await startServer({ from: option.from || 'localhost:5173', to: domain, - https: option.https ?? false, + cleanUrls: option.cleanUrls || false, + https: option.https || false, etcHostsCleanup: option.etcHostsCleanup || false, verbose: option.verbose || false, _cachedSSLConfig: sslConfig, diff --git a/src/types.ts b/src/types.ts index a6f7f52..988ab14 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,6 +3,7 @@ import type { TlsConfig, TlsOption } from '@stacksjs/tlsx' export interface BaseReverseProxyConfig { from: string // localhost:5173 to: string // stacks.localhost + cleanUrls: boolean // false } export type BaseReverseProxyOptions = Partial