mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-03-22 18:45:49 +00:00
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 <test123@gmail.com>
This commit is contained in:
@@ -857,6 +857,7 @@ class SockoptStreamSettings extends XrayCommonClass {
|
||||
V6Only = false,
|
||||
tcpWindowClamp = 600,
|
||||
interfaceName = "",
|
||||
trustedXForwardedFor = [],
|
||||
) {
|
||||
super();
|
||||
this.acceptProxyProtocol = acceptProxyProtocol;
|
||||
@@ -875,6 +876,7 @@ class SockoptStreamSettings extends XrayCommonClass {
|
||||
this.V6Only = V6Only;
|
||||
this.tcpWindowClamp = tcpWindowClamp;
|
||||
this.interfaceName = interfaceName;
|
||||
this.trustedXForwardedFor = trustedXForwardedFor;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
@@ -896,11 +898,12 @@ class SockoptStreamSettings extends XrayCommonClass {
|
||||
json.V6Only,
|
||||
json.tcpWindowClamp,
|
||||
json.interface,
|
||||
json.trustedXForwardedFor || [],
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
const result = {
|
||||
acceptProxyProtocol: this.acceptProxyProtocol,
|
||||
tcpFastOpen: this.tcpFastOpen,
|
||||
mark: this.mark,
|
||||
@@ -918,6 +921,10 @@ class SockoptStreamSettings extends XrayCommonClass {
|
||||
tcpWindowClamp: this.tcpWindowClamp,
|
||||
interface: this.interfaceName,
|
||||
};
|
||||
if (this.trustedXForwardedFor && this.trustedXForwardedFor.length > 0) {
|
||||
result.trustedXForwardedFor = this.trustedXForwardedFor;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1870,6 +1877,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
|
||||
encryption = "none",
|
||||
fallbacks = [],
|
||||
selectedAuth = undefined,
|
||||
testseed = [900, 500, 900, 256],
|
||||
) {
|
||||
super(protocol);
|
||||
this.vlesses = vlesses;
|
||||
@@ -1877,6 +1885,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
|
||||
this.encryption = encryption;
|
||||
this.fallbacks = fallbacks;
|
||||
this.selectedAuth = selectedAuth;
|
||||
this.testseed = testseed;
|
||||
}
|
||||
|
||||
addFallback() {
|
||||
@@ -1888,13 +1897,20 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
// Ensure testseed is always initialized as an array
|
||||
let testseed = [900, 500, 900, 256];
|
||||
if (json.testseed && Array.isArray(json.testseed) && json.testseed.length >= 4) {
|
||||
testseed = json.testseed;
|
||||
}
|
||||
|
||||
const obj = new Inbound.VLESSSettings(
|
||||
Protocols.VLESS,
|
||||
(json.clients || []).map(client => Inbound.VLESSSettings.VLESS.fromJson(client)),
|
||||
json.decryption,
|
||||
json.encryption,
|
||||
Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks || []),
|
||||
json.selectedAuth
|
||||
json.selectedAuth,
|
||||
testseed
|
||||
);
|
||||
return obj;
|
||||
}
|
||||
@@ -1920,6 +1936,10 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
|
||||
json.selectedAuth = this.selectedAuth;
|
||||
}
|
||||
|
||||
if (this.testseed && this.testseed.length >= 4) {
|
||||
json.testseed = this.testseed;
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
|
||||
@@ -432,6 +432,7 @@ class SockoptStreamSettings extends CommonClass {
|
||||
tcpMptcp = false,
|
||||
penetrate = false,
|
||||
addressPortStrategy = Address_Port_Strategy.NONE,
|
||||
trustedXForwardedFor = [],
|
||||
) {
|
||||
super();
|
||||
this.dialerProxy = dialerProxy;
|
||||
@@ -440,6 +441,7 @@ class SockoptStreamSettings extends CommonClass {
|
||||
this.tcpMptcp = tcpMptcp;
|
||||
this.penetrate = penetrate;
|
||||
this.addressPortStrategy = addressPortStrategy;
|
||||
this.trustedXForwardedFor = trustedXForwardedFor;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
@@ -450,12 +452,13 @@ class SockoptStreamSettings extends CommonClass {
|
||||
json.tcpKeepAliveInterval,
|
||||
json.tcpMptcp,
|
||||
json.penetrate,
|
||||
json.addressPortStrategy
|
||||
json.addressPortStrategy,
|
||||
json.trustedXForwardedFor || []
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
const result = {
|
||||
dialerProxy: this.dialerProxy,
|
||||
tcpFastOpen: this.tcpFastOpen,
|
||||
tcpKeepAliveInterval: this.tcpKeepAliveInterval,
|
||||
@@ -463,6 +466,10 @@ class SockoptStreamSettings extends CommonClass {
|
||||
penetrate: this.penetrate,
|
||||
addressPortStrategy: this.addressPortStrategy
|
||||
};
|
||||
if (this.trustedXForwardedFor && this.trustedXForwardedFor.length > 0) {
|
||||
result.trustedXForwardedFor = this.trustedXForwardedFor;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1050,13 +1057,15 @@ Outbound.VmessSettings = class extends CommonClass {
|
||||
}
|
||||
};
|
||||
Outbound.VLESSSettings = class extends CommonClass {
|
||||
constructor(address, port, id, flow, encryption) {
|
||||
constructor(address, port, id, flow, encryption, testpre = 0, testseed = [900, 500, 900, 256]) {
|
||||
super();
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
this.id = id;
|
||||
this.flow = flow;
|
||||
this.encryption = encryption;
|
||||
this.testpre = testpre;
|
||||
this.testseed = testseed;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
@@ -1066,18 +1075,27 @@ Outbound.VLESSSettings = class extends CommonClass {
|
||||
json.port,
|
||||
json.id,
|
||||
json.flow,
|
||||
json.encryption
|
||||
json.encryption,
|
||||
json.testpre || 0,
|
||||
json.testseed && json.testseed.length >= 4 ? json.testseed : [900, 500, 900, 256]
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
const result = {
|
||||
address: this.address,
|
||||
port: this.port,
|
||||
id: this.id,
|
||||
flow: this.flow,
|
||||
encryption: this.encryption,
|
||||
};
|
||||
if (this.testpre > 0) {
|
||||
result.testpre = this.testpre;
|
||||
}
|
||||
if (this.testseed && this.testseed.length >= 4) {
|
||||
result.testseed = this.testseed;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
Outbound.TrojanSettings = class extends CommonClass {
|
||||
|
||||
145
web/assets/js/websocket.js
Normal file
145
web/assets/js/websocket.js
Normal file
@@ -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 : '');
|
||||
Reference in New Issue
Block a user