- 主进程新增4个IPC handler处理远程文件操作 - 前端通过IPC调用而非浏览器fetch访问远程API - Remote服务新增3003端口专门处理文件传输 - 上传使用文件路径方案,下载使用保存对话框方案
558 lines
17 KiB
JavaScript
558 lines
17 KiB
JavaScript
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;
|