Add relay server for remote HTTPS access
Node.js/TypeScript relay server that enables remote access to MacroPad: - WebSocket-based communication between desktop and relay - Password authentication with bcrypt hashing - Session management with consistent IDs - REST API proxying to desktop app - Web client WebSocket relay - Login page and PWA-ready app page - Designed for cloud-node-container deployment 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
// Express + WebSocket Server Setup
|
||||
|
||||
import express from 'express';
|
||||
import fs from 'fs';
|
||||
import http from 'http';
|
||||
import WebSocket, { WebSocketServer } from 'ws';
|
||||
import cors from 'cors';
|
||||
import helmet from 'helmet';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import path from 'path';
|
||||
|
||||
import { config } from './config';
|
||||
import { logger } from './utils/logger';
|
||||
import { SessionManager } from './services/SessionManager';
|
||||
import { ConnectionManager } from './services/ConnectionManager';
|
||||
import { handleDesktopConnection } from './handlers/desktopHandler';
|
||||
import { handleWebClientConnection } from './handlers/webClientHandler';
|
||||
import { createApiProxy } from './handlers/apiProxy';
|
||||
|
||||
export function createServer() {
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
|
||||
// Initialize managers
|
||||
const sessionManager = new SessionManager();
|
||||
const connectionManager = new ConnectionManager();
|
||||
|
||||
// Middleware
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: false // Allow inline scripts for login page
|
||||
}));
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// Rate limiting
|
||||
const limiter = rateLimit({
|
||||
windowMs: config.rateLimitWindowMs,
|
||||
max: config.rateLimitMax,
|
||||
message: { error: 'Too many requests, please try again later' }
|
||||
});
|
||||
app.use(limiter);
|
||||
|
||||
// Static files - check both locations (dev vs production)
|
||||
const publicPath = fs.existsSync(path.join(__dirname, 'public'))
|
||||
? path.join(__dirname, 'public')
|
||||
: path.join(__dirname, '..', 'public');
|
||||
app.use('/static', express.static(publicPath));
|
||||
|
||||
// Health check
|
||||
app.get('/health', (req, res) => {
|
||||
const stats = connectionManager.getStats();
|
||||
res.json({
|
||||
status: 'ok',
|
||||
...stats
|
||||
});
|
||||
});
|
||||
|
||||
// Login page for session
|
||||
app.get('/:sessionId', (req, res) => {
|
||||
const session = sessionManager.getSession(req.params.sessionId);
|
||||
if (!session) {
|
||||
return res.status(404).send('Session not found');
|
||||
}
|
||||
res.sendFile(path.join(publicPath, 'login.html'));
|
||||
});
|
||||
|
||||
// PWA app page (after authentication)
|
||||
app.get('/:sessionId/app', (req, res) => {
|
||||
const session = sessionManager.getSession(req.params.sessionId);
|
||||
if (!session) {
|
||||
return res.status(404).send('Session not found');
|
||||
}
|
||||
res.sendFile(path.join(publicPath, 'app.html'));
|
||||
});
|
||||
|
||||
// API proxy routes
|
||||
const apiProxy = createApiProxy(connectionManager, sessionManager);
|
||||
app.all('/:sessionId/api/*', apiProxy);
|
||||
|
||||
// WebSocket server
|
||||
const wss = new WebSocketServer({ noServer: true });
|
||||
|
||||
// Handle HTTP upgrade for WebSocket
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
const url = new URL(request.url || '', `http://${request.headers.host}`);
|
||||
const pathname = url.pathname;
|
||||
|
||||
// Desktop connection: /desktop
|
||||
if (pathname === '/desktop') {
|
||||
wss.handleUpgrade(request, socket, head, (ws) => {
|
||||
handleDesktopConnection(ws, connectionManager, sessionManager);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Web client connection: /:sessionId/ws
|
||||
const webClientMatch = pathname.match(/^\/([a-zA-Z0-9]+)\/ws$/);
|
||||
if (webClientMatch) {
|
||||
const sessionId = webClientMatch[1];
|
||||
wss.handleUpgrade(request, socket, head, (ws) => {
|
||||
handleWebClientConnection(ws, sessionId, connectionManager, sessionManager);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Invalid path
|
||||
socket.destroy();
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
const shutdown = () => {
|
||||
logger.info('Shutting down...');
|
||||
connectionManager.shutdown();
|
||||
server.close(() => {
|
||||
logger.info('Server closed');
|
||||
process.exit(0);
|
||||
});
|
||||
};
|
||||
|
||||
process.on('SIGTERM', shutdown);
|
||||
process.on('SIGINT', shutdown);
|
||||
|
||||
return { app, server, sessionManager, connectionManager };
|
||||
}
|
||||
Reference in New Issue
Block a user