From 313a2acbf66125feb4b145a5636351ed03e666da Mon Sep 17 00:00:00 2001 From: lolka1333 Date: Sat, 3 Jan 2026 05:26:00 +0100 Subject: feat: Add WebSocket support for real-time updates and enhance VLESS settings (#3605) * feat: add support for trusted X-Forwarded-For and testseed parameters in VLESS settings * chore: update Xray Core version to 25.12.8 in release workflow * chore: update Xray Core version to 25.12.8 in Docker initialization script * chore: bump version to 2.8.6 and add watcher for security changes in inbound modal * refactor: remove default and random seed buttons from outbound form * refactor: update VLESS form to rename 'Test Seed' to 'Vision Seed' and change button functionality for seed generation * refactor: enhance TLS settings form layout with improved button styling and spacing * feat: integrate WebSocket support for real-time updates on inbounds and Xray service status * chore: downgrade version to 2.8.5 * refactor: translate comments to English * fix: ensure testseed is initialized correctly for VLESS protocol and improve client handling in inbound modal * refactor: simplify VLESS divider condition by removing unnecessary flow checks * fix: add fallback date formatting for cases when IntlUtil is not available * refactor: simplify WebSocket message handling by removing batching and ensuring individual message delivery * refactor: disable WebSocket notifications in inbound and index HTML files * refactor: enhance VLESS testseed initialization and button functionality in inbound modal * fix: * refactor: ensure proper WebSocket URL construction by normalizing basePath * fix: * fix: * fix: * refactor: update testseed methods for improved reactivity and binding in VLESS form * logger info to debug --------- Co-authored-by: lolka1333 --- web/assets/js/websocket.js | 145 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 web/assets/js/websocket.js (limited to 'web/assets/js/websocket.js') diff --git a/web/assets/js/websocket.js b/web/assets/js/websocket.js new file mode 100644 index 00000000..5b8a3948 --- /dev/null +++ b/web/assets/js/websocket.js @@ -0,0 +1,145 @@ +/** + * WebSocket client for real-time updates + */ +class WebSocketClient { + constructor(basePath = '') { + this.basePath = basePath; + this.ws = null; + this.reconnectAttempts = 0; + this.maxReconnectAttempts = 10; + this.reconnectDelay = 1000; + this.listeners = new Map(); + this.isConnected = false; + this.shouldReconnect = true; + } + + connect() { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + return; + } + + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + // Ensure basePath ends with '/' for proper URL construction + let basePath = this.basePath || ''; + if (basePath && !basePath.endsWith('/')) { + basePath += '/'; + } + const wsUrl = `${protocol}//${window.location.host}${basePath}ws`; + + console.log('WebSocket connecting to:', wsUrl, 'basePath:', this.basePath); + + try { + this.ws = new WebSocket(wsUrl); + + this.ws.onopen = () => { + console.log('WebSocket connected'); + this.isConnected = true; + this.reconnectAttempts = 0; + this.emit('connected'); + }; + + this.ws.onmessage = (event) => { + try { + // Validate message size (prevent memory issues) + const maxMessageSize = 10 * 1024 * 1024; // 10MB + if (event.data && event.data.length > maxMessageSize) { + console.error('WebSocket message too large:', event.data.length, 'bytes'); + this.ws.close(); + return; + } + + const message = JSON.parse(event.data); + if (!message || typeof message !== 'object') { + console.error('Invalid WebSocket message format'); + return; + } + + this.handleMessage(message); + } catch (e) { + console.error('Failed to parse WebSocket message:', e); + } + }; + + this.ws.onerror = (error) => { + console.error('WebSocket error:', error); + this.emit('error', error); + }; + + this.ws.onclose = () => { + console.log('WebSocket disconnected'); + this.isConnected = false; + this.emit('disconnected'); + + if (this.shouldReconnect && this.reconnectAttempts < this.maxReconnectAttempts) { + this.reconnectAttempts++; + const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1); + console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`); + setTimeout(() => this.connect(), delay); + } + }; + } catch (e) { + console.error('Failed to create WebSocket connection:', e); + this.emit('error', e); + } + } + + handleMessage(message) { + const { type, payload, time } = message; + + // Emit to specific type listeners + this.emit(type, payload, time); + + // Emit to all listeners + this.emit('message', { type, payload, time }); + } + + on(event, callback) { + if (!this.listeners.has(event)) { + this.listeners.set(event, []); + } + this.listeners.get(event).push(callback); + } + + off(event, callback) { + if (!this.listeners.has(event)) { + return; + } + const callbacks = this.listeners.get(event); + const index = callbacks.indexOf(callback); + if (index > -1) { + callbacks.splice(index, 1); + } + } + + emit(event, ...args) { + if (this.listeners.has(event)) { + this.listeners.get(event).forEach(callback => { + try { + callback(...args); + } catch (e) { + console.error('Error in WebSocket event handler:', e); + } + }); + } + } + + disconnect() { + this.shouldReconnect = false; + if (this.ws) { + this.ws.close(); + this.ws = null; + } + } + + send(data) { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify(data)); + } else { + console.warn('WebSocket is not connected'); + } + } +} + +// Create global WebSocket client instance +// Safely get basePath from global scope (defined in page.html) +window.wsClient = new WebSocketClient(typeof basePath !== 'undefined' ? basePath : ''); -- cgit v1.2.3