mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-26 15:07:39 +00:00
feat: add compression, security headers, and caching optimizations
Implement several performance and security improvements for Express 5: Performance: - Add compression middleware with Brotli support for all responses - Enable WebSocket compression (perMessageDeflate) for terminal data - Exclude compression for SSE streams (/api/sessions/:id/stream) to prevent asciicast issues - Add intelligent caching headers for static assets: - Immutable assets (JS, CSS, fonts, images): 1 year cache - HTML files: 1 hour cache - Enable ETags and Last-Modified headers Security: - Add Helmet middleware for security headers - Disable CSP and COEP to maintain compatibility with terminal features Additional improvements: - Remove obsolete @ts-expect-error directives (Express 5 properly types res.flush) - Balanced compression level (6) for optimal performance 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
3beb90a0fd
commit
93ba0064bd
5 changed files with 124 additions and 7 deletions
|
|
@ -68,7 +68,9 @@
|
|||
"authenticate-pam": "^1.0.5",
|
||||
"bonjour-service": "^1.3.0",
|
||||
"chalk": "^5.4.1",
|
||||
"compression": "^1.8.0",
|
||||
"express": "^5.1.0",
|
||||
"helmet": "^8.1.0",
|
||||
"http-proxy-middleware": "^3.0.5",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lit": "^3.3.1",
|
||||
|
|
@ -87,6 +89,7 @@
|
|||
"@playwright/test": "^1.54.1",
|
||||
"@prettier/plugin-oxc": "^0.0.4",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@types/compression": "^1.8.1",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/mime-types": "^3.0.1",
|
||||
|
|
|
|||
|
|
@ -50,9 +50,15 @@ importers:
|
|||
chalk:
|
||||
specifier: ^5.4.1
|
||||
version: 5.4.1
|
||||
compression:
|
||||
specifier: ^1.8.0
|
||||
version: 1.8.0
|
||||
express:
|
||||
specifier: ^5.1.0
|
||||
version: 5.1.0
|
||||
helmet:
|
||||
specifier: ^8.1.0
|
||||
version: 8.1.0
|
||||
http-proxy-middleware:
|
||||
specifier: ^3.0.5
|
||||
version: 3.0.5
|
||||
|
|
@ -102,6 +108,9 @@ importers:
|
|||
'@testing-library/dom':
|
||||
specifier: ^10.4.0
|
||||
version: 10.4.0
|
||||
'@types/compression':
|
||||
specifier: ^1.8.1
|
||||
version: 1.8.1
|
||||
'@types/express':
|
||||
specifier: ^5.0.3
|
||||
version: 5.0.3
|
||||
|
|
@ -839,6 +848,9 @@ packages:
|
|||
'@types/co-body@6.1.3':
|
||||
resolution: {integrity: sha512-UhuhrQ5hclX6UJctv5m4Rfp52AfG9o9+d9/HwjxhVB5NjXxr5t9oKgJxN8xRHgr35oo8meUEHUPFWiKg6y71aA==}
|
||||
|
||||
'@types/compression@1.8.1':
|
||||
resolution: {integrity: sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==}
|
||||
|
||||
'@types/connect@3.4.38':
|
||||
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
||||
|
||||
|
|
@ -1363,6 +1375,14 @@ packages:
|
|||
component-emitter@1.3.1:
|
||||
resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==}
|
||||
|
||||
compressible@2.0.18:
|
||||
resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
compression@1.8.0:
|
||||
resolution: {integrity: sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
concat-stream@2.0.0:
|
||||
resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==}
|
||||
engines: {'0': node >= 6.0}
|
||||
|
|
@ -1434,6 +1454,14 @@ packages:
|
|||
debounce@1.2.1:
|
||||
resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==}
|
||||
|
||||
debug@2.6.9:
|
||||
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
debug@3.2.7:
|
||||
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
|
||||
peerDependencies:
|
||||
|
|
@ -1829,6 +1857,10 @@ packages:
|
|||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
helmet@8.1.0:
|
||||
resolution: {integrity: sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
html-escaper@2.0.2:
|
||||
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
||||
|
||||
|
|
@ -2266,6 +2298,9 @@ packages:
|
|||
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
ms@2.0.0:
|
||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
|
|
@ -2299,6 +2334,10 @@ packages:
|
|||
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
negotiator@0.6.4:
|
||||
resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
negotiator@1.0.0:
|
||||
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
|
@ -2353,6 +2392,10 @@ packages:
|
|||
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
on-headers@1.0.2:
|
||||
resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
|
||||
|
|
@ -3794,6 +3837,11 @@ snapshots:
|
|||
'@types/node': 24.0.13
|
||||
'@types/qs': 6.14.0
|
||||
|
||||
'@types/compression@1.8.1':
|
||||
dependencies:
|
||||
'@types/express': 5.0.3
|
||||
'@types/node': 24.0.13
|
||||
|
||||
'@types/connect@3.4.38':
|
||||
dependencies:
|
||||
'@types/node': 24.0.13
|
||||
|
|
@ -4420,6 +4468,22 @@ snapshots:
|
|||
|
||||
component-emitter@1.3.1: {}
|
||||
|
||||
compressible@2.0.18:
|
||||
dependencies:
|
||||
mime-db: 1.54.0
|
||||
|
||||
compression@1.8.0:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
compressible: 2.0.18
|
||||
debug: 2.6.9
|
||||
negotiator: 0.6.4
|
||||
on-headers: 1.0.2
|
||||
safe-buffer: 5.2.1
|
||||
vary: 1.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
concat-stream@2.0.0:
|
||||
dependencies:
|
||||
buffer-from: 1.1.2
|
||||
|
|
@ -4485,6 +4549,10 @@ snapshots:
|
|||
|
||||
debounce@1.2.1: {}
|
||||
|
||||
debug@2.6.9:
|
||||
dependencies:
|
||||
ms: 2.0.0
|
||||
|
||||
debug@3.2.7:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
|
@ -4901,6 +4969,8 @@ snapshots:
|
|||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
helmet@8.1.0: {}
|
||||
|
||||
html-escaper@2.0.2: {}
|
||||
|
||||
http-assert@1.5.0:
|
||||
|
|
@ -5370,6 +5440,8 @@ snapshots:
|
|||
|
||||
mrmime@2.0.1: {}
|
||||
|
||||
ms@2.0.0: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
multer@2.0.1:
|
||||
|
|
@ -5403,6 +5475,8 @@ snapshots:
|
|||
|
||||
negotiator@0.6.3: {}
|
||||
|
||||
negotiator@0.6.4: {}
|
||||
|
||||
negotiator@1.0.0: {}
|
||||
|
||||
netmask@2.0.2: {}
|
||||
|
|
@ -5441,6 +5515,8 @@ snapshots:
|
|||
dependencies:
|
||||
ee-first: 1.1.1
|
||||
|
||||
on-headers@1.0.2: {}
|
||||
|
||||
once@1.4.0:
|
||||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
|
|
|
|||
|
|
@ -846,7 +846,6 @@ export function createSessionRoutes(config: SessionRoutesConfig): Router {
|
|||
|
||||
// Send initial connection event
|
||||
res.write(':ok\n\n');
|
||||
// @ts-expect-error - flush exists but not in types
|
||||
if (res.flush) res.flush();
|
||||
|
||||
// Add client to stream watcher
|
||||
|
|
@ -856,7 +855,6 @@ export function createSessionRoutes(config: SessionRoutesConfig): Router {
|
|||
// Send heartbeat every 30 seconds to keep connection alive
|
||||
const heartbeat = setInterval(() => {
|
||||
res.write(':heartbeat\n\n');
|
||||
// @ts-expect-error - flush exists but not in types
|
||||
if (res.flush) res.flush();
|
||||
}, 30000);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import chalk from 'chalk';
|
||||
import compression from 'compression';
|
||||
import type { Response as ExpressResponse } from 'express';
|
||||
import express from 'express';
|
||||
import * as fs from 'fs';
|
||||
import helmet from 'helmet';
|
||||
import type * as http from 'http';
|
||||
import { createServer } from 'http';
|
||||
import * as os from 'os';
|
||||
|
|
@ -374,7 +376,34 @@ export async function createApp(): Promise<AppInstance> {
|
|||
logger.log('Initializing VibeTunnel server components');
|
||||
const app = express();
|
||||
const server = createServer(app);
|
||||
const wss = new WebSocketServer({ noServer: true });
|
||||
const wss = new WebSocketServer({ noServer: true, perMessageDeflate: true });
|
||||
|
||||
// Add security headers with Helmet
|
||||
app.use(
|
||||
helmet({
|
||||
contentSecurityPolicy: false, // We handle CSP ourselves for the web terminal
|
||||
crossOriginEmbedderPolicy: false, // Allow embedding in iframes for integrations
|
||||
})
|
||||
);
|
||||
logger.debug('Configured security headers with helmet');
|
||||
|
||||
// Add compression middleware with Brotli support
|
||||
// Skip compression for SSE streams (asciicast)
|
||||
app.use(
|
||||
compression({
|
||||
filter: (req, res) => {
|
||||
// Skip compression for Server-Sent Events (asciicast streams)
|
||||
if (req.path.match(/\/api\/sessions\/[^/]+\/stream$/)) {
|
||||
return false;
|
||||
}
|
||||
// Use default filter for other requests
|
||||
return compression.filter(req, res);
|
||||
},
|
||||
// Enable Brotli compression with highest priority
|
||||
level: 6, // Balanced compression level
|
||||
})
|
||||
);
|
||||
logger.debug('Configured compression middleware (with asciicast exclusion)');
|
||||
|
||||
// Add JSON body parser middleware with size limit
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
|
|
@ -519,14 +548,27 @@ export async function createApp(): Promise<AppInstance> {
|
|||
localAuthToken: config.localAuthToken || undefined,
|
||||
});
|
||||
|
||||
// Serve static files with .html extension handling
|
||||
// Serve static files with .html extension handling and caching headers
|
||||
const publicPath = path.join(process.cwd(), 'public');
|
||||
app.use(
|
||||
express.static(publicPath, {
|
||||
extensions: ['html'], // This allows /logs to resolve to /logs.html
|
||||
maxAge: '1d', // Cache static assets for 1 day
|
||||
etag: true, // Enable ETag generation
|
||||
lastModified: true, // Enable Last-Modified header
|
||||
setHeaders: (res, filePath) => {
|
||||
// Set longer cache for immutable assets
|
||||
if (filePath.match(/\.(js|css|woff2?|ttf|eot|svg|png|jpg|jpeg|gif|ico)$/)) {
|
||||
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
||||
}
|
||||
// Shorter cache for HTML files
|
||||
else if (filePath.endsWith('.html')) {
|
||||
res.setHeader('Cache-Control', 'public, max-age=3600'); // 1 hour
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
logger.debug(`Serving static files from: ${publicPath}`);
|
||||
logger.debug(`Serving static files from: ${publicPath} with caching headers`);
|
||||
|
||||
// Health check endpoint (no auth required)
|
||||
app.get('/api/health', (_req, res) => {
|
||||
|
|
|
|||
|
|
@ -461,7 +461,6 @@ export class StreamWatcher {
|
|||
|
||||
try {
|
||||
client.response.write(clientData);
|
||||
// @ts-expect-error - flush exists but not in types
|
||||
if (client.response.flush) client.response.flush();
|
||||
} catch (error) {
|
||||
logger.debug(
|
||||
|
|
@ -482,7 +481,6 @@ export class StreamWatcher {
|
|||
|
||||
try {
|
||||
client.response.write(clientData);
|
||||
// @ts-expect-error - flush exists but not in types
|
||||
if (client.response.flush) client.response.flush();
|
||||
} catch (error) {
|
||||
logger.debug(
|
||||
|
|
|
|||
Loading…
Reference in a new issue