mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-04-22 15:35:52 +00:00
* feat: implement real-time traffic monitoring and UI updates using a high-performance WebSocket hub and background job system * feat: add bulk client management support and improve inbound data handling * Fix bug * **Fixes & Changes:** 1. **Fixed XPadding Placement Dropdown**: - Added the missing `cookie` and `query` options to `xPaddingPlacement` (`stream_xhttp.html`). - *Why:* Previously, users wanting `cookie` obfuscation were forced to use the `header` placement string. This caused Xray-core to blindly intercept the entire monolithic HTTP Cookie header, failing internal padding-length validations and causing the inbound to silently drop the connection. 2. **Fixed Uplink Data Placement Validation**: - Replaced the unsupported `query` option with `cookie` in `uplinkDataPlacement`. - *Why:* Xray-core's `transport_internet.go` explicitly forbids `query` as an uplink placement option. Selecting it from the UI previously sent a payload that would cause Xray-core to instantly throw an `unsupported uplink data placement: query` panic. Adding `cookie` perfectly aligns the UI with Xray-core restrictions. ### Related Issues - Resolves #3992 * This commit fixes structural payload issues preventing XHTTP from functioning correctly and eliminates WebSocket log spam. - **[Fix X-Padding UI]** Added missing `cookie` and `query` options to X-Padding Placement. Fixes the issue where using Cookie fallback triggers whole HTTP Cookie header interception and silent drop in Xray-core. (Resolves [#3992](https://github.com/MHSanaei/3x-ui/issues/3992)) - **[Fix Uplink Data Options]** Replaced the invalid `query` option with `cookie` in Uplink Data Placement dropdown to prevent Xray-core backend panic `unsupported uplink data placement: query`. - **[Fix WebSockets Spam]** Boosted `maxMessageSize` boundary to 100MB and gracefully handled fallback fetch signals via `broadcastInvalidate` to avoid buffer dropping spam. (Resolves [#3984](https://github.com/MHSanaei/3x-ui/issues/3984)) * Fix * gofmt * fix(websocket): resolve channel race condition and graceful shutdown deadlock * Fix: inbounds switch * Change max quantity from 10000 to 500 * fix
172 lines
7.1 KiB
HTML
172 lines
7.1 KiB
HTML
{{define "modals/clientsModal"}}
|
|
<a-modal id="client-modal" v-model="clientModal.visible" :title="clientModal.title" @ok="clientModal.ok"
|
|
:confirm-loading="clientModal.confirmLoading" :closable="true" :mask-closable="false"
|
|
:class="themeSwitcher.currentTheme"
|
|
:ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'>
|
|
<template v-if="isEdit">
|
|
<a-tag v-if="isExpiry || isTrafficExhausted" color="red" :style="{ marginBottom: '10px', display: 'block', textAlign: 'center' }">Account is (Expired|Traffic Ended) And Disabled</a-tag>
|
|
</template>
|
|
{{template "form/client"}}
|
|
</a-modal>
|
|
<script>
|
|
|
|
const clientModal = {
|
|
visible: false,
|
|
confirmLoading: false,
|
|
title: '',
|
|
okText: '',
|
|
isEdit: false,
|
|
dbInbound: new DBInbound(),
|
|
inbound: new Inbound(),
|
|
clients: [],
|
|
clientStats: [],
|
|
oldClientId: "",
|
|
index: null,
|
|
clientIps: null,
|
|
delayedStart: false,
|
|
ok() {
|
|
if (clientModal.isEdit) {
|
|
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.oldClientId);
|
|
} else {
|
|
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id);
|
|
}
|
|
},
|
|
show({ title = '', okText = '{{ i18n "sure" }}', index = null, dbInbound = null, confirm = () => { }, isEdit = false }) {
|
|
this.visible = true;
|
|
this.title = title;
|
|
this.okText = okText;
|
|
this.isEdit = isEdit;
|
|
this.dbInbound = new DBInbound(dbInbound);
|
|
this.inbound = Inbound.fromJson(dbInbound.toInbound().toJson());
|
|
this.clients = this.inbound.clients;
|
|
this.index = index === null ? this.clients.length : index;
|
|
this.delayedStart = false;
|
|
if (isEdit) {
|
|
if (this.clients[index].expiryTime < 0) {
|
|
this.delayedStart = true;
|
|
}
|
|
this.oldClientId = this.getClientId(dbInbound.protocol, clients[index]);
|
|
} else {
|
|
this.addClient(this.inbound, this.clients);
|
|
}
|
|
this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
|
|
this.confirm = confirm;
|
|
},
|
|
getClientId(protocol, client) {
|
|
switch (protocol) {
|
|
case Protocols.TROJAN: return client.password;
|
|
case Protocols.SHADOWSOCKS: return client.email;
|
|
default: return client.id;
|
|
}
|
|
},
|
|
addClient(inbound, clients) {
|
|
switch (inbound.protocol) {
|
|
case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.VMESS());
|
|
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
|
|
case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
|
|
case Protocols.SHADOWSOCKS: return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks(clients[0].method, RandomUtil.randomShadowsocksPassword(inbound.settings.method)));
|
|
default: return null;
|
|
}
|
|
},
|
|
close() {
|
|
clientModal.visible = false;
|
|
clientModal.loading(false);
|
|
},
|
|
loading(loading=true) {
|
|
clientModal.confirmLoading = loading;
|
|
},
|
|
};
|
|
|
|
const clientModalApp = new Vue({
|
|
delimiters: ['[[', ']]'],
|
|
el: '#client-modal',
|
|
data: {
|
|
clientModal,
|
|
get inbound() {
|
|
return this.clientModal.inbound;
|
|
},
|
|
get client() {
|
|
return this.clientModal.clients[this.clientModal.index];
|
|
},
|
|
get clientStats() {
|
|
return this.clientModal.clientStats;
|
|
},
|
|
get isEdit() {
|
|
return this.clientModal.isEdit;
|
|
},
|
|
get datepicker() {
|
|
return app.datepicker;
|
|
},
|
|
get isTrafficExhausted() {
|
|
if (!this.clientStats) return false
|
|
if (this.clientStats.total <= 0) return false
|
|
if (this.clientStats.up + this.clientStats.down < this.clientStats.total) return false
|
|
return true
|
|
},
|
|
get isExpiry() {
|
|
return this.clientModal.isEdit && this.client.expiryTime >0 ? (this.client.expiryTime < new Date().getTime()) : false;
|
|
},
|
|
get delayedStart() {
|
|
return this.clientModal.delayedStart;
|
|
},
|
|
set delayedStart(value) {
|
|
this.clientModal.delayedStart = value;
|
|
},
|
|
get delayedExpireDays() {
|
|
return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
|
|
},
|
|
set delayedExpireDays(days) {
|
|
this.client.expiryTime = -86400000 * days;
|
|
},
|
|
},
|
|
methods: {
|
|
async getDBClientIps(email) {
|
|
const msg = await HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`);
|
|
if (!msg.success) {
|
|
document.getElementById("clientIPs").value = msg.obj;
|
|
return;
|
|
}
|
|
let ips = msg.obj;
|
|
if (typeof ips === 'string' && ips.startsWith('[') && ips.endsWith(']')) {
|
|
try {
|
|
ips = JSON.parse(ips);
|
|
ips = Array.isArray(ips) ? ips.join("\n") : ips;
|
|
} catch (e) {
|
|
console.error('Error parsing JSON:', e);
|
|
}
|
|
}
|
|
document.getElementById("clientIPs").value = ips;
|
|
},
|
|
async clearDBClientIps(email) {
|
|
try {
|
|
const msg = await HttpUtil.post(`/panel/api/inbounds/clearClientIps/${email}`);
|
|
if (!msg.success) {
|
|
return;
|
|
}
|
|
document.getElementById("clientIPs").value = "";
|
|
} catch (error) {
|
|
}
|
|
},
|
|
resetClientTraffic(email, dbInboundId, iconElement) {
|
|
this.$confirm({
|
|
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
|
|
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
|
|
class: themeSwitcher.currentTheme,
|
|
okText: '{{ i18n "reset"}}',
|
|
cancelText: '{{ i18n "cancel"}}',
|
|
onOk: async () => {
|
|
iconElement.disabled = true;
|
|
const msg = await HttpUtil.postWithModal('/panel/api/inbounds/' + dbInboundId + '/resetClientTraffic/' + email);
|
|
if (msg.success) {
|
|
this.clientModal.clientStats.up = 0;
|
|
this.clientModal.clientStats.down = 0;
|
|
}
|
|
iconElement.disabled = false;
|
|
},
|
|
})
|
|
},
|
|
},
|
|
});
|
|
|
|
</script>
|
|
{{end}}
|