Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ce53920fe | ||
|
|
5100bbba52 | ||
|
|
f93d912644 | ||
|
|
ce8551b8c4 | ||
|
|
d26c21d900 | ||
|
|
f5f9347661 | ||
|
|
a0f5875cb3 | ||
|
|
3055c68615 | ||
|
|
c3ed8051f3 | ||
|
|
d2cdc51c54 | ||
|
|
ee896662f5 | ||
|
|
177bd036a3 | ||
|
|
d03e049320 | ||
|
|
957d9e24fb | ||
|
|
865e47e9a6 | ||
|
|
607c5d3598 | ||
|
|
8879541999 | ||
|
|
0b896d9c31 | ||
|
|
6f4a2809e2 | ||
|
|
103a26edb6 |
11
README.md
11
README.md
@@ -20,10 +20,10 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
|
||||
|
||||
## Install custom version
|
||||
|
||||
To install your desired version you can add the version to the end of install command. Example for ver `v1.3.2`:
|
||||
To install your desired version you can add the version to the end of install command. Example for ver `v1.3.3`:
|
||||
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v1.3.2
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v1.3.3
|
||||
```
|
||||
|
||||
# SSL
|
||||
@@ -177,6 +177,8 @@ Reference syntax:
|
||||
| `POST` | `"/resetAllClientTraffics/:id"` | Reset traffics of all clients in an inbound |
|
||||
| `POST` | `"/delDepletedClients/:id"` | Delete inbound depleted clients (-1: all) |
|
||||
|
||||
- [Postman Collection](https://gist.github.com/mehdikhody/9a862801a2e41f6b5fb6bbc7e1326044)
|
||||
|
||||
# A Special Thanks To
|
||||
|
||||
- [alireza0](https://github.com/alireza0/)
|
||||
@@ -190,11 +192,8 @@ Reference syntax:
|
||||
|
||||
# Buy Me a Coffee
|
||||
|
||||
[](#)
|
||||
- Tron USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||
|
||||
```
|
||||
TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC
|
||||
```
|
||||
|
||||
# Pictures
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.3.2
|
||||
1.3.4
|
||||
|
||||
2
go.sum
2
go.sum
@@ -9,8 +9,6 @@ github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
||||
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.8.7 h1:d3sry5vGgVq/OpgozRUNP6xBsSo0mtNdwliApw+SAMQ=
|
||||
github.com/bytedance/sonic v1.8.7/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/bytedance/sonic v1.8.8 h1:Kj4AYbZSeENfyXicsYppYKO0K2YWab+i2UTSY7Ukz9Q=
|
||||
github.com/bytedance/sonic v1.8.8/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
|
||||
@@ -73,7 +73,7 @@ config_after_install() {
|
||||
/usr/local/x-ui/x-ui migrate
|
||||
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
|
||||
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
|
||||
echo -e "${yellow}Your username will be:${config_account}${plain}"
|
||||
read -p "Please set up your password:" config_password
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"github.com/op/go-logging"
|
||||
"os"
|
||||
|
||||
"github.com/op/go-logging"
|
||||
)
|
||||
|
||||
var logger *logging.Logger
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -28,20 +28,6 @@ const SSMethods = {
|
||||
BLAKE3_CHACHA20_POLY1305: '2022-blake3-chacha20-poly1305',
|
||||
};
|
||||
|
||||
const RULE_IP = {
|
||||
PRIVATE: 'geoip:private',
|
||||
CN: 'geoip:cn',
|
||||
};
|
||||
|
||||
const RULE_DOMAIN = {
|
||||
ADS: 'geosite:category-ads',
|
||||
ADS_ALL: 'geosite:category-ads-all',
|
||||
CN: 'geosite:cn',
|
||||
GOOGLE: 'geosite:google',
|
||||
FACEBOOK: 'geosite:facebook',
|
||||
SPEEDTEST: 'geosite:speedtest',
|
||||
};
|
||||
|
||||
const XTLS_FLOW_CONTROL = {
|
||||
ORIGIN: "xtls-rprx-origin",
|
||||
DIRECT: "xtls-rprx-direct",
|
||||
@@ -93,21 +79,26 @@ 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);
|
||||
Object.freeze(VmessMethods);
|
||||
Object.freeze(SSMethods);
|
||||
Object.freeze(RULE_IP);
|
||||
Object.freeze(RULE_DOMAIN);
|
||||
Object.freeze(XTLS_FLOW_CONTROL);
|
||||
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 {
|
||||
|
||||
@@ -456,18 +447,26 @@ class QuicStreamSettings extends XrayCommonClass {
|
||||
}
|
||||
|
||||
class GrpcStreamSettings extends XrayCommonClass {
|
||||
constructor(serviceName="") {
|
||||
constructor(
|
||||
serviceName="",
|
||||
multiMode=false
|
||||
) {
|
||||
super();
|
||||
this.serviceName = serviceName;
|
||||
this.multiMode = multiMode;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
return new GrpcStreamSettings(json.serviceName);
|
||||
return new GrpcStreamSettings(
|
||||
json.serviceName,
|
||||
json.multiMode
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
serviceName: this.serviceName,
|
||||
multiMode: this.multiMode
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -888,7 +887,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;
|
||||
@@ -898,7 +897,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(
|
||||
@@ -1160,6 +1159,7 @@ class Inbound extends XrayCommonClass {
|
||||
case Protocols.VMESS:
|
||||
case Protocols.VLESS:
|
||||
case Protocols.TROJAN:
|
||||
case Protocols.SHADOWSOCKS:
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
@@ -1261,50 +1261,6 @@ class Inbound extends XrayCommonClass {
|
||||
if (this.protocol !== Protocols.VMESS) {
|
||||
return '';
|
||||
}
|
||||
let network = this.stream.network;
|
||||
let type = 'none';
|
||||
let host = '';
|
||||
let path = '';
|
||||
if (network === 'tcp') {
|
||||
let tcp = this.stream.tcp;
|
||||
type = tcp.type;
|
||||
if (type === 'http') {
|
||||
let request = tcp.request;
|
||||
path = request.path.join(',');
|
||||
let index = request.headers.findIndex(header => header.name.toLowerCase() === 'host');
|
||||
if (index >= 0) {
|
||||
host = request.headers[index].value;
|
||||
}
|
||||
}
|
||||
} else if (network === 'kcp') {
|
||||
let kcp = this.stream.kcp;
|
||||
type = kcp.type;
|
||||
path = kcp.seed;
|
||||
} else if (network === 'ws') {
|
||||
let ws = this.stream.ws;
|
||||
path = ws.path;
|
||||
let index = ws.headers.findIndex(header => header.name.toLowerCase() === 'host');
|
||||
if (index >= 0) {
|
||||
host = ws.headers[index].value;
|
||||
}
|
||||
} else if (network === 'http') {
|
||||
network = 'h2';
|
||||
path = this.stream.http.path;
|
||||
host = this.stream.http.host.join(',');
|
||||
} else if (network === 'quic') {
|
||||
type = this.stream.quic.type;
|
||||
host = this.stream.quic.security;
|
||||
path = this.stream.quic.key;
|
||||
} else if (network === 'grpc') {
|
||||
path = this.stream.grpc.serviceName;
|
||||
}
|
||||
|
||||
if (this.stream.security === 'tls') {
|
||||
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
|
||||
address = this.stream.tls.server;
|
||||
}
|
||||
}
|
||||
|
||||
let obj = {
|
||||
v: '2',
|
||||
ps: remark,
|
||||
@@ -1312,16 +1268,66 @@ class Inbound extends XrayCommonClass {
|
||||
port: this.port,
|
||||
id: this.settings.vmesses[clientIndex].id,
|
||||
aid: this.settings.vmesses[clientIndex].alterId,
|
||||
net: network,
|
||||
type: type,
|
||||
host: host,
|
||||
path: path,
|
||||
net: this.stream.network,
|
||||
type: 'none',
|
||||
tls: this.stream.security,
|
||||
sni: this.stream.tls.settings.serverName,
|
||||
fp: this.stream.tls.settings.fingerprint,
|
||||
alpn: this.stream.tls.alpn.join(','),
|
||||
allowInsecure: this.stream.tls.settings.allowInsecure,
|
||||
};
|
||||
let network = this.stream.network;
|
||||
if (network === 'tcp') {
|
||||
let tcp = this.stream.tcp;
|
||||
obj.type = tcp.type;
|
||||
if (tcp.type === 'http') {
|
||||
let request = tcp.request;
|
||||
obj.path = request.path.join(',');
|
||||
let index = request.headers.findIndex(header => header.name.toLowerCase() === 'host');
|
||||
if (index >= 0) {
|
||||
obj.host = request.headers[index].value;
|
||||
}
|
||||
}
|
||||
} else if (network === 'kcp') {
|
||||
let kcp = this.stream.kcp;
|
||||
obj.type = kcp.type;
|
||||
obj.path = kcp.seed;
|
||||
} else if (network === 'ws') {
|
||||
let ws = this.stream.ws;
|
||||
obj.path = ws.path;
|
||||
let index = ws.headers.findIndex(header => header.name.toLowerCase() === 'host');
|
||||
if (index >= 0) {
|
||||
obj.host = ws.headers[index].value;
|
||||
}
|
||||
} else if (network === 'http') {
|
||||
obj.net = 'h2';
|
||||
obj.path = this.stream.http.path;
|
||||
obj.host = this.stream.http.host.join(',');
|
||||
} else if (network === 'quic') {
|
||||
obj.type = this.stream.quic.type;
|
||||
obj.host = this.stream.quic.security;
|
||||
obj.path = this.stream.quic.key;
|
||||
} else if (network === 'grpc') {
|
||||
obj.path = this.stream.grpc.serviceName;
|
||||
if (this.stream.grpc.multiMode){
|
||||
obj.type = 'multi'
|
||||
}
|
||||
}
|
||||
|
||||
if (this.stream.security === 'tls') {
|
||||
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
|
||||
obj.add = this.stream.tls.server;
|
||||
}
|
||||
if (!ObjectUtil.isEmpty(this.stream.tls.settings.serverName)){
|
||||
obj.sni = this.stream.tls.settings.serverName;
|
||||
}
|
||||
if (!ObjectUtil.isEmpty(this.stream.tls.settings.fingerprint)){
|
||||
obj.fp = this.stream.tls.settings.fingerprint;
|
||||
}
|
||||
if (this.stream.tls.alpn.length>0){
|
||||
obj.alpn = this.stream.tls.alpn.join(',');
|
||||
}
|
||||
if (this.stream.tls.settings.allowInsecure){
|
||||
obj.allowInsecure = this.stream.tls.settings.allowInsecure;
|
||||
}
|
||||
}
|
||||
|
||||
return 'vmess://' + base64(JSON.stringify(obj, null, 2));
|
||||
}
|
||||
|
||||
@@ -1374,6 +1380,9 @@ class Inbound extends XrayCommonClass {
|
||||
case "grpc":
|
||||
const grpc = this.stream.grpc;
|
||||
params.set("serviceName", grpc.serviceName);
|
||||
if(grpc.multiMode){
|
||||
params.set("mode", "multi");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1491,6 +1500,9 @@ class Inbound extends XrayCommonClass {
|
||||
case "grpc":
|
||||
const grpc = this.stream.grpc;
|
||||
params.set("serviceName", grpc.serviceName);
|
||||
if(grpc.multiMode){
|
||||
params.set("mode", "multi");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"x-ui/web/session"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type BaseController struct {
|
||||
|
||||
@@ -2,8 +2,9 @@ package global
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/robfig/cron/v3"
|
||||
_ "unsafe"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
var webServer WebServer
|
||||
|
||||
@@ -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/element-ui@2.15.0/theme-chalk/display.css">
|
||||
<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>
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</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' : ''"
|
||||
v-model="clientsBulkModal.expiryTime" style="width: 300px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
const siderDrawer = {
|
||||
visible: false,
|
||||
collapsed: false,
|
||||
isDarkTheme: localStorage.getItem("dark-mode") === 'true' ? true : false,
|
||||
isDarkTheme: localStorage.getItem("dark-mode") === 'false' ? false : true,
|
||||
show() {
|
||||
this.visible = true;
|
||||
},
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</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' : ''"
|
||||
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
|
||||
<a-tag color="red" v-if="isExpiry">Expired</a-tag>
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</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' : ''"
|
||||
v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</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' : ''"
|
||||
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</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' : ''"
|
||||
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</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' : ''"
|
||||
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
|
||||
@@ -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}}
|
||||
@@ -3,5 +3,8 @@
|
||||
<a-form-item label="ServiceName">
|
||||
<a-input v-model.trim="inbound.stream.grpc.serviceName"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Multi Mode">
|
||||
<a-switch v-model="inbound.stream.grpc.multiMode"></a-switch>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
@@ -37,7 +37,7 @@
|
||||
<a-input v-model.trim="inbound.stream.tls.server" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
<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 v-for="key in TLS_CIPHER_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
@@ -56,14 +56,15 @@
|
||||
<a-input v-model.trim="inbound.stream.tls.settings.serverName" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
<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 v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Alpn">
|
||||
<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-form-item>
|
||||
<a-form-item label="Allow insecure">
|
||||
@@ -82,7 +83,7 @@
|
||||
<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-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 v-else>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
|
||||
@@ -120,7 +121,7 @@
|
||||
<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-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 v-else>
|
||||
<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-form-item>
|
||||
<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>
|
||||
</a-form-item>
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
|
||||
<template v-if="inbound.isGrpc">
|
||||
<tr><td>grpc serviceName</td><td><a-tag color="green">[[ inbound.serviceName ]]</a-tag></td></tr>
|
||||
<tr><td>grpc multiMode</td><td><a-tag color="green">[[ inbound.stream.grpc.multiMode ]]</a-tag></td></tr>
|
||||
</template>
|
||||
</table>
|
||||
</td></tr>
|
||||
|
||||
@@ -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"></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"
|
||||
@@ -263,7 +277,7 @@
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.protocol" }}',
|
||||
align: 'left',
|
||||
width: 80,
|
||||
width: 90,
|
||||
scopedSlots: { customRender: 'protocol' },
|
||||
}, {
|
||||
title: '{{ i18n "clients" }}',
|
||||
@@ -315,25 +329,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;
|
||||
}
|
||||
@@ -345,17 +356,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 = [];
|
||||
@@ -788,6 +798,25 @@
|
||||
}
|
||||
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: {
|
||||
searchKey: debounce(function (newVal) {
|
||||
@@ -795,8 +824,15 @@
|
||||
}, 500)
|
||||
},
|
||||
mounted() {
|
||||
this.loading();
|
||||
this.getDefaultSettings();
|
||||
this.getDBInbounds();
|
||||
if (this.isRefreshEnabled) {
|
||||
this.startDataRefreshLoop();
|
||||
}
|
||||
else {
|
||||
this.getDBInbounds();
|
||||
}
|
||||
this.loading(false);
|
||||
},
|
||||
computed: {
|
||||
total() {
|
||||
@@ -823,7 +859,6 @@
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
{{template "inboundModal"}}
|
||||
|
||||
@@ -4,23 +4,22 @@ import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"regexp"
|
||||
ss "strings"
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
"x-ui/web/service"
|
||||
"x-ui/xray"
|
||||
// "strconv"
|
||||
"github.com/go-cmd/cmd"
|
||||
|
||||
"net"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-cmd/cmd"
|
||||
)
|
||||
|
||||
type CheckClientIpJob struct {
|
||||
xrayService service.XrayService
|
||||
inboundService service.InboundService
|
||||
xrayService service.XrayService
|
||||
}
|
||||
|
||||
var job *CheckClientIpJob
|
||||
@@ -36,7 +35,7 @@ func (j *CheckClientIpJob) Run() {
|
||||
processLogFile()
|
||||
|
||||
// disAllowedIps = []string{"192.168.1.183","192.168.1.197"}
|
||||
blockedIps := []byte(ss.Join(disAllowedIps, ","))
|
||||
blockedIps := []byte(strings.Join(disAllowedIps, ","))
|
||||
err := os.WriteFile(xray.GetBlockedIPsPath(), blockedIps, 0755)
|
||||
checkError(err)
|
||||
|
||||
@@ -58,7 +57,7 @@ func processLogFile() {
|
||||
checkError(err)
|
||||
}
|
||||
|
||||
lines := ss.Split(string(data), "\n")
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
ipRegx, _ := regexp.Compile(`[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`)
|
||||
emailRegx, _ := regexp.Compile(`email:.+`)
|
||||
@@ -74,7 +73,7 @@ func processLogFile() {
|
||||
if matchesEmail == "" {
|
||||
continue
|
||||
}
|
||||
matchesEmail = ss.Split(matchesEmail, "email: ")[1]
|
||||
matchesEmail = strings.Split(matchesEmail, "email: ")[1]
|
||||
|
||||
if InboundClientIps[matchesEmail] != nil {
|
||||
if contains(InboundClientIps[matchesEmail], ip) {
|
||||
@@ -92,14 +91,12 @@ func processLogFile() {
|
||||
|
||||
for clientEmail, ips := range InboundClientIps {
|
||||
inboundClientIps, err := GetInboundClientIps(clientEmail)
|
||||
sort.Sort(sort.StringSlice(ips))
|
||||
sort.Strings(ips)
|
||||
if err != nil {
|
||||
addInboundClientIps(clientEmail, ips)
|
||||
|
||||
} else {
|
||||
updateInboundClientIps(inboundClientIps, clientEmail, ips)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// check if inbound connection is more than limited ip and drop connection
|
||||
@@ -202,6 +199,8 @@ func updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmai
|
||||
json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||
clients := settings["clients"]
|
||||
|
||||
var disAllowedIps []string // initialize the slice
|
||||
|
||||
for _, client := range clients {
|
||||
if client.Email == clientEmail {
|
||||
|
||||
@@ -214,7 +213,7 @@ func updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmai
|
||||
}
|
||||
}
|
||||
logger.Debug("disAllowedIps ", disAllowedIps)
|
||||
sort.Sort(sort.StringSlice(disAllowedIps))
|
||||
sort.Strings(disAllowedIps)
|
||||
|
||||
db := database.GetDB()
|
||||
err = db.Save(inboundClientIps).Error
|
||||
@@ -223,6 +222,7 @@ func updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmai
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DisableInbound(id int) error {
|
||||
db := database.GetDB()
|
||||
result := db.Model(model.Inbound{}).
|
||||
|
||||
@@ -830,12 +830,14 @@ func (s *InboundService) GetClientTrafficByEmail(email string) (traffic *xray.Cl
|
||||
|
||||
err = db.Model(xray.ClientTraffic{}).Where("email = ?", email).Find(&traffics).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
logger.Warning(err)
|
||||
return nil, err
|
||||
}
|
||||
logger.Warning(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) {
|
||||
@@ -912,6 +914,8 @@ func (s *InboundService) SearchInbounds(query string) ([]*model.Inbound, error)
|
||||
|
||||
func (s *InboundService) MigrationRequirements() {
|
||||
db := database.GetDB()
|
||||
|
||||
// Fix inbounds based problems
|
||||
var inbounds []*model.Inbound
|
||||
err := db.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan"}).Find(&inbounds).Error
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
@@ -922,6 +926,7 @@ func (s *InboundService) MigrationRequirements() {
|
||||
json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)
|
||||
clients, ok := settings["clients"].([]interface{})
|
||||
if ok {
|
||||
// Fix Clinet configuration problems
|
||||
var newClients []interface{}
|
||||
for client_index := range clients {
|
||||
c := clients[client_index].(map[string]interface{})
|
||||
@@ -947,6 +952,7 @@ func (s *InboundService) MigrationRequirements() {
|
||||
|
||||
inbounds[inbound_index].Settings = string(modifiedSettings)
|
||||
}
|
||||
// Add client traffic row for all clients which has email
|
||||
modelClients, err := s.getClients(inbounds[inbound_index])
|
||||
if err != nil {
|
||||
return
|
||||
@@ -962,4 +968,7 @@ func (s *InboundService) MigrationRequirements() {
|
||||
}
|
||||
}
|
||||
db.Save(inbounds)
|
||||
|
||||
// Remove orphaned traffics
|
||||
db.Where("inbound_id = 0").Delete(xray.ClientTraffic{})
|
||||
}
|
||||
|
||||
@@ -102,80 +102,89 @@ func (s *SubService) getLink(inbound *model.Inbound, email string) string {
|
||||
}
|
||||
|
||||
func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||
address := s.address
|
||||
if inbound.Protocol != model.VMess {
|
||||
return ""
|
||||
}
|
||||
obj := map[string]interface{}{
|
||||
"v": "2",
|
||||
"ps": email,
|
||||
"add": s.address,
|
||||
"port": inbound.Port,
|
||||
"type": "none",
|
||||
}
|
||||
var stream map[string]interface{}
|
||||
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
||||
network, _ := stream["network"].(string)
|
||||
typeStr := "none"
|
||||
host := ""
|
||||
path := ""
|
||||
sni := ""
|
||||
fp := ""
|
||||
var alpn []string
|
||||
allowInsecure := false
|
||||
obj["net"] = network
|
||||
switch network {
|
||||
case "tcp":
|
||||
tcp, _ := stream["tcpSettings"].(map[string]interface{})
|
||||
header, _ := tcp["header"].(map[string]interface{})
|
||||
typeStr, _ = header["type"].(string)
|
||||
typeStr, _ := header["type"].(string)
|
||||
obj["type"] = typeStr
|
||||
if typeStr == "http" {
|
||||
request := header["request"].(map[string]interface{})
|
||||
requestPath, _ := request["path"].([]interface{})
|
||||
path = requestPath[0].(string)
|
||||
obj["path"] = requestPath[0].(string)
|
||||
headers, _ := request["headers"].(map[string]interface{})
|
||||
host = searchHost(headers)
|
||||
obj["host"] = searchHost(headers)
|
||||
}
|
||||
case "kcp":
|
||||
kcp, _ := stream["kcpSettings"].(map[string]interface{})
|
||||
header, _ := kcp["header"].(map[string]interface{})
|
||||
typeStr, _ = header["type"].(string)
|
||||
path, _ = kcp["seed"].(string)
|
||||
obj["type"], _ = header["type"].(string)
|
||||
obj["path"], _ = kcp["seed"].(string)
|
||||
case "ws":
|
||||
ws, _ := stream["wsSettings"].(map[string]interface{})
|
||||
path = ws["path"].(string)
|
||||
obj["path"] = ws["path"].(string)
|
||||
headers, _ := ws["headers"].(map[string]interface{})
|
||||
host = searchHost(headers)
|
||||
obj["host"] = searchHost(headers)
|
||||
case "http":
|
||||
network = "h2"
|
||||
obj["net"] = "h2"
|
||||
http, _ := stream["httpSettings"].(map[string]interface{})
|
||||
path, _ = http["path"].(string)
|
||||
host = searchHost(http)
|
||||
obj["path"], _ = http["path"].(string)
|
||||
obj["host"] = searchHost(http)
|
||||
case "quic":
|
||||
quic, _ := stream["quicSettings"].(map[string]interface{})
|
||||
header := quic["header"].(map[string]interface{})
|
||||
typeStr, _ = header["type"].(string)
|
||||
host, _ = quic["security"].(string)
|
||||
path, _ = quic["key"].(string)
|
||||
obj["type"], _ = header["type"].(string)
|
||||
obj["host"], _ = quic["security"].(string)
|
||||
obj["path"], _ = quic["key"].(string)
|
||||
case "grpc":
|
||||
grpc, _ := stream["grpcSettings"].(map[string]interface{})
|
||||
path = grpc["serviceName"].(string)
|
||||
obj["path"] = grpc["serviceName"].(string)
|
||||
if grpc["multiMode"].(bool) {
|
||||
obj["type"] = "multi"
|
||||
}
|
||||
}
|
||||
|
||||
security, _ := stream["security"].(string)
|
||||
obj["tls"] = security
|
||||
if security == "tls" {
|
||||
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
|
||||
alpns, _ := tlsSetting["alpn"].([]interface{})
|
||||
for _, a := range alpns {
|
||||
alpn = append(alpn, a.(string))
|
||||
if len(alpns) > 0 {
|
||||
var alpn []string
|
||||
for _, a := range alpns {
|
||||
alpn = append(alpn, a.(string))
|
||||
}
|
||||
obj["alpn"] = strings.Join(alpn, ",")
|
||||
}
|
||||
tlsSettings, _ := searchKey(tlsSetting, "settings")
|
||||
if tlsSetting != nil {
|
||||
if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
|
||||
sni, _ = sniValue.(string)
|
||||
obj["sni"], _ = sniValue.(string)
|
||||
}
|
||||
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
|
||||
fp, _ = fpValue.(string)
|
||||
obj["fp"], _ = fpValue.(string)
|
||||
}
|
||||
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
|
||||
allowInsecure, _ = insecure.(bool)
|
||||
obj["allowInsecure"], _ = insecure.(bool)
|
||||
}
|
||||
}
|
||||
serverName, _ := tlsSetting["serverName"].(string)
|
||||
if serverName != "" {
|
||||
address = serverName
|
||||
obj["add"] = serverName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,24 +196,9 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||
break
|
||||
}
|
||||
}
|
||||
obj["id"] = clients[clientIndex].ID
|
||||
obj["aid"] = clients[clientIndex].AlterIds
|
||||
|
||||
obj := map[string]interface{}{
|
||||
"v": "2",
|
||||
"ps": email,
|
||||
"add": address,
|
||||
"port": inbound.Port,
|
||||
"id": clients[clientIndex].ID,
|
||||
"aid": clients[clientIndex].AlterIds,
|
||||
"net": network,
|
||||
"type": typeStr,
|
||||
"host": host,
|
||||
"path": path,
|
||||
"tls": security,
|
||||
"sni": sni,
|
||||
"fp": fp,
|
||||
"alpn": strings.Join(alpn, ","),
|
||||
"allowInsecure": allowInsecure,
|
||||
}
|
||||
jsonStr, _ := json.MarshalIndent(obj, "", " ")
|
||||
return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
|
||||
}
|
||||
@@ -266,6 +260,9 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
case "grpc":
|
||||
grpc, _ := stream["grpcSettings"].(map[string]interface{})
|
||||
params["serviceName"] = grpc["serviceName"].(string)
|
||||
if grpc["multiMode"].(bool) {
|
||||
params["mode"] = "multi"
|
||||
}
|
||||
}
|
||||
|
||||
security, _ := stream["security"].(string)
|
||||
@@ -444,6 +441,9 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
||||
case "grpc":
|
||||
grpc, _ := stream["grpcSettings"].(map[string]interface{})
|
||||
params["serviceName"] = grpc["serviceName"].(string)
|
||||
if grpc["multiMode"].(bool) {
|
||||
params["mode"] = "multi"
|
||||
}
|
||||
}
|
||||
|
||||
security, _ := stream["security"].(string)
|
||||
|
||||
@@ -33,9 +33,6 @@ import (
|
||||
//go:embed assets/*
|
||||
var assetsFS embed.FS
|
||||
|
||||
//go:embed assets/favicon.ico
|
||||
var favicon []byte
|
||||
|
||||
//go:embed html/*
|
||||
var htmlFS embed.FS
|
||||
|
||||
@@ -161,11 +158,6 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
|
||||
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()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
128
x-ui.sh
128
x-ui.sh
@@ -59,13 +59,13 @@ fi
|
||||
confirm() {
|
||||
if [[ $# > 1 ]]; then
|
||||
echo && read -p "$1 [Default $2]: " temp
|
||||
if [[ x"${temp}" == x"" ]]; then
|
||||
if [[ "${temp}" == "" ]]; then
|
||||
temp=$2
|
||||
fi
|
||||
else
|
||||
read -p "$1 [y/n]: " temp
|
||||
fi
|
||||
if [[ x"${temp}" == x"y" || x"${temp}" == x"Y" ]]; then
|
||||
if [[ "${temp}" == "y" || "${temp}" == "Y" ]]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
@@ -342,7 +342,7 @@ check_status() {
|
||||
return 2
|
||||
fi
|
||||
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
|
||||
else
|
||||
return 1
|
||||
@@ -351,7 +351,7 @@ check_status() {
|
||||
|
||||
check_enabled() {
|
||||
temp=$(systemctl is-enabled x-ui)
|
||||
if [[ x"${temp}" == x"enabled" ]]; then
|
||||
if [[ "${temp}" == "enabled" ]]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
@@ -431,32 +431,6 @@ show_xray_status() {
|
||||
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() {
|
||||
if ! command -v ufw &> /dev/null
|
||||
then
|
||||
@@ -544,7 +518,7 @@ install_acme() {
|
||||
}
|
||||
|
||||
#method for standalone mode
|
||||
ssl_cert_issue_standalone() {
|
||||
ssl_cert_issue() {
|
||||
#check for acme.sh first
|
||||
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
|
||||
echo "acme.sh could not be found. we will install it"
|
||||
@@ -555,7 +529,7 @@ ssl_cert_issue_standalone() {
|
||||
fi
|
||||
fi
|
||||
#install socat second
|
||||
if [[ x"${release}" == x"centos" ]]; then
|
||||
if [[ "${release}" == "centos" ]] || [[ "${release}" == "fedora" ]] ; then
|
||||
yum install socat -y
|
||||
else
|
||||
apt install socat -y
|
||||
@@ -569,7 +543,7 @@ ssl_cert_issue_standalone() {
|
||||
|
||||
#get the domain here,and we need verify it
|
||||
local domain=""
|
||||
read -p "please input your domain:" domain
|
||||
read -p "Please enter your domain name:" domain
|
||||
LOGD "your domain is:${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}')
|
||||
@@ -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() {
|
||||
curl -fsSL https://gist.githubusercontent.com/hamid-gh98/dc5dd9b0cc5b0412af927b1ccdb294c7/raw/install_warp_proxy.sh | bash
|
||||
|
||||
Reference in New Issue
Block a user