mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-05-13 11:48:06 +00:00
Merge branch 'MHSanaei:main' into main
This commit is contained in:
commit
f038b681e8
18 changed files with 131 additions and 193 deletions
|
@ -177,6 +177,8 @@ Reference syntax:
|
||||||
| `POST` | `"/resetAllClientTraffics/:id"` | Reset traffics of all clients in an inbound |
|
| `POST` | `"/resetAllClientTraffics/:id"` | Reset traffics of all clients in an inbound |
|
||||||
| `POST` | `"/delDepletedClients/:id"` | Delete inbound depleted clients (-1: all) |
|
| `POST` | `"/delDepletedClients/:id"` | Delete inbound depleted clients (-1: all) |
|
||||||
|
|
||||||
|
- [Postman Collection](https://gist.github.com/mehdikhody/9a862801a2e41f6b5fb6bbc7e1326044)
|
||||||
|
|
||||||
# A Special Thanks To
|
# A Special Thanks To
|
||||||
|
|
||||||
- [alireza0](https://github.com/alireza0/)
|
- [alireza0](https://github.com/alireza0/)
|
||||||
|
@ -190,11 +192,8 @@ Reference syntax:
|
||||||
|
|
||||||
# Buy Me a Coffee
|
# Buy Me a Coffee
|
||||||
|
|
||||||
[](#)
|
- Tron USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||||
|
|
||||||
```
|
|
||||||
TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC
|
|
||||||
```
|
|
||||||
|
|
||||||
# Pictures
|
# Pictures
|
||||||
|
|
||||||
|
|
|
@ -73,7 +73,7 @@ config_after_install() {
|
||||||
/usr/local/x-ui/x-ui migrate
|
/usr/local/x-ui/x-ui migrate
|
||||||
echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}"
|
echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}"
|
||||||
read -p "Do you want to continue with the modification [y/n]? ": config_confirm
|
read -p "Do you want to continue with the modification [y/n]? ": config_confirm
|
||||||
if [[ x"${config_confirm}" == x"y" || x"${config_confirm}" == x"Y" ]]; then
|
if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then
|
||||||
read -p "Please set up your username:" config_account
|
read -p "Please set up your username:" config_account
|
||||||
echo -e "${yellow}Your username will be:${config_account}${plain}"
|
echo -e "${yellow}Your username will be:${config_account}${plain}"
|
||||||
read -p "Please set up your password:" config_password
|
read -p "Please set up your password:" config_password
|
||||||
|
|
|
@ -212,6 +212,7 @@
|
||||||
.ant-card-dark .ant-modal-close,
|
.ant-card-dark .ant-modal-close,
|
||||||
.ant-card-dark i,
|
.ant-card-dark i,
|
||||||
.ant-card-dark .ant-select-dropdown-menu-item,
|
.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-month-select,
|
||||||
.ant-card-dark .ant-calendar-year-select,
|
.ant-card-dark .ant-calendar-year-select,
|
||||||
.ant-card-dark .ant-calendar-date,
|
.ant-card-dark .ant-calendar-date,
|
||||||
|
@ -226,7 +227,7 @@
|
||||||
.ant-card-dark .ant-calendar-date:hover,
|
.ant-card-dark .ant-calendar-date:hover,
|
||||||
.ant-card-dark .ant-select-dropdown-menu-item-active,
|
.ant-card-dark .ant-select-dropdown-menu-item-active,
|
||||||
.ant-card-dark li.ant-calendar-time-picker-select-option-selected {
|
.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,
|
.ant-card-dark tbody .ant-table-expanded-row,
|
||||||
|
@ -243,7 +244,7 @@
|
||||||
.ant-card-dark .ant-select-selection,
|
.ant-card-dark .ant-select-selection,
|
||||||
.ant-card-dark .ant-calendar-picker-clear {
|
.ant-card-dark .ant-calendar-picker-clear {
|
||||||
color: hsla(0,0%,100%,.65);
|
color: hsla(0,0%,100%,.65);
|
||||||
background-color: #2e3b52;
|
background-color: #193752;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-card-dark .ant-select-disabled .ant-select-selection {
|
.ant-card-dark .ant-select-disabled .ant-select-selection {
|
||||||
|
@ -264,12 +265,19 @@
|
||||||
|
|
||||||
.ant-card-dark .ant-modal-content,
|
.ant-card-dark .ant-modal-content,
|
||||||
.ant-card-dark .ant-modal-body,
|
.ant-card-dark .ant-modal-body,
|
||||||
.ant-card-dark .ant-modal-header,
|
.ant-card-dark .ant-modal-header {
|
||||||
.ant-card-dark .ant-calendar-selected-day .ant-calendar-date {
|
|
||||||
color: hsla(0,0%,100%,.65);
|
color: hsla(0,0%,100%,.65);
|
||||||
background-color: #222a37;
|
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 {
|
.client-table-header {
|
||||||
background-color: #f0f2f5;
|
background-color: #f0f2f5;
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,9 +79,15 @@ const UTLS_FINGERPRINT = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const ALPN_OPTION = {
|
const ALPN_OPTION = {
|
||||||
H3: "h3",
|
|
||||||
H2: "h2",
|
|
||||||
HTTP1: "http/1.1",
|
HTTP1: "http/1.1",
|
||||||
|
H2: "h2",
|
||||||
|
H3: "h3",
|
||||||
|
};
|
||||||
|
|
||||||
|
const SNIFFING_OPTION = {
|
||||||
|
HTTP: "http",
|
||||||
|
TLS: "tls",
|
||||||
|
QUIC: "quic",
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.freeze(Protocols);
|
Object.freeze(Protocols);
|
||||||
|
@ -92,6 +98,7 @@ Object.freeze(TLS_FLOW_CONTROL);
|
||||||
Object.freeze(TLS_VERSION_OPTION);
|
Object.freeze(TLS_VERSION_OPTION);
|
||||||
Object.freeze(TLS_CIPHER_OPTION);
|
Object.freeze(TLS_CIPHER_OPTION);
|
||||||
Object.freeze(ALPN_OPTION);
|
Object.freeze(ALPN_OPTION);
|
||||||
|
Object.freeze(SNIFFING_OPTION);
|
||||||
|
|
||||||
class XrayCommonClass {
|
class XrayCommonClass {
|
||||||
|
|
||||||
|
@ -880,7 +887,7 @@ class StreamSettings extends XrayCommonClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
class Sniffing extends XrayCommonClass {
|
class Sniffing extends XrayCommonClass {
|
||||||
constructor(enabled=true, destOverride=['http', 'tls']) {
|
constructor(enabled=true, destOverride=['http', 'tls', 'quic']) {
|
||||||
super();
|
super();
|
||||||
this.enabled = enabled;
|
this.enabled = enabled;
|
||||||
this.destOverride = destOverride;
|
this.destOverride = destOverride;
|
||||||
|
@ -890,7 +897,7 @@ class Sniffing extends XrayCommonClass {
|
||||||
let destOverride = ObjectUtil.clone(json.destOverride);
|
let destOverride = ObjectUtil.clone(json.destOverride);
|
||||||
if (!ObjectUtil.isEmpty(destOverride) && !ObjectUtil.isArrEmpty(destOverride)) {
|
if (!ObjectUtil.isEmpty(destOverride) && !ObjectUtil.isArrEmpty(destOverride)) {
|
||||||
if (ObjectUtil.isEmpty(destOverride[0])) {
|
if (ObjectUtil.isEmpty(destOverride[0])) {
|
||||||
destOverride = ['http', 'tls'];
|
destOverride = ['http', 'tls', 'quic'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new Sniffing(
|
return new Sniffing(
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"x-ui/web/session"
|
"x-ui/web/session"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BaseController struct {
|
type BaseController struct {
|
||||||
|
|
|
@ -7,7 +7,8 @@
|
||||||
<link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue@1.7.2/antd.min.css">
|
<link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue@1.7.2/antd.min.css">
|
||||||
<link rel="stylesheet" href="{{ .base_path }}assets/element-ui@2.15.0/theme-chalk/display.css">
|
<link rel="stylesheet" href="{{ .base_path }}assets/element-ui@2.15.0/theme-chalk/display.css">
|
||||||
<link rel="stylesheet" href="{{ .base_path }}assets/css/custom.css?{{ .cur_ver }}">
|
<link rel="stylesheet" href="{{ .base_path }}assets/css/custom.css?{{ .cur_ver }}">
|
||||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
|
<link rel=”icon” type=”image/x-icon” href="{{ .base_path }}assets/favicon.ico">
|
||||||
|
<link rel="shortcut icon" type="image/x-icon" href="{{ .base_path }}assets/favicon.ico">
|
||||||
<style>
|
<style>
|
||||||
[v-cloak] {
|
[v-cloak] {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
@ -91,7 +91,7 @@
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<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' : ''"
|
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
|
||||||
v-model="clientsBulkModal.expiryTime" style="width: 300px;"></a-date-picker>
|
v-model="clientsBulkModal.expiryTime" style="width: 300px;"></a-date-picker>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
|
@ -120,7 +120,7 @@
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<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' : ''"
|
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
|
||||||
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
|
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
|
||||||
<a-tag color="red" v-if="isExpiry">Expired</a-tag>
|
<a-tag color="red" v-if="isExpiry">Expired</a-tag>
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<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' : ''"
|
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
|
||||||
v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker>
|
v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
|
@ -65,7 +65,7 @@
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<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' : ''"
|
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
|
||||||
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
|
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
|
@ -71,7 +71,7 @@
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<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' : ''"
|
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
|
||||||
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
|
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
|
@ -62,7 +62,7 @@
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<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' : ''"
|
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
|
||||||
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
|
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
|
@ -12,5 +12,10 @@
|
||||||
</span>
|
</span>
|
||||||
<a-switch v-model="inbound.sniffing.enabled"></a-switch>
|
<a-switch v-model="inbound.sniffing.enabled"></a-switch>
|
||||||
</a-form-item>
|
</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>
|
</a-form>
|
||||||
{{end}}
|
{{end}}
|
|
@ -37,7 +37,7 @@
|
||||||
<a-input v-model.trim="inbound.stream.tls.server" style="width: 250px"></a-input>
|
<a-input v-model.trim="inbound.stream.tls.server" style="width: 250px"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="CipherSuites">
|
<a-form-item label="CipherSuites">
|
||||||
<a-select v-model="inbound.stream.tls.cipherSuites" style="width: 300px">
|
<a-select v-model="inbound.stream.tls.cipherSuites" style="width: 300px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||||
<a-select-option value="">auto</a-select-option>
|
<a-select-option value="">auto</a-select-option>
|
||||||
<a-select-option v-for="key in TLS_CIPHER_OPTION" :value="key">[[ key ]]</a-select-option>
|
<a-select-option v-for="key in TLS_CIPHER_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
|
@ -56,14 +56,15 @@
|
||||||
<a-input v-model.trim="inbound.stream.tls.settings.serverName" style="width: 250px"></a-input>
|
<a-input v-model.trim="inbound.stream.tls.settings.serverName" style="width: 250px"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="uTLS">
|
<a-form-item label="uTLS">
|
||||||
<a-select v-model="inbound.stream.tls.settings.fingerprint" style="width: 170px">
|
<a-select v-model="inbound.stream.tls.settings.fingerprint"
|
||||||
|
style="width: 170px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||||
<a-select-option value=''>None</a-select-option>
|
<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-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="Alpn">
|
<a-form-item label="Alpn">
|
||||||
<a-checkbox-group v-model="inbound.stream.tls.alpn" style="width:200px">
|
<a-checkbox-group v-model="inbound.stream.tls.alpn" style="width:200px">
|
||||||
<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-checkbox-group>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="Allow insecure">
|
<a-form-item label="Allow insecure">
|
||||||
|
@ -82,7 +83,7 @@
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
|
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
|
||||||
<a-input v-model.trim="inbound.stream.tls.certs[0].keyFile" style="width:300px;"></a-input>
|
<a-input v-model.trim="inbound.stream.tls.certs[0].keyFile" style="width:300px;"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-button @click="setDefaultCertData">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
|
<a-button type="primary" icon="import" @click="setDefaultCertData">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
|
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
|
||||||
|
@ -120,7 +121,7 @@
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
|
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
|
||||||
<a-input v-model.trim="inbound.stream.xtls.certs[0].keyFile" style="width:300px;"></a-input>
|
<a-input v-model.trim="inbound.stream.xtls.certs[0].keyFile" style="width:300px;"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-button @click="setDefaultCertData">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
|
<a-button type="primary" icon="import" @click="setDefaultCertData">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
|
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
|
||||||
|
@ -142,7 +143,8 @@
|
||||||
<a-input type="number" v-model.number="inbound.stream.reality.xver" :min="0" style="width: 60px"></a-input>
|
<a-input type="number" v-model.number="inbound.stream.reality.xver" :min="0" style="width: 60px"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="uTLS" >
|
<a-form-item label="uTLS" >
|
||||||
<a-select v-model="inbound.stream.reality.settings.fingerprint" style="width: 135px">
|
<a-select v-model="inbound.stream.reality.settings.fingerprint"
|
||||||
|
style="width: 135px" :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-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
|
@ -66,28 +66,42 @@
|
||||||
<transition name="list" appear>
|
<transition name="list" appear>
|
||||||
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
||||||
<div slot="title">
|
<div slot="title">
|
||||||
<a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button>
|
<a-row>
|
||||||
<a-dropdown :trigger="['click']">
|
<a-col :xs="24" :sm="24" :lg="12">
|
||||||
<a-button type="primary" icon="menu">{{ i18n "pages.inbounds.generalActions" }}</a-button>
|
<a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button>
|
||||||
<a-menu slot="overlay" @click="a => generalActions(a)" :theme="siderDrawer.theme">
|
<a-dropdown :trigger="['click']">
|
||||||
<a-menu-item key="export">
|
<a-button type="primary" icon="menu">{{ i18n "pages.inbounds.generalActions" }}</a-button>
|
||||||
<a-icon type="export"></a-icon>
|
<a-menu slot="overlay" @click="a => generalActions(a)" :theme="siderDrawer.theme">
|
||||||
{{ i18n "pages.inbounds.export" }}
|
<a-menu-item key="export">
|
||||||
</a-menu-item>
|
<a-icon type="export"></a-icon>
|
||||||
<a-menu-item key="resetInbounds">
|
{{ i18n "pages.inbounds.export" }}
|
||||||
<a-icon type="reload"></a-icon>
|
</a-menu-item>
|
||||||
{{ i18n "pages.inbounds.resetAllTraffic" }}
|
<a-menu-item key="resetInbounds">
|
||||||
</a-menu-item>
|
<a-icon type="reload"></a-icon>
|
||||||
<a-menu-item key="resetClients">
|
{{ i18n "pages.inbounds.resetAllTraffic" }}
|
||||||
<a-icon type="file-done"></a-icon>
|
</a-menu-item>
|
||||||
{{ i18n "pages.inbounds.resetAllClientTraffics" }}
|
<a-menu-item key="resetClients">
|
||||||
</a-menu-item>
|
<a-icon type="file-done"></a-icon>
|
||||||
<a-menu-item key="delDepletedClients">
|
{{ i18n "pages.inbounds.resetAllClientTraffics" }}
|
||||||
<a-icon type="rest"></a-icon>
|
</a-menu-item>
|
||||||
{{ i18n "pages.inbounds.delDepletedClients" }}
|
<a-menu-item key="delDepletedClients">
|
||||||
</a-menu-item>
|
<a-icon type="rest"></a-icon>
|
||||||
</a-menu>
|
{{ i18n "pages.inbounds.delDepletedClients" }}
|
||||||
</a-dropdown>
|
</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"></a-icon>
|
||||||
|
<a-switch v-model="isRefreshEnabled" @change="toggleRefresh"></a-switch>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
</div>
|
</div>
|
||||||
<a-input v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px"></a-input>
|
<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"
|
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
|
||||||
|
@ -263,7 +277,7 @@
|
||||||
}, {
|
}, {
|
||||||
title: '{{ i18n "pages.inbounds.protocol" }}',
|
title: '{{ i18n "pages.inbounds.protocol" }}',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
width: 80,
|
width: 90,
|
||||||
scopedSlots: { customRender: 'protocol' },
|
scopedSlots: { customRender: 'protocol' },
|
||||||
}, {
|
}, {
|
||||||
title: '{{ i18n "clients" }}',
|
title: '{{ i18n "clients" }}',
|
||||||
|
@ -315,25 +329,22 @@
|
||||||
defaultCert: '',
|
defaultCert: '',
|
||||||
defaultKey: '',
|
defaultKey: '',
|
||||||
clientCount: {},
|
clientCount: {},
|
||||||
|
isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
|
||||||
|
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loading(spinning=true) {
|
loading(spinning=true) {
|
||||||
this.spinning = spinning;
|
this.spinning = spinning;
|
||||||
},
|
},
|
||||||
async getDBInbounds() {
|
async getDBInbounds() {
|
||||||
this.loading();
|
|
||||||
const msg = await HttpUtil.post('/xui/inbound/list');
|
const msg = await HttpUtil.post('/xui/inbound/list');
|
||||||
this.loading(false);
|
|
||||||
if (!msg.success) {
|
if (!msg.success) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setInbounds(msg.obj);
|
this.setInbounds(msg.obj);
|
||||||
this.searchKey = '';
|
|
||||||
},
|
},
|
||||||
async getDefaultSettings() {
|
async getDefaultSettings() {
|
||||||
this.loading();
|
|
||||||
const msg = await HttpUtil.post('/xui/setting/defaultSettings');
|
const msg = await HttpUtil.post('/xui/setting/defaultSettings');
|
||||||
this.loading(false);
|
|
||||||
if (!msg.success) {
|
if (!msg.success) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -345,17 +356,16 @@
|
||||||
setInbounds(dbInbounds) {
|
setInbounds(dbInbounds) {
|
||||||
this.inbounds.splice(0);
|
this.inbounds.splice(0);
|
||||||
this.dbInbounds.splice(0);
|
this.dbInbounds.splice(0);
|
||||||
this.searchedInbounds.splice(0);
|
|
||||||
for (const inbound of dbInbounds) {
|
for (const inbound of dbInbounds) {
|
||||||
const dbInbound = new DBInbound(inbound);
|
const dbInbound = new DBInbound(inbound);
|
||||||
to_inbound = dbInbound.toInbound()
|
to_inbound = dbInbound.toInbound()
|
||||||
this.inbounds.push(to_inbound);
|
this.inbounds.push(to_inbound);
|
||||||
this.dbInbounds.push(dbInbound);
|
this.dbInbounds.push(dbInbound);
|
||||||
this.searchedInbounds.push(dbInbound);
|
|
||||||
if([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN].includes(inbound.protocol) ){
|
if([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN].includes(inbound.protocol) ){
|
||||||
this.clientCount[inbound.id] = this.getClientCounts(inbound,to_inbound);
|
this.clientCount[inbound.id] = this.getClientCounts(inbound,to_inbound);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.searchInbounds(this.searchKey);
|
||||||
},
|
},
|
||||||
getClientCounts(dbInbound,inbound){
|
getClientCounts(dbInbound,inbound){
|
||||||
let clientCount = 0,active = [], deactive = [], depleted = [], expiring = [];
|
let clientCount = 0,active = [], deactive = [], depleted = [], expiring = [];
|
||||||
|
@ -788,6 +798,25 @@
|
||||||
}
|
}
|
||||||
txtModal.show('{{ i18n "pages.inbounds.export"}}',copyText,'All-Inbounds');
|
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);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
searchKey: debounce(function (newVal) {
|
searchKey: debounce(function (newVal) {
|
||||||
|
@ -795,8 +824,15 @@
|
||||||
}, 500)
|
}, 500)
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.loading();
|
||||||
this.getDefaultSettings();
|
this.getDefaultSettings();
|
||||||
this.getDBInbounds();
|
if (this.isRefreshEnabled) {
|
||||||
|
this.startDataRefreshLoop();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.getDBInbounds();
|
||||||
|
}
|
||||||
|
this.loading(false);
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
total() {
|
total() {
|
||||||
|
@ -823,7 +859,6 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{{template "inboundModal"}}
|
{{template "inboundModal"}}
|
||||||
|
|
|
@ -830,12 +830,14 @@ func (s *InboundService) GetClientTrafficByEmail(email string) (traffic *xray.Cl
|
||||||
|
|
||||||
err = db.Model(xray.ClientTraffic{}).Where("email = ?", email).Find(&traffics).Error
|
err = db.Model(xray.ClientTraffic{}).Where("email = ?", email).Find(&traffics).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == gorm.ErrRecordNotFound {
|
logger.Warning(err)
|
||||||
logger.Warning(err)
|
return nil, err
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return traffics[0], err
|
if len(traffics) > 0 {
|
||||||
|
return traffics[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.ClientTraffic, err error) {
|
func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.ClientTraffic, err error) {
|
||||||
|
|
|
@ -33,9 +33,6 @@ import (
|
||||||
//go:embed assets/*
|
//go:embed assets/*
|
||||||
var assetsFS embed.FS
|
var assetsFS embed.FS
|
||||||
|
|
||||||
//go:embed assets/favicon.ico
|
|
||||||
var favicon []byte
|
|
||||||
|
|
||||||
//go:embed html/*
|
//go:embed html/*
|
||||||
var htmlFS embed.FS
|
var htmlFS embed.FS
|
||||||
|
|
||||||
|
@ -161,11 +158,6 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||||
|
|
||||||
engine := gin.Default()
|
engine := gin.Default()
|
||||||
|
|
||||||
// Add favicon
|
|
||||||
engine.GET("/favicon.ico", func(c *gin.Context) {
|
|
||||||
c.Data(200, "image/x-icon", favicon)
|
|
||||||
})
|
|
||||||
|
|
||||||
secret, err := s.settingService.GetSecret()
|
secret, err := s.settingService.GetSecret()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
128
x-ui.sh
128
x-ui.sh
|
@ -59,13 +59,13 @@ fi
|
||||||
confirm() {
|
confirm() {
|
||||||
if [[ $# > 1 ]]; then
|
if [[ $# > 1 ]]; then
|
||||||
echo && read -p "$1 [Default $2]: " temp
|
echo && read -p "$1 [Default $2]: " temp
|
||||||
if [[ x"${temp}" == x"" ]]; then
|
if [[ "${temp}" == "" ]]; then
|
||||||
temp=$2
|
temp=$2
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
read -p "$1 [y/n]: " temp
|
read -p "$1 [y/n]: " temp
|
||||||
fi
|
fi
|
||||||
if [[ x"${temp}" == x"y" || x"${temp}" == x"Y" ]]; then
|
if [[ "${temp}" == "y" || "${temp}" == "Y" ]]; then
|
||||||
return 0
|
return 0
|
||||||
else
|
else
|
||||||
return 1
|
return 1
|
||||||
|
@ -342,7 +342,7 @@ check_status() {
|
||||||
return 2
|
return 2
|
||||||
fi
|
fi
|
||||||
temp=$(systemctl status x-ui | grep Active | awk '{print $3}' | cut -d "(" -f2 | cut -d ")" -f1)
|
temp=$(systemctl status x-ui | grep Active | awk '{print $3}' | cut -d "(" -f2 | cut -d ")" -f1)
|
||||||
if [[ x"${temp}" == x"running" ]]; then
|
if [[ "${temp}" == "running" ]]; then
|
||||||
return 0
|
return 0
|
||||||
else
|
else
|
||||||
return 1
|
return 1
|
||||||
|
@ -351,7 +351,7 @@ check_status() {
|
||||||
|
|
||||||
check_enabled() {
|
check_enabled() {
|
||||||
temp=$(systemctl is-enabled x-ui)
|
temp=$(systemctl is-enabled x-ui)
|
||||||
if [[ x"${temp}" == x"enabled" ]]; then
|
if [[ "${temp}" == "enabled" ]]; then
|
||||||
return 0
|
return 0
|
||||||
else
|
else
|
||||||
return 1
|
return 1
|
||||||
|
@ -431,32 +431,6 @@ show_xray_status() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
#this will be an entrance for ssl cert issue
|
|
||||||
#here we can provide two different methods to issue cert
|
|
||||||
#first.standalone mode second.DNS API mode
|
|
||||||
ssl_cert_issue() {
|
|
||||||
local method=""
|
|
||||||
echo -E ""
|
|
||||||
LOGD "********Usage********"
|
|
||||||
LOGI "this shell script will use acme to help issue certs."
|
|
||||||
LOGI "here we provide two methods for issuing certs:"
|
|
||||||
LOGI "method 1:acme standalone mode,need to keep port:80 open"
|
|
||||||
LOGI "method 2:acme DNS API mode,need provide Cloudflare Global API Key"
|
|
||||||
LOGI "recommend method 2 first,if it fails,you can try method 1."
|
|
||||||
LOGI "certs will be installed in /root/cert directory"
|
|
||||||
read -p "please choose which method do you want,type 1 or 2": method
|
|
||||||
LOGI "you choosed method:${method}"
|
|
||||||
|
|
||||||
if [ "${method}" == "1" ]; then
|
|
||||||
ssl_cert_issue_standalone
|
|
||||||
elif [ "${method}" == "2" ]; then
|
|
||||||
ssl_cert_issue_by_cloudflare
|
|
||||||
else
|
|
||||||
LOGE "invalid input,please check it..."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
open_ports() {
|
open_ports() {
|
||||||
if ! command -v ufw &> /dev/null
|
if ! command -v ufw &> /dev/null
|
||||||
then
|
then
|
||||||
|
@ -544,7 +518,7 @@ install_acme() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#method for standalone mode
|
#method for standalone mode
|
||||||
ssl_cert_issue_standalone() {
|
ssl_cert_issue() {
|
||||||
#check for acme.sh first
|
#check for acme.sh first
|
||||||
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
|
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
|
||||||
echo "acme.sh could not be found. we will install it"
|
echo "acme.sh could not be found. we will install it"
|
||||||
|
@ -555,7 +529,7 @@ ssl_cert_issue_standalone() {
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
#install socat second
|
#install socat second
|
||||||
if [[ x"${release}" == x"centos" ]]; then
|
if [[ "${release}" == "centos" ]] || [[ "${release}" == "fedora" ]] ; then
|
||||||
yum install socat -y
|
yum install socat -y
|
||||||
else
|
else
|
||||||
apt install socat -y
|
apt install socat -y
|
||||||
|
@ -569,7 +543,7 @@ ssl_cert_issue_standalone() {
|
||||||
|
|
||||||
#get the domain here,and we need verify it
|
#get the domain here,and we need verify it
|
||||||
local domain=""
|
local domain=""
|
||||||
read -p "please input your domain:" domain
|
read -p "Please enter your domain name:" domain
|
||||||
LOGD "your domain is:${domain},check it..."
|
LOGD "your domain is:${domain},check it..."
|
||||||
#here we need to judge whether there exists cert already
|
#here we need to judge whether there exists cert already
|
||||||
local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}')
|
local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}')
|
||||||
|
@ -636,94 +610,6 @@ ssl_cert_issue_standalone() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#method for DNS API mode
|
|
||||||
ssl_cert_issue_by_cloudflare() {
|
|
||||||
echo -E ""
|
|
||||||
LOGD "******Preconditions******"
|
|
||||||
LOGI "1.need Cloudflare account associated email"
|
|
||||||
LOGI "2.need Cloudflare Global API Key"
|
|
||||||
LOGI "3.your domain use Cloudflare as resolver"
|
|
||||||
confirm "I have confirmed all these info above[y/n]" "y"
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
install_acme
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
LOGE "install acme failed,please check logs"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
CF_Domain=""
|
|
||||||
CF_GlobalKey=""
|
|
||||||
CF_AccountEmail=""
|
|
||||||
|
|
||||||
LOGD "please input your domain:"
|
|
||||||
read -p "Input your domain here:" CF_Domain
|
|
||||||
LOGD "your domain is:${CF_Domain},check it..."
|
|
||||||
#here we need to judge whether there exists cert already
|
|
||||||
local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}')
|
|
||||||
if [ ${currentCert} == ${CF_Domain} ]; then
|
|
||||||
local certInfo=$(~/.acme.sh/acme.sh --list)
|
|
||||||
LOGE "system already have certs here,can not issue again,current certs details:"
|
|
||||||
LOGI "$certInfo"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
LOGI "your domain is ready for issuing cert now..."
|
|
||||||
fi
|
|
||||||
|
|
||||||
#create a directory for install cert
|
|
||||||
certPath="/root/cert/${CF_Domain}"
|
|
||||||
if [ ! -d "$certPath" ]; then
|
|
||||||
mkdir -p "$certPath"
|
|
||||||
else
|
|
||||||
rm -rf "$certPath"
|
|
||||||
mkdir -p "$certPath"
|
|
||||||
fi
|
|
||||||
|
|
||||||
LOGD "please inout your cloudflare global API key:"
|
|
||||||
read -p "Input your key here:" CF_GlobalKey
|
|
||||||
LOGD "your cloudflare global API key is:${CF_GlobalKey}"
|
|
||||||
LOGD "please input your cloudflare account email:"
|
|
||||||
read -p "Input your email here:" CF_AccountEmail
|
|
||||||
LOGD "your cloudflare account email:${CF_AccountEmail}"
|
|
||||||
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
LOGE "change the default CA to Lets'Encrypt failed,exit"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
export CF_Key="${CF_GlobalKey}"
|
|
||||||
export CF_Email=${CF_AccountEmail}
|
|
||||||
~/.acme.sh/acme.sh --issue --dns dns_cf -d ${CF_Domain} -d *.${CF_Domain} --log
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
LOGE "issue cert failed,exit"
|
|
||||||
rm -rf ~/.acme.sh/${CF_Domain}
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
LOGI "Certificate issued Successfully, Installing..."
|
|
||||||
fi
|
|
||||||
~/.acme.sh/acme.sh --installcert -d ${CF_Domain} -d *.${CF_Domain} \
|
|
||||||
--key-file /root/cert/${CF_Domain}/privkey.pem \
|
|
||||||
--fullchain-file /root/cert/${CF_Domain}/fullchain.pem
|
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
LOGE "install cert failed,exit"
|
|
||||||
rm -rf ~/.acme.sh/${CF_Domain}
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
LOGI "Certificate installed Successfully,Turning on automatic updates..."
|
|
||||||
fi
|
|
||||||
~/.acme.sh/acme.sh --upgrade --auto-upgrade
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
LOGE "auto renew failed, certs details:"
|
|
||||||
ls -lah cert/*
|
|
||||||
chmod 755 $certPath/*
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
LOGI "auto renew succeed, certs details:"
|
|
||||||
ls -lah cert/*
|
|
||||||
chmod 755 $certPath/*
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
show_menu
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
warp_fixchatgpt() {
|
warp_fixchatgpt() {
|
||||||
curl -fsSL https://gist.githubusercontent.com/hamid-gh98/dc5dd9b0cc5b0412af927b1ccdb294c7/raw/install_warp_proxy.sh | bash
|
curl -fsSL https://gist.githubusercontent.com/hamid-gh98/dc5dd9b0cc5b0412af927b1ccdb294c7/raw/install_warp_proxy.sh | bash
|
||||||
|
|
Loading…
Reference in a new issue