const Container = require('./Container'); const EventBus = require('./EventBus'); const EventTypes = require('./events'); const { ErrorHandler } = require('./ErrorHandler'); const logger = require('../utils/logger'); const MessageTypes = require('../server/messageTypes'); const SHUTDOWN_TIMEOUT = 30000; class App { constructor() { this.container = null; this.eventBus = null; this.started = false; this.shuttingDown = false; this.lastClipboardText = ''; } async bootstrap() { this.container = new Container(); this.eventBus = new EventBus(); this.container.register('eventBus', () => this.eventBus); this._registerConfig(); this._registerLogger(); this._registerErrorHandler(); this._registerAuthServices(); this._registerStreamServices(); this._registerInputServices(); this._registerClipboardServices(); this._registerFileServices(); this._registerNetworkServices(); this._registerServer(); logger.info('Application bootstrap completed'); return this; } _registerConfig() { this.container.register('config', (c) => { return require('../config'); }); } _registerLogger() { this.container.register('logger', (c) => { return require('../utils/logger'); }); } _registerEventBus() { this.container.register('eventBus', (c) => { return this.eventBus; }); } _registerErrorHandler() { this.container.register('errorHandler', (c) => { return new ErrorHandler(); }); } _registerAuthServices() { this.container.register('authService', (c) => { const AuthService = require('../services/auth/AuthService'); return new AuthService(); }); this.container.register('tokenManager', (c) => { const TokenManager = require('../services/auth/TokenManager'); return new TokenManager(); }); } _registerStreamServices() { this.container.register('ffmpegEncoder', (c) => { const FFmpegEncoder = require('../services/stream/FFmpegEncoder'); return new FFmpegEncoder(); }); } _registerInputServices() { this.container.register('inputService', (c) => { const PowerShellInput = require('../services/input/PowerShellInput'); return new PowerShellInput(); }); this.container.register('inputHandler', (c) => { const InputHandler = require('../server/InputHandler'); const inputService = c.resolve('inputService'); return new InputHandler(inputService); }); } _registerClipboardServices() { this.container.register('clipboardService', (c) => { const { clipboardService } = require('../services/clipboard'); return clipboardService; }); } _registerFileServices() { this.container.register('fileService', (c) => { const { fileService } = require('../services/file'); return fileService; }); } _registerNetworkServices() { this.container.register('frpService', (c) => { const FRPService = require('../services/network/FRPService'); const config = c.resolve('config'); const frpConfig = config.getSection('frp') || {}; return new FRPService({ enabled: frpConfig.enabled !== false }); }); this.container.register('openCodeService', (c) => { const OpenCodeService = require('../services/opencode/OpenCodeService'); const config = c.resolve('config'); const opencodeConfig = config.getSection('opencode') || {}; return new OpenCodeService({ enabled: opencodeConfig.enabled !== false, port: opencodeConfig.port || 3002 }); }); this.container.register('giteaService', (c) => { const GiteaService = require('../services/network/GiteaService'); const config = c.resolve('config'); const giteaConfig = config.getSection('gitea') || {}; return new GiteaService({ enabled: giteaConfig.enabled !== false }); }); } _registerServer() { this.container.register('httpServer', (c) => { const Server = require('../server/Server'); const config = c.resolve('config'); const serverConfig = config.getSection('server') || {}; return new Server({ port: serverConfig.port || 3000, fileTransferPort: serverConfig.fileTransferPort || 3003, host: serverConfig.host || '0.0.0.0' }); }); this.container.register('wsServer', (c) => { const WebSocketServer = require('../server/WebSocketServer'); return new WebSocketServer(); }); this.container.register('streamBroadcaster', (c) => { const StreamBroadcaster = require('../server/StreamBroadcaster'); const wsServer = c.resolve('wsServer'); return new StreamBroadcaster(wsServer); }); } async start() { if (this.started) { logger.warn('Application already started'); return; } const errorHandler = this.container.resolve('errorHandler'); errorHandler.initialize(); this._setupRoutes(); const httpServer = this.container.resolve('httpServer'); const address = await httpServer.start(); logger.info('HTTP server started', { address }); const wsServer = this.container.resolve('wsServer'); wsServer.start(httpServer.getHTTPServer()); logger.info('WebSocket server started'); this._setupWebSocketHandlers(); const ffmpegEncoder = this.container.resolve('ffmpegEncoder'); ffmpegEncoder.start(); logger.info('FFmpeg encoder started'); const screenRes = ffmpegEncoder.getScreenResolution(); wsServer.setScreenResolution(screenRes.width, screenRes.height); const streamBroadcaster = this.container.resolve('streamBroadcaster'); streamBroadcaster.setEncoder(ffmpegEncoder); logger.info('Stream broadcaster attached to encoder'); const inputService = this.container.resolve('inputService'); await inputService.start(); logger.info('Input service started'); const frpService = this.container.resolve('frpService'); frpService.start(); logger.info('FRP service started'); const openCodeService = this.container.resolve('openCodeService'); openCodeService.start(); logger.info('OpenCode service started'); const giteaService = this.container.resolve('giteaService'); giteaService.start(); logger.info('Gitea service started'); // 启动剪贴板监控,主动同步到主控 this._startClipboardWatcher(); this.started = true; await this.eventBus.emit(EventTypes.APP_START, { timestamp: new Date().toISOString(), address }); logger.info('Application started successfully'); this._setupGracefulShutdown(); } _setupRoutes() { const httpServer = this.container.resolve('httpServer'); const config = this.container.resolve('config'); const authService = this.container.resolve('authService'); const tokenManager = this.container.resolve('tokenManager'); const inputHandler = this.container.resolve('inputHandler'); const paths = require('../utils/paths'); const express = require('express'); const cookieParser = require('cookie-parser'); const authMiddleware = require('../middlewares/auth'); const routes = require('../routes'); // 简单的 CORS 中间件 httpServer.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); if (req.method === 'OPTIONS') { return res.sendStatus(200); } next(); }); httpServer.use(cookieParser()); httpServer.use(express.json()); httpServer.use(express.urlencoded({ extended: true })); httpServer.app.post('/login', async (req, res) => { const { password } = req.body; const isValid = await authService.authenticate(password); if (isValid) { const token = tokenManager.generateToken({ userId: 'default-user' }); res.cookie('auth', token, { httpOnly: true, maxAge: 24 * 60 * 60 * 1000 }); return res.redirect('/'); } httpServer.renderLoginPage(res, '密码错误'); }); httpServer.use(async (req, res, next) => { // 放行 CORS 预检请求 if (req.method === 'OPTIONS') { return next(); } if (!authService.hasPassword()) { res.locals.authenticated = true; return next(); } const token = req.cookies && req.cookies.auth; if (token) { const decoded = tokenManager.verifyToken(token); if (decoded) { logger.info('HTTP auth: token valid from cookie', { path: req.path, ip: req.socket?.remoteAddress }); res.locals.authenticated = true; return next(); } else { logger.info('HTTP auth: token invalid from cookie', { path: req.path, ip: req.socket?.remoteAddress }); } } // 检查 URL 参数中的 password const urlPassword = req.query.password; if (urlPassword) { logger.info('HTTP auth: checking password from URL', { path: req.path, ip: req.socket?.remoteAddress, passwordLength: urlPassword.length }); const isValid = await authService.authenticate(urlPassword); if (isValid) { const newToken = tokenManager.generateToken({ userId: 'default-user' }); res.cookie('auth', newToken, { httpOnly: true, maxAge: 24 * 60 * 60 * 1000 }); logger.info('HTTP auth: password valid, token generated', { path: req.path, ip: req.socket?.remoteAddress }); res.locals.authenticated = true; return next(); } else { logger.warn('HTTP auth: password invalid', { path: req.path, ip: req.socket?.remoteAddress }); } } if (req.path === '/' || req.path === '/index.html') { return httpServer.renderLoginPage(res); } res.status(401).json({ error: 'Authentication required' }); }); httpServer.static(paths.getPublicPath()); httpServer.use('/api', authMiddleware); httpServer.use('/api', routes); httpServer.app.get('/api/config', (req, res) => { try { res.json(config.getAll()); } catch (error) { logger.error('Error getting config', { error: error.message }); res.status(500).json({ error: 'Failed to get config' }); } }); const wsServer = this.container.resolve('wsServer'); const originalSetup = wsServer.setupConnectionHandler.bind(wsServer); wsServer.setupConnectionHandler = function() { originalSetup(); const originalHandlers = this.wss.listeners('connection'); this.wss.removeAllListeners('connection'); this.wss.on('connection', async (ws, req) => { const url = new URL(req.url, `http://${req.headers.host}`); const fullUrl = req.url; let isAuthenticated = false; let authMethod = ''; // 检查 URL 参数中的 password const urlPassword = url.searchParams.get('password'); if (urlPassword) { logger.info('WebSocket auth: checking password from URL', { ip: req.socket?.remoteAddress, urlLength: fullUrl.length, passwordLength: urlPassword.length, passwordPrefix: urlPassword.substring(0, 2) + '***' }); const isValid = await authService.authenticate(urlPassword); if (isValid) { isAuthenticated = true; authMethod = 'password_url'; logger.info('WebSocket auth: password from URL valid', { ip: req.socket?.remoteAddress }); } else { logger.warn('WebSocket auth: password from URL invalid', { ip: req.socket?.remoteAddress }); } } // 检查 Cookie 中的 token if (!isAuthenticated && req.cookies && req.cookies.auth) { logger.info('WebSocket auth: checking token from cookie', { ip: req.socket?.remoteAddress }); const decoded = tokenManager.verifyToken(req.cookies.auth); if (decoded) { isAuthenticated = true; authMethod = 'cookie_token'; logger.info('WebSocket auth: token from cookie valid', { ip: req.socket?.remoteAddress }); } else { logger.warn('WebSocket auth: token from cookie invalid', { ip: req.socket?.remoteAddress }); } } // 未认证,拒绝连接 if (!isAuthenticated) { logger.warn('WebSocket authentication failed', { ip: req.socket?.remoteAddress, hasPassword: !!urlPassword, hasCookie: !!(req.cookies && req.cookies.auth), fullUrl: fullUrl.substring(0, 200) }); ws.close(1008, 'Authentication required'); return; } logger.debug('WebSocket authenticated', { ip: req.socket?.remoteAddress, authMethod }); // 保存认证状态 ws.isAuthenticated = true; // 处理输入消息 ws.on('message', (data) => { try { const message = JSON.parse(data); inputHandler.handleMessage(message, ws); } catch (error) { logger.debug('Failed to parse WebSocket message', { error: error.message }); } }); // 调用原始 handlers originalHandlers.forEach(handler => { handler(ws, req); }); }); }; } _setupWebSocketHandlers() { // WebSocket handlers set up in _ aresetupRoutes() // to avoid duplicate message handling } _startClipboardWatcher() { const clipboardService = this.container.resolve('clipboardService'); const wsServer = this.container.resolve('wsServer'); clipboardService.read().then(content => { if (content.type === 'text') { this.lastClipboardText = content.data || ''; } }).catch(() => {}); setInterval(async () => { try { const content = await clipboardService.read(); if (content.type === 'text' && content.data !== this.lastClipboardText) { this.lastClipboardText = content.data || ''; const message = JSON.stringify({ type: MessageTypes.CLIPBOARD_DATA, contentType: content.type, data: content.data, size: content.size }); wsServer.clients.forEach((client) => { if (client.readyState === 1) { // WebSocket.OPEN client.send(message); } }); logger.info('Clipboard changed, synced to client'); } } catch (error) { // ignore } }, 1000); } _setupGracefulShutdown() { const shutdown = async (signal) => { if (this.shuttingDown) { return; } this.shuttingDown = true; logger.info(`Received ${signal}, starting graceful shutdown`); await this.stop(); process.exit(0); }; process.on('SIGINT', () => shutdown('SIGINT')); process.on('SIGTERM', () => shutdown('SIGTERM')); process.on('exit', () => { logger.info('Process exiting'); }); } async stop() { if (!this.started) { return; } logger.info('Stopping application'); const shutdownPromise = async () => { const frpService = this.container.resolve('frpService'); frpService.stop(); logger.info('FRP service stopped'); const openCodeService = this.container.resolve('openCodeService'); openCodeService.stop(); logger.info('OpenCode service stopped'); const giteaService = this.container.resolve('giteaService'); giteaService.stop(); logger.info('Gitea service stopped'); const streamBroadcaster = this.container.resolve('streamBroadcaster'); streamBroadcaster.stop(); logger.info('Stream broadcaster stopped'); const ffmpegEncoder = this.container.resolve('ffmpegEncoder'); ffmpegEncoder.stop(); logger.info('FFmpeg encoder stopped'); const inputHandler = this.container.resolve('inputHandler'); inputHandler.stop(); logger.info('Input handler stopped'); const inputService = this.container.resolve('inputService'); await inputService.stop(); logger.info('Input service stopped'); const wsServer = this.container.resolve('wsServer'); wsServer.stop(); logger.info('WebSocket server stopped'); const httpServer = this.container.resolve('httpServer'); await httpServer.stop(); logger.info('HTTP server stopped'); await this.eventBus.emit(EventTypes.APP_STOP, { timestamp: new Date().toISOString() }); this.started = false; logger.info('Application stopped successfully'); }; const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new Error('Shutdown timeout exceeded')); }, SHUTDOWN_TIMEOUT); }); try { await Promise.race([shutdownPromise(), timeoutPromise]); } catch (error) { logger.error('Error during shutdown', { error: error.message }); this.started = false; } } getService(name) { if (!this.container) { throw new Error('Application not bootstrapped'); } return this.container.resolve(name); } getEventBus() { return this.eventBus; } isStarted() { return this.started; } } module.exports = App;