Compare commits

...

11 Commits
1.1.2 ... 1.2.0

Author SHA1 Message Date
Alireza Ahmadi
2c5bb94894 v1.2.0 2023-05-02 19:57:52 +02:00
Alireza Ahmadi
39c1a4276d [feature] multi-user shadowsocks 2023-05-02 19:56:41 +02:00
Alireza Ahmadi
4736786c6f [feature] ibounds manual refresh 2023-05-02 19:12:06 +02:00
Alireza Ahmadi
402c713f06 v1.1.3 2023-05-01 12:13:55 +02:00
Alireza Ahmadi
a371bec2aa [feature] bottom for reset xray config to default 2023-05-01 12:13:06 +02:00
Alireza Ahmadi
427d008bd1 [darkmode] better colors + add sec to calendar 2023-04-29 20:52:57 +02:00
Alireza Ahmadi
e69d17be67 [feature] inbounds auto refresh option 2023-04-29 19:34:51 +02:00
Alireza Ahmadi
09c61976ea [darkmode] fix UTLS option 2023-04-29 19:09:27 +02:00
Alireza Ahmadi
6e17c282e0 prettify alpn 2023-04-29 15:29:20 +02:00
Alireza Ahmadi
700973655c [feature] add sniffing DestOverride options #276 2023-04-29 15:28:15 +02:00
Alireza Ahmadi
dcb54267f2 [feature] add quic to sniffingObject 2023-04-29 14:03:00 +02:00
25 changed files with 437 additions and 153 deletions

View File

@@ -54,13 +54,18 @@ xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese)**
| `POST` | `"/del/:id"` | Delete Inbound |
| `POST` | `"/update/:id"` | Update Inbound |
| `POST` | `"/addClient/"` | Add Client to inbound |
| `POST` | `"/:id/delClient/:clientId"` | Delete Client by UID/Password as clientId |
| `POST` | `"/updateClient/:clientId"` | Update Client by UID/Password as clientId |
| `POST` | `"/:id/delClient/:clientId"` | Delete Client by clientId* |
| `POST` | `"/updateClient/:clientId"` | Update Client by clientId* |
| `POST` | `"/getClientTraffics/:email"` | Get Client's Traffic |
| `POST` | `"/resetAllTraffics"` | Reset traffics of all inbounds |
| `POST` | `"/resetAllClientTraffics/:id"` | Reset inbound clients traffics (-1: all) |
| `POST` | `"/delDepletedClients/:id"` | Delete inbound depleted clients (-1: all) |
*- The field `clientId` should be filled by:
- `client.id` for VMESS and VLESS
- `client.password` for TROJAN
- `client.email` for Shadowsocks
# Environment Variables
| Variable | Type | Default |

View File

@@ -1 +1 @@
1.1.2
1.2.0

View File

@@ -212,6 +212,7 @@
.ant-card-dark .ant-modal-close,
.ant-card-dark i,
.ant-card-dark .ant-select-dropdown-menu-item,
.ant-card-dark .ant-calendar-day-select,
.ant-card-dark .ant-calendar-month-select,
.ant-card-dark .ant-calendar-year-select,
.ant-card-dark .ant-calendar-date,
@@ -226,7 +227,7 @@
.ant-card-dark .ant-calendar-date:hover,
.ant-card-dark .ant-select-dropdown-menu-item-active,
.ant-card-dark li.ant-calendar-time-picker-select-option-selected {
background-color: #004488;
background-color: #11314d;
}
.ant-card-dark tbody .ant-table-expanded-row,
@@ -243,7 +244,7 @@
.ant-card-dark .ant-select-selection,
.ant-card-dark .ant-calendar-picker-clear {
color: hsla(0,0%,100%,.65);
background-color: #2e3b52;
background-color: #193752;
}
.ant-card-dark .ant-select-disabled .ant-select-selection {
@@ -264,12 +265,19 @@
.ant-card-dark .ant-modal-content,
.ant-card-dark .ant-modal-body,
.ant-card-dark .ant-modal-header,
.ant-card-dark .ant-calendar-selected-day .ant-calendar-date {
.ant-card-dark .ant-modal-header {
color: hsla(0,0%,100%,.65);
background-color: #222a37;
}
.ant-card-dark .ant-calendar-selected-day .ant-calendar-date {
background-color: #1668dc;
}
.ant-card-dark .ant-calendar-time-picker-select li:hover {
background: #1668dc;
}
.client-table-header {
background-color: #f0f2f5;
}

View File

@@ -21,12 +21,12 @@ const SSMethods = {
// AES_128_CFB: 'aes-128-cfb',
// CHACHA20: 'chacha20',
// CHACHA20_IETF: 'chacha20-ietf',
CHACHA20_POLY1305: 'chacha20-poly1305',
AES_256_GCM: 'aes-256-gcm',
AES_128_GCM: 'aes-128-gcm',
// CHACHA20_POLY1305: 'chacha20-poly1305',
// AES_256_GCM: 'aes-256-gcm',
// AES_128_GCM: 'aes-128-gcm',
BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',
BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',
BLAKE3_CHACHA20_POLY1305: '2022-blake3-chacha20-poly1305',
// BLAKE3_CHACHA20_POLY1305: '2022-blake3-chacha20-poly1305',
};
const TLS_FLOW_CONTROL = {
@@ -75,9 +75,15 @@ const UTLS_FINGERPRINT = {
};
const ALPN_OPTION = {
H3: "h3",
H2: "h2",
HTTP1: "http/1.1",
H2: "h2",
H3: "h3",
};
const SNIFFING_OPTION = {
HTTP: "http",
TLS: "tls",
QUIC: "quic",
};
Object.freeze(Protocols);
@@ -87,6 +93,7 @@ Object.freeze(TLS_FLOW_CONTROL);
Object.freeze(TLS_VERSION_OPTION);
Object.freeze(TLS_CIPHER_OPTION);
Object.freeze(ALPN_OPTION);
Object.freeze(SNIFFING_OPTION);
class XrayCommonClass {
@@ -741,7 +748,7 @@ class StreamSettings extends XrayCommonClass {
}
class Sniffing extends XrayCommonClass {
constructor(enabled=true, destOverride=['http', 'tls']) {
constructor(enabled=true, destOverride=['http', 'tls', 'quic']) {
super();
this.enabled = enabled;
this.destOverride = destOverride;
@@ -751,7 +758,7 @@ class Sniffing extends XrayCommonClass {
let destOverride = ObjectUtil.clone(json.destOverride);
if (!ObjectUtil.isEmpty(destOverride) && !ObjectUtil.isArrEmpty(destOverride)) {
if (ObjectUtil.isEmpty(destOverride[0])) {
destOverride = ['http', 'tls'];
destOverride = ['http', 'tls', 'quic'];
}
}
return new Sniffing(
@@ -853,66 +860,6 @@ class Inbound extends XrayCommonClass {
return this.network === "http";
}
// VMess & VLess
get uuid() {
switch (this.protocol) {
case Protocols.VMESS:
return this.settings.vmesses[0].id;
case Protocols.VLESS:
return this.settings.vlesses[0].id;
default:
return "";
}
}
// VLess & Trojan
get flow() {
switch (this.protocol) {
case Protocols.VLESS:
return this.settings.vlesses[0].flow;
case Protocols.TROJAN:
return this.settings.trojans[0].flow;
default:
return "";
}
}
// VMess
get alterId() {
switch (this.protocol) {
case Protocols.VMESS:
return this.settings.vmesses[0].alterId;
default:
return "";
}
}
// Socks & HTTP
get username() {
switch (this.protocol) {
case Protocols.SOCKS:
case Protocols.HTTP:
return this.settings.accounts[0].user;
default:
return "";
}
}
// Trojan & Shadowsocks & Socks & HTTP
get password() {
switch (this.protocol) {
case Protocols.TROJAN:
return this.settings.trojans[0].password;
case Protocols.SHADOWSOCKS:
return this.settings.password;
case Protocols.SOCKS:
case Protocols.HTTP:
return this.settings.accounts[0].pass;
default:
return "";
}
}
// Shadowsocks
get method() {
switch (this.protocol) {
@@ -986,10 +933,14 @@ class Inbound extends XrayCommonClass {
if(this.settings.vlesses[index].expiryTime > 0)
return this.settings.vlesses[index].expiryTime < new Date().getTime();
return false
case Protocols.TROJAN:
if(this.settings.trojans[index].expiryTime > 0)
return this.settings.trojans[index].expiryTime < new Date().getTime();
return false
case Protocols.TROJAN:
if(this.settings.trojans[index].expiryTime > 0)
return this.settings.trojans[index].expiryTime < new Date().getTime();
return false
case Protocols.SHADOWSOCKS:
if(this.settings.shadowsockses[index].expiryTime > 0)
return this.settings.shadowsockses[index].expiryTime < new Date().getTime();
return false
default:
return false;
}
@@ -1000,7 +951,6 @@ class Inbound extends XrayCommonClass {
case Protocols.VMESS:
case Protocols.VLESS:
case Protocols.TROJAN:
case Protocols.SHADOWSOCKS:
break;
default:
return false;
@@ -1059,7 +1009,6 @@ class Inbound extends XrayCommonClass {
case Protocols.VMESS:
case Protocols.VLESS:
case Protocols.TROJAN:
case Protocols.SHADOWSOCKS:
return true;
default:
return false;
@@ -1265,18 +1214,19 @@ class Inbound extends XrayCommonClass {
return url.toString();
}
genSSLink(address='', remark='') {
genSSLink(address='', remark='', clientIndex = 0) {
let settings = this.settings;
const server = this.stream.tls.server;
if (!ObjectUtil.isEmpty(server)) {
address = server;
}
if (settings.method == SSMethods.BLAKE3_AES_128_GCM || settings.method == SSMethods.BLAKE3_AES_256_GCM || settings.method == SSMethods.BLAKE3_CHACHA20_POLY1305) {
return `ss://${settings.method}:${settings.password}@${address}:${this.port}#${encodeURIComponent(remark)}`;
} else {
return 'ss://' + safeBase64(settings.method + ':' + settings.password + '@' + address + ':' + this.port)
+ '#' + encodeURIComponent(remark);
}
const port = this.port;
return 'ss://' + safeBase64(settings.method + ':' + settings.password + ':' +settings.shadowsockses[clientIndex].password) + '@' + address + ':' + this.port + '#' + encodeURIComponent(remark);
// if (settings.method == SSMethods.BLAKE3_AES_128_GCM || settings.method == SSMethods.BLAKE3_AES_256_GCM || settings.method == SSMethods.BLAKE3_CHACHA20_POLY1305) {
// return `ss://${settings.method}:${settings.password}@${address}:${this.port}#${encodeURIComponent(remark)}`;
// } else {
// return 'ss://' + safeBase64(settings.method + ':' + settings.password + '@' + address + ':' + this.port)
// + '#' + encodeURIComponent(remark);
// }
}
genTrojanLink(address = '', remark = '', clientIndex = 0) {
@@ -1390,7 +1340,11 @@ class Inbound extends XrayCommonClass {
remark = this.settings.vlesses[clientIndex].email
}
return this.genVLESSLink(address, remark, clientIndex);
case Protocols.SHADOWSOCKS: return this.genSSLink(address, remark);
case Protocols.SHADOWSOCKS:
if (this.settings.shadowsockses[clientIndex].email != ""){
remark = this.settings.shadowsockses[clientIndex].email
}
return this.genSSLink(address, remark, clientIndex);
case Protocols.TROJAN:
if (this.settings.trojans[clientIndex].email != ""){
remark = this.settings.trojans[clientIndex].email
@@ -1847,13 +1801,15 @@ Inbound.TrojanSettings.Fallback = class extends XrayCommonClass {
Inbound.ShadowsocksSettings = class extends Inbound.Settings {
constructor(protocol,
method=SSMethods.BLAKE3_AES_256_GCM,
password=RandomUtil.randomSeq(44),
network='tcp,udp'
password=RandomUtil.randomShadowsocksPassword(),
network='tcp,udp',
shadowsockses=[new Inbound.ShadowsocksSettings.Shadowsocks()]
) {
super(protocol);
this.method = method;
this.password = password;
this.network = network;
this.shadowsockses = shadowsockses;
}
static fromJson(json={}) {
@@ -1862,6 +1818,7 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings {
json.method,
json.password,
json.network,
json.clients.map(client => Inbound.ShadowsocksSettings.Shadowsocks.fromJson(client)),
);
}
@@ -1870,10 +1827,74 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings {
method: this.method,
password: this.password,
network: this.network,
clients: Inbound.ShadowsocksSettings.toJsonArray(this.shadowsockses)
};
}
};
Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
constructor(password=RandomUtil.randomShadowsocksPassword(), email=RandomUtil.randomText(), totalGB=0, expiryTime=0, enable=true, tgId='', subId='') {
super();
this.password = password;
this.email = email;
this.totalGB = totalGB;
this.expiryTime = expiryTime;
this.enable = enable;
this.tgId = tgId;
this.subId = subId;
}
toJson() {
return {
password: this.password,
email: this.email,
totalGB: this.totalGB,
expiryTime: this.expiryTime,
enable: this.enable,
tgId: this.tgId,
subId: this.subId,
};
}
static fromJson(json = {}) {
return new Inbound.ShadowsocksSettings.Shadowsocks(
json.password,
json.email,
json.totalGB,
json.expiryTime,
json.enable,
json.tgId,
json.subId,
);
}
get _expiryTime() {
if (this.expiryTime === 0 || this.expiryTime === "") {
return null;
}
if (this.expiryTime < 0){
return this.expiryTime / -86400000;
}
return moment(this.expiryTime);
}
set _expiryTime(t) {
if (t == null || t === "") {
this.expiryTime = 0;
} else {
this.expiryTime = t.valueOf();
}
}
get _totalGB() {
return toFixed(this.totalGB / ONE_GB, 2);
}
set _totalGB(gb) {
this.totalGB = toFixed(gb * ONE_GB, 0);
}
};
Inbound.DokodemoSettings = class extends Inbound.Settings {
constructor(protocol, address, port, network='tcp,udp', followRedirect=false) {
super(protocol);

View File

@@ -146,6 +146,12 @@ class RandomUtil {
}
return string;
}
static randomShadowsocksPassword(){
let array = new Uint8Array(32);
window.crypto.getRandomValues(array);
return btoa(String.fromCharCode.apply(null, array));
}
}
class ObjectUtil {

View File

@@ -37,6 +37,7 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
g.POST("/update", a.updateSetting)
g.POST("/updateUser", a.updateUser)
g.POST("/restartPanel", a.restartPanel)
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
}
func (a *SettingController) getAllSetting(c *gin.Context) {
@@ -118,3 +119,12 @@ func (a *SettingController) restartPanel(c *gin.Context) {
err := a.panelService.RestartPanel(time.Second * 3)
jsonMsg(c, I18n(c, "pages.setting.restartPanel"), err)
}
func (a *SettingController) getDefaultXrayConfig(c *gin.Context) {
defaultJsonConfig, err := a.settingService.GetDefaultXrayConfig()
if err != nil {
jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err)
return
}
jsonObj(c, defaultJsonConfig, nil)
}

View File

@@ -131,7 +131,7 @@
</td>
<td>
<a-form-item>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="clientsBulkModal.expiryTime" style="width: 250px;"></a-date-picker>
</a-form-item>

View File

@@ -43,7 +43,7 @@
if (this.clients[index].expiryTime < 0){
this.delayedStart = true;
}
this.oldClientId = this.dbInbound.protocol == "trojan" ? this.clients[index].password : this.clients[index].id;
this.oldClientId = this.getClientId(dbInbound.protocol,clients[index]);
} else {
this.addClient(this.inbound.protocol, this.clients);
}
@@ -55,14 +55,23 @@
case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans;
case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
default: return null;
}
},
getClientId(protocol, client) {
switch(protocol){
case Protocols.TROJAN: return client.password;
case Protocols.SHADOWSOCKS: return client.email;
default: return client.id;
}
},
addClient(protocol, clients) {
switch (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());
default: return null;
}
},

View File

@@ -28,8 +28,10 @@
</a-form-item>
</td>
</tr>
<tr v-if="inbound.protocol === Protocols.TROJAN">
<td>password</td>
<tr v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS">
<td>password
<a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS" @click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
</td>
<td>
<a-form-item>
<a-input v-model.trim="client.password" style="width: 250px"></a-input>
@@ -137,7 +139,7 @@
</td>
<td>
<a-form-item>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="client._expiryTime" style="width: 250px;"></a-date-picker>
<a-tag color="red" v-if="isExpiry">Expired</a-tag>

View File

@@ -79,7 +79,7 @@
</td>
<td>
<a-form-item>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="dbInbound._expiryTime" style="width: 250px;"></a-date-picker>
</a-form-item>

View File

@@ -1,21 +1,133 @@
{{define "form/shadowsocks"}}
<a-form layout="inline">
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.shadowsockses.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header="{{ i18n "pages.inbounds.client" }}">
<table width="100%" class="ant-table-tbody">
<tr>
<td>
<span>{{ i18n "pages.inbounds.Email" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span>
</template>
<a-icon @click="getNewEmail(client)" type="sync"> </a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>password
<a-icon @click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
</td>
<td>
<a-form-item>
<a-input v-model.trim="client.password" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>Subscription</td>
<td>
<a-form-item v-if="client.email">
<a-input v-model.trim="client.subId" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>Telegram Username</td>
<td>
<a-form-item v-if="client.email">
<a-input v-model.trim="client.tgId" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.client.delayedStart" }}</td>
<td>
<a-form-item>
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
</a-form-item>
</td>
</tr>
<tr v-if="delayedStart">
<td>{{ i18n "pages.client.expireDays" }}</td>
<td>
<a-form-item>
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
<tr v-else>
<td>
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="client._expiryTime" style="width: 200px;"></a-date-picker>
</a-form-item>
</td>
</tr>
</table>
</a-collapse-panel>
</a-collapse>
<a-collapse v-else>
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.shadowsockses.length">
<table width="100%">
<tr class="client-table-header">
<th v-for="col in Object.keys(inbound.settings.shadowsockses[0]).slice(0, 3)">[[ col ]]</th>
</tr>
<tr v-for="(client, index) in inbound.settings.shadowsockses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
</tr>
</table>
</a-collapse-panel>
</a-collapse>
<table width="100%" class="ant-table-tbody">
<tr>
<td>{{ i18n "encryption" }}</td>
<td>
<a-form-item>
<a-select v-model="inbound.settings.method" style="width: 165px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select v-model="inbound.settings.method" style="width: 250px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "password" }}</td>
<td>{{ i18n "password" }}
<a-icon @click="inbound.settings.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.settings.password"></a-input>
<a-input v-model.trim="inbound.settings.password" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>

View File

@@ -87,7 +87,7 @@
</td>
<td>
<a-form-item>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="client._expiryTime" style="width: 200px;"></a-date-picker>
</a-form-item>

View File

@@ -98,7 +98,7 @@
</td>
<td>
<a-form-item>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="client._expiryTime" style="width: 200px;"></a-date-picker>
</a-form-item>

View File

@@ -95,7 +95,7 @@
</td>
<td>
<a-form-item>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="client._expiryTime" style="width: 200px;"></a-date-picker>
</a-form-item>

View File

@@ -12,5 +12,10 @@
</span>
<a-switch v-model="inbound.sniffing.enabled"></a-switch>
</a-form-item>
<a-form-item>
<a-checkbox-group v-model="inbound.sniffing.destOverride" v-if="inbound.sniffing.enabled">
<a-checkbox v-for="key,value in SNIFFING_OPTION" :value="key">[[ value ]]</a-checkbox>
</a-checkbox-group>
</a-form-item>
</a-form>
{{end}}

View File

@@ -56,7 +56,8 @@
<td>uTLS</td>
<td>
<a-form-item>
<a-select v-model="inbound.stream.tls.settings.fingerprint" style="width: 250px">
<a-select v-model="inbound.stream.tls.settings.fingerprint"
style="width: 250px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value=''>None</a-select-option>
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
@@ -76,7 +77,7 @@
<td>
<a-form-item>
<a-checkbox-group v-model="inbound.stream.tls.alpn">
<a-checkbox v-for="key in ALPN_OPTION" :value="key">[[ key ]]</a-checkbox>
<a-checkbox v-for="key,value in ALPN_OPTION" :value="key">[[ value ]]</a-checkbox>
</a-checkbox-group>
</a-form-item>
</td>
@@ -175,7 +176,8 @@
<td>uTLS</td>
<td>
<a-form-item >
<a-select v-model="inbound.stream.reality.settings.fingerprint" style="width: 250px">
<a-select v-model="inbound.stream.reality.settings.fingerprint"
style="width: 250px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>

View File

@@ -48,6 +48,7 @@
case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans;
case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
default: return null;
}
},

View File

@@ -66,28 +66,42 @@
<transition name="list" appear>
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
<div slot="title">
<a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button>
<a-dropdown :trigger="['click']">
<a-button type="primary" icon="menu">{{ i18n "pages.inbounds.generalActions" }}</a-button>
<a-menu slot="overlay" @click="a => generalActions(a)" :theme="siderDrawer.theme">
<a-menu-item key="export">
<a-icon type="export"></a-icon>
{{ i18n "pages.inbounds.export" }}
</a-menu-item>
<a-menu-item key="resetInbounds">
<a-icon type="reload"></a-icon>
{{ i18n "pages.inbounds.resetAllTraffic" }}
</a-menu-item>
<a-menu-item key="resetClients">
<a-icon type="file-done"></a-icon>
{{ i18n "pages.inbounds.resetAllClientTraffics" }}
</a-menu-item>
<a-menu-item key="delDepletedClients">
<a-icon type="rest"></a-icon>
{{ i18n "pages.inbounds.delDepletedClients" }}
</a-menu-item>
</a-menu>
</a-dropdown>
<a-row>
<a-col :xs="24" :sm="24" :lg="12">
<a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button>
<a-dropdown :trigger="['click']">
<a-button type="primary" icon="menu">{{ i18n "pages.inbounds.generalActions" }}</a-button>
<a-menu slot="overlay" @click="a => generalActions(a)" :theme="siderDrawer.theme">
<a-menu-item key="export">
<a-icon type="export"></a-icon>
{{ i18n "pages.inbounds.export" }}
</a-menu-item>
<a-menu-item key="resetInbounds">
<a-icon type="reload"></a-icon>
{{ i18n "pages.inbounds.resetAllTraffic" }}
</a-menu-item>
<a-menu-item key="resetClients">
<a-icon type="file-done"></a-icon>
{{ i18n "pages.inbounds.resetAllClientTraffics" }}
</a-menu-item>
<a-menu-item key="delDepletedClients">
<a-icon type="rest"></a-icon>
{{ i18n "pages.inbounds.delDepletedClients" }}
</a-menu-item>
</a-menu>
</a-dropdown>
</a-col>
<a-col :xs="24" :sm="24" :lg="12" style="text-align: right;">
<a-select v-model="refreshInterval"
v-if="isRefreshEnabled"
@change="changeRefreshInterval"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option v-for="key in [5,10,30,60]" :value="key*1000">[[ key ]]s</a-select-option>
</a-select>
<a-icon type="sync" :spin="isRefreshEnabled" @click="manualRefresh"></a-icon>
<a-switch v-model="isRefreshEnabled" @change="toggleRefresh"></a-switch>
</a-col>
</a-row>
</div>
<a-input v-model.lazy="searchKey" placeholder="{{ i18n "search" }}" autofocus style="max-width: 300px"></a-input>
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
@@ -100,15 +114,11 @@
<a-dropdown :trigger="['click']">
<a-icon @click="e => e.preventDefault()" type="menu"></a-icon>
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="siderDrawer.theme">
<a-menu-item v-if="dbInbound.isSS" key="qrcode">
<a-icon type="qrcode"></a-icon>
{{ i18n "qrCode" }}
</a-menu-item>
<a-menu-item key="edit">
<a-icon type="edit"></a-icon>
{{ i18n "edit" }}
</a-menu-item>
<template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess">
<template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess || dbInbound.isSS">
<a-menu-item key="addClient">
<a-icon type="user-add"></a-icon>
{{ i18n "pages.client.add"}}
@@ -152,7 +162,7 @@
</template>
<template slot="protocol" slot-scope="text, dbInbound">
<a-tag style="margin:0;" color="blue">[[ dbInbound.protocol ]]</a-tag>
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan">
<a-tag style="margin:0;" color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="cyan">tls</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="cyan">reality</a-tag>
@@ -214,7 +224,7 @@
{{template "client_table"}}
</a-table>
<a-table
v-else-if="record.protocol === Protocols.TROJAN"
v-else-if="record.protocol === Protocols.TROJAN || record.protocol === Protocols.SHADOWSOCKS"
:row-key="client => client.id"
:columns="innerTrojanColumns"
:data-source="getInboundClients(record)"
@@ -312,25 +322,22 @@
defaultCert: '',
defaultKey: '',
clientCount: {},
isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
},
methods: {
loading(spinning=true) {
this.spinning = spinning;
},
async getDBInbounds() {
this.loading();
const msg = await HttpUtil.post('/xui/inbound/list');
this.loading(false);
if (!msg.success) {
return;
}
this.setInbounds(msg.obj);
this.searchKey = '';
},
async getDefaultSettings() {
this.loading();
const msg = await HttpUtil.post('/xui/setting/defaultSettings');
this.loading(false);
if (!msg.success) {
return;
}
@@ -342,17 +349,16 @@
setInbounds(dbInbounds) {
this.inbounds.splice(0);
this.dbInbounds.splice(0);
this.searchedInbounds.splice(0);
for (const inbound of dbInbounds) {
const dbInbound = new DBInbound(inbound);
to_inbound = dbInbound.toInbound()
this.inbounds.push(to_inbound);
this.dbInbounds.push(dbInbound);
this.searchedInbounds.push(dbInbound);
if([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN].includes(inbound.protocol) ){
this.clientCount[inbound.id] = this.getClientCounts(inbound,to_inbound);
}
}
this.searchInbounds(this.searchKey);
},
getClientCounts(dbInbound,inbound){
let clientCount = 0,active = [], deactive = [], depleted = [], expiring = [];
@@ -654,7 +660,7 @@
},
delClient(dbInboundId,client) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
clientId = dbInbound.protocol == "trojan" ? client.password : client.id;
clientId = clientId = getClientId(dbInbound.protocol,client);;
this.$confirm({
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
@@ -669,9 +675,17 @@
case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans;
case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
default: return null;
}
},
getClientId(protocol, client) {
switch(protocol){
case Protocols.TROJAN: return client.password;
case Protocols.SHADOWSOCKS: return client.email;
default: return client.id;
}
},
showQrcode(dbInbound, clientIndex) {
const link = dbInbound.genLink(clientIndex);
qrModal.show('{{ i18n "qrCode"}}', link, dbInbound);
@@ -690,7 +704,7 @@
clients = this.getClients(dbInbound.protocol, inbound.settings);
index = this.findIndexOfClient(clients, client);
clients[index].enable = !clients[index].enable;
clientId = dbInbound.protocol == "trojan" ? clients[index].password : clients[index].id;
clientId = getClientId(dbInbound.protocol,clients[index]);
await this.updateClient(clients[index],dbInboundId, clientId);
this.loading(false);
},
@@ -702,11 +716,13 @@
},
getInboundClients(dbInbound) {
if(dbInbound.protocol == Protocols.VLESS) {
return dbInbound.toInbound().settings.vlesses
return dbInbound.toInbound().settings.vlesses;
} else if(dbInbound.protocol == Protocols.VMESS) {
return dbInbound.toInbound().settings.vmesses
return dbInbound.toInbound().settings.vmesses;
} else if(dbInbound.protocol == Protocols.TROJAN) {
return dbInbound.toInbound().settings.trojans
return dbInbound.toInbound().settings.trojans;
} else if(dbInbound.protocol == Protocols.SHADOWSOCKS) {
return dbInbound.toInbound().settings.shadowsockses;
}
},
resetClientTraffic(client,dbInboundId) {
@@ -785,6 +801,32 @@
}
txtModal.show('{{ i18n "pages.inbounds.export"}}',copyText,'All-Inbounds');
},
async startDataRefreshLoop() {
while (this.isRefreshEnabled) {
try {
await this.getDBInbounds();
} catch (e) {
console.error(e);
}
await PromiseUtil.sleep(this.refreshInterval);
}
},
toggleRefresh() {
localStorage.setItem("isRefreshEnabled", this.isRefreshEnabled);
if (this.isRefreshEnabled) {
this.startDataRefreshLoop();
}
},
changeRefreshInterval(){
localStorage.setItem("refreshInterval", this.refreshInterval);
},
async manualRefresh(){
if(!this.isRefreshEnabled){
this.spinning = true;
await this.getDBInbounds();
this.spinning = false;
}
},
},
watch: {
searchKey: debounce(function (newVal) {
@@ -792,8 +834,15 @@
}, 500)
},
mounted() {
this.loading();
this.getDefaultSettings();
this.getDBInbounds();
if (this.isRefreshEnabled) {
this.startDataRefreshLoop();
}
else {
this.getDBInbounds();
}
this.loading(false);
},
computed: {
total() {

View File

@@ -113,6 +113,9 @@
</a-collapse-panel>
</a-collapse>
<a-divider>{{ i18n "pages.setting.completeTemplate"}}</a-divider>
<a-space direction="horizontal" style="padding: 0 20px">
<a-button type="primary" @click="resetXrayConfigToDefault">{{ i18n "pages.setting.resetDefaultConfig" }}</a-button>
</a-space>
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigTemplate"}}' desc='{{ i18n "pages.setting.xrayConfigTemplateDesc"}}' v-model="allSetting.xrayTemplateConfig"></setting-list-item>
</a-list>
</a-tab-pane>
@@ -201,7 +204,16 @@
await PromiseUtil.sleep(5000);
location.reload();
}
}
},
async resetXrayConfigToDefault() {
this.loading(true);
const msg = await HttpUtil.get("/xui/setting/getDefaultJsonConfig");
this.loading(false);
if (msg.success) {
this.templateSettings = JSON.parse(JSON.stringify(msg.obj, null, 2));
this.saveBtnDisable = true;
}
},
},
async mounted() {
await this.getAllSetting();

View File

@@ -318,6 +318,9 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) error
if oldInbound.Protocol == "trojan" {
client_key = "password"
}
if oldInbound.Protocol == "shadowsocks" {
client_key = "email"
}
inerfaceClients := settings["clients"].([]interface{})
var newClients []interface{}
@@ -378,6 +381,8 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
oldClientId := ""
if oldInbound.Protocol == "trojan" {
oldClientId = oldClient.Password
} else if oldInbound.Protocol == "shadowsocks" {
oldClientId = oldClient.Email
} else {
oldClientId = oldClient.ID
}

View File

@@ -2,6 +2,7 @@ package service
import (
_ "embed"
"encoding/json"
"errors"
"fmt"
"reflect"
@@ -326,3 +327,12 @@ func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
}
return common.Combine(errs...)
}
func (s *SettingService) GetDefaultXrayConfig() (interface{}, error) {
var jsonData interface{}
err := json.Unmarshal([]byte(xrayTemplateConfig), &jsonData)
if err != nil {
return nil, err
}
return jsonData, nil
}

View File

@@ -97,6 +97,8 @@ func (s *SubService) getLink(inbound *model.Inbound, email string) string {
return s.genVlessLink(inbound, email)
case "trojan":
return s.genTrojanLink(inbound, email)
case "shadowsocks":
return s.genShadowsocksLink(inbound, email)
}
return ""
}
@@ -504,6 +506,28 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
return url.String()
}
func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) string {
address := s.address
if inbound.Protocol != model.Shadowsocks {
return ""
}
clients, _ := s.inboundService.getClients(inbound)
var settings map[string]interface{}
json.Unmarshal([]byte(inbound.Settings), &settings)
inboundPassword := settings["password"].(string)
method := settings["method"].(string)
clientIndex := -1
for i, client := range clients {
if client.Email == email {
clientIndex = i
break
}
}
encPart := fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password)
return fmt.Sprintf("ss://%s@%s:%d#%s", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port, clients[clientIndex].Email)
}
func searchKey(data interface{}, key string) (interface{}, bool) {
switch val := data.(type) {
case map[string]interface{}:

View File

@@ -193,6 +193,7 @@
"save" = "Save"
"restartPanel" = "Restart Panel"
"restartPanelDesc" = "Are you sure you want to restart the panel? Click OK to restart after 3 seconds. If you cannot access the panel after restarting, please go to the server to view the panel log information"
"resetDefaultConfig" = "Reset to default config"
"panelConfig" = "Panel Configuration"
"userSetting" = "User Setting"
"xrayConfiguration" = "xray Configuration"

View File

@@ -193,6 +193,7 @@
"save" = "ذخیره"
"restartPanel" = "ریستارت پنل"
"restartPanelDesc" = "آیا مطمئن هستید که می خواهید پنل را دوباره راه اندازی کنید؟ برای راه اندازی مجدد روی OK کلیک کنید. اگر بعد از 3 ثانیه نمی توانید به پنل دسترسی پیدا کنید، لطفاً برای مشاهده اطلاعات گزارش پانل به سرور برگردید"
"resetDefaultConfig" = "برگشت به تنظیمات پیشفرض"
"panelConfig" = "تنظیمات پنل"
"userSetting" = "تنظیمات مدیر"
"xrayConfiguration" = "تنظیمات Xray"

View File

@@ -193,6 +193,7 @@
"save" = "保存配置"
"restartPanel" = "重启面板"
"restartPanelDesc" = "确定要重启面板吗?点击确定将于 3 秒后重启,若重启后无法访问面板,请前往服务器查看面板日志信息"
"resetDefaultConfig" = "重置为默认配置"
"panelConfig" = "面板配置"
"userSetting" = "用户设置"
"xrayConfiguration" = "xray 相关设置"