Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c87c1017d8 | ||
|
|
43aea38641 | ||
|
|
88744d92b3 | ||
|
|
7b38d02ff0 | ||
|
|
3da6c4d7d9 | ||
|
|
606360ae03 | ||
|
|
e2fd84a6ae | ||
|
|
f56dd43999 | ||
|
|
f0f5163a83 | ||
|
|
373628a6a3 | ||
|
|
a790efb18d | ||
|
|
868224ae97 | ||
|
|
60169bd055 | ||
|
|
33db9d0f90 | ||
|
|
27d020709e | ||
|
|
77be5cf7d8 | ||
|
|
c9d768a086 | ||
|
|
9c0718bc44 | ||
|
|
826c7264b5 | ||
|
|
162349f8c8 | ||
|
|
a6dfdcdd31 | ||
|
|
03a6c131f9 | ||
|
|
8dad9a4338 | ||
|
|
f0d4dbf838 | ||
|
|
3152d5f191 | ||
|
|
17f64462d2 | ||
|
|
bbec13c0da | ||
|
|
466ad1605b | ||
|
|
a068c350ee | ||
|
|
0605221628 | ||
|
|
3856c4d0f9 | ||
|
|
557a9d020a | ||
|
|
14d7cb812e | ||
|
|
c49a9e877c | ||
|
|
b08d653e02 | ||
|
|
0928e408c0 | ||
|
|
032ed73c0d | ||
|
|
c5bbee354f | ||
|
|
fe7ce3f74b | ||
|
|
63acd585ba | ||
|
|
9a1cf70451 | ||
|
|
1777f257a8 | ||
|
|
b4997da51c | ||
|
|
4f9aff3043 | ||
|
|
e68317c6bd |
56
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
56
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: Issue Report
|
||||
description: "Create a report to help us improve."
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Welcome
|
||||
options:
|
||||
- label: Yes, I'm using the latest major release. Only such installations are supported.
|
||||
required: true
|
||||
- label: Yes, I'm using the supported system. Only such systems are supported.
|
||||
required: true
|
||||
- label: Yes, I have read all WIKI document,nothing can help me in my problem.
|
||||
required: true
|
||||
- label: Yes, I've searched similar issues on GitHub and didn't find any.
|
||||
required: true
|
||||
- label: Yes, I've included all information below (version, config, log, etc).
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Description of the problem,screencshot would be good
|
||||
placeholder: Your problem description
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: version
|
||||
attributes:
|
||||
label: Version of 3x-ui
|
||||
value: |-
|
||||
<details>
|
||||
|
||||
```console
|
||||
# Paste here
|
||||
```
|
||||
|
||||
</details>
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: log
|
||||
attributes:
|
||||
label: x-ui log reports or xray log
|
||||
value: |-
|
||||
<details>
|
||||
|
||||
```console
|
||||
# paste log here
|
||||
```
|
||||
|
||||
</details>
|
||||
validations:
|
||||
required: true
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
10
.github/ISSUE_TEMPLATE/question-.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/question-.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: 'Question '
|
||||
about: Describe this issue template's purpose here.
|
||||
title: ''
|
||||
labels: question
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -6,7 +6,7 @@ jobs:
|
||||
name: build x-ui amd64 version
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3.4.0
|
||||
- uses: actions/checkout@v3.5.0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4.0.0
|
||||
with:
|
||||
|
||||
@@ -15,7 +15,11 @@ xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese)**
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
|
||||
```
|
||||
|
||||
## Install custom version
|
||||
To install your desired version you can add the version to the end of install command. Example for ver `v1.0.9`:
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v1.0.9
|
||||
```
|
||||
# SSL
|
||||
```
|
||||
apt-get install certbot -y
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.1.0
|
||||
1.1.3
|
||||
|
||||
2
go.mod
2
go.mod
@@ -16,7 +16,7 @@ require (
|
||||
github.com/xtls/xray-core v1.8.0
|
||||
go.uber.org/atomic v1.10.0
|
||||
golang.org/x/text v0.8.0
|
||||
google.golang.org/grpc v1.53.0
|
||||
google.golang.org/grpc v1.54.0
|
||||
gorm.io/driver/sqlite v1.4.4
|
||||
gorm.io/gorm v1.24.6
|
||||
)
|
||||
|
||||
4
go.sum
4
go.sum
@@ -235,8 +235,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 h1:DdoeryqhaXp1LtT/emMP1BRJPHHKFi5akj/nbx/zNTA=
|
||||
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s=
|
||||
google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
|
||||
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
|
||||
google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=
|
||||
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM=
|
||||
|
||||
@@ -98,8 +98,8 @@ config_after_install() {
|
||||
/usr/local/x-ui/x-ui setting -username ${usernameTemp} -password ${passwordTemp}
|
||||
echo -e "this is a fresh installation,will generate random login info for security concerns:"
|
||||
echo -e "###############################################"
|
||||
echo -e "${green}user name:${usernameTemp}${plain}"
|
||||
echo -e "${green}user password:${passwordTemp}${plain}"
|
||||
echo -e "${green}username:${usernameTemp}${plain}"
|
||||
echo -e "${green}password:${passwordTemp}${plain}"
|
||||
echo -e "###############################################"
|
||||
echo -e "${red}if you forgot your login info,you can type x-ui and then type 7 to check after installation${plain}"
|
||||
else
|
||||
|
||||
@@ -156,6 +156,16 @@
|
||||
padding:16px;
|
||||
}
|
||||
|
||||
.ant-menu-dark,
|
||||
.ant-menu-dark .ant-menu-sub,
|
||||
.ant-layout-header,
|
||||
.ant-layout-sider-dark,
|
||||
.ant-layout-sider-zero-width-trigger,
|
||||
.ant-dropdown-menu-dark,.ant-dropdown-menu-dark .ant-dropdown-menu,
|
||||
.ant-menu-dark.ant-menu-horizontal>.ant-menu-item,.ant-menu-dark.ant-menu-horizontal>.ant-menu-submenu {
|
||||
background:#161b22
|
||||
}
|
||||
|
||||
.ant-card-dark {
|
||||
color: hsla(0,0%,100%,.65);
|
||||
background-color: #1a212a;
|
||||
@@ -178,9 +188,10 @@
|
||||
|
||||
.ant-card-dark .ant-collapse-content,
|
||||
.ant-card-dark .ant-calendar,
|
||||
.ant-card-dark .ant-table-placeholder {
|
||||
.ant-card-dark .ant-table-placeholder,
|
||||
.ant-card-dark .ant-input-group-addon {
|
||||
color: hsla(0,0%,100%,.65);
|
||||
background-color: #1a212a;
|
||||
background-color: #262f3d;
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-list-item-meta-title,
|
||||
@@ -198,7 +209,8 @@
|
||||
.ant-card-dark .ant-calendar-year-select,
|
||||
.ant-card-dark .ant-calendar-date,
|
||||
.ant-card-dark .ant-collapse>.ant-collapse-item>.ant-collapse-header,
|
||||
.ant-card-dark .ant-empty-normal {
|
||||
.ant-card-dark .ant-empty-normal,
|
||||
.ant-card-dark .ant-checkbox+span {
|
||||
color: hsla(0,0%,100%,.65);
|
||||
}
|
||||
|
||||
@@ -210,7 +222,7 @@
|
||||
|
||||
.ant-card-dark tbody .ant-table-expanded-row {
|
||||
color: hsla(0,0%,100%,.65);
|
||||
background-color: #023366;
|
||||
background-color: #1a212a;
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-input,
|
||||
@@ -219,7 +231,7 @@
|
||||
.ant-card-dark .ant-select-dropdown-menu-item-selected,
|
||||
.ant-card-dark .ant-select-selection {
|
||||
color: hsla(0,0%,100%,.65);
|
||||
background-color: #023366;
|
||||
background-color: #2e3b52;
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-collapse-item {
|
||||
@@ -232,7 +244,7 @@
|
||||
.ant-card-dark .ant-modal-header,
|
||||
.ant-card-dark .ant-calendar-selected-day .ant-calendar-date {
|
||||
color: hsla(0,0%,100%,.65);
|
||||
background-color: #242c3a;
|
||||
background-color: #222a37;
|
||||
}
|
||||
|
||||
.client-table-header {
|
||||
@@ -244,7 +256,7 @@
|
||||
}
|
||||
|
||||
.ant-card-dark .client-table-header {
|
||||
background-color: #023366;
|
||||
background-color: #1a212a;
|
||||
color: hsla(0,0%,100%,.65);
|
||||
}
|
||||
|
||||
@@ -266,4 +278,62 @@
|
||||
.ant-drawer-dark .drawer-handle {
|
||||
background-color: #1a212a;
|
||||
border: 1px solid hsla(0,0%,100%,.30);
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-tag-blue {
|
||||
color: #3c9ae8;
|
||||
background: #111d2c;
|
||||
border-color: #15395b;
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-tag-green {
|
||||
color: #6abe39;
|
||||
background: #162312;
|
||||
border-color: #274916;
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-tag-cyan {
|
||||
color: #33bcb7;
|
||||
background: #112123;
|
||||
border-color: #144848;
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-tag-red {
|
||||
color: #e84749;
|
||||
background: #2a1215;
|
||||
border-color: #58181c;
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-tag-orange {
|
||||
color: #e89a3c;
|
||||
background: #2b1d11;
|
||||
border-color: #593815;
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-table-row-expand-icon,
|
||||
.ant-card-dark .ant-checkbox-inner {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-switch-checked {
|
||||
background-color: #0c61b0;
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-btn,
|
||||
.ant-card-dark .ant-radio-button-wrapper {
|
||||
color: hsla(0,0%,100%,.65);
|
||||
background: none;
|
||||
border: 1px solid hsla(0,0%,100%,.65);
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-radio-button-wrapper:hover {
|
||||
color: #177ddc;
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-btn-primary {
|
||||
color: hsla(0,0%,100%,.65);
|
||||
background-color: #073763;
|
||||
border-color: #1890ff;
|
||||
text-shadow: 0 -1px 0 rgba(0,0,0,.12);
|
||||
box-shadow: 0 2px 0 rgba(0,0,0,.045);
|
||||
}
|
||||
@@ -95,7 +95,6 @@ const UTLS_FINGERPRINT = {
|
||||
const ALPN_OPTION = {
|
||||
H2: "h2",
|
||||
HTTP1: "http/1.1",
|
||||
BOTH: "h2,http/1.1",
|
||||
};
|
||||
|
||||
Object.freeze(Protocols);
|
||||
@@ -476,7 +475,7 @@ class GrpcStreamSettings extends XrayCommonClass {
|
||||
}
|
||||
|
||||
class TlsStreamSettings extends XrayCommonClass {
|
||||
constructor(serverName = '', minVersion = TLS_VERSION_OPTION.TLS12, maxVersion = TLS_VERSION_OPTION.TLS13,
|
||||
constructor(serverName = '', minVersion = TLS_VERSION_OPTION.TLS10, maxVersion = TLS_VERSION_OPTION.TLS12,
|
||||
cipherSuites = '',
|
||||
certificates = [new TlsStreamSettings.Cert()], alpn=[''] ,settings=[new TlsStreamSettings.Settings()]) {
|
||||
super();
|
||||
@@ -575,9 +574,9 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
|
||||
};
|
||||
|
||||
TlsStreamSettings.Settings = class extends XrayCommonClass {
|
||||
constructor(insecure = false, fingerprint = '', serverName = '') {
|
||||
constructor(allowInsecure = false, fingerprint = '', serverName = '') {
|
||||
super();
|
||||
this.inSecure = insecure;
|
||||
this.allowInsecure = allowInsecure;
|
||||
this.fingerprint = fingerprint;
|
||||
this.serverName = serverName;
|
||||
}
|
||||
@@ -590,7 +589,7 @@ TlsStreamSettings.Settings = class extends XrayCommonClass {
|
||||
}
|
||||
toJson() {
|
||||
return {
|
||||
allowInsecure: this.inSecure,
|
||||
allowInsecure: this.allowInsecure,
|
||||
fingerprint: this.fingerprint,
|
||||
serverName: this.serverName,
|
||||
};
|
||||
@@ -1084,7 +1083,8 @@ class Inbound extends XrayCommonClass {
|
||||
tls: this.stream.security,
|
||||
sni: this.stream.tls.settings[0]['serverName'],
|
||||
fp: this.stream.tls.settings[0]['fingerprint'],
|
||||
alpn: this.stream.tls.alpn[0],
|
||||
alpn: this.stream.tls.alpn.join(','),
|
||||
allowInsecure: this.stream.tls.settings[0].allowInsecure,
|
||||
};
|
||||
return 'vmess://' + base64(JSON.stringify(obj, null, 2));
|
||||
}
|
||||
@@ -1096,7 +1096,6 @@ class Inbound extends XrayCommonClass {
|
||||
const type = this.stream.network;
|
||||
const params = new Map();
|
||||
params.set("type", this.stream.network);
|
||||
params.set("security", this.stream.security);
|
||||
switch (type) {
|
||||
case "tcp":
|
||||
const tcp = this.stream.tcp;
|
||||
@@ -1143,8 +1142,12 @@ class Inbound extends XrayCommonClass {
|
||||
}
|
||||
|
||||
if (this.tls) {
|
||||
params.set("security", "tls");
|
||||
params.set("fp" , this.stream.tls.settings[0]['fingerprint']);
|
||||
params.set("alpn", this.stream.tls.alpn[0]);
|
||||
params.set("alpn", this.stream.tls.alpn);
|
||||
if(this.stream.tls.settings[0].allowInsecure){
|
||||
params.set("allowInsecure", "1");
|
||||
}
|
||||
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
|
||||
address = this.stream.tls.server;
|
||||
}
|
||||
@@ -1156,14 +1159,17 @@ class Inbound extends XrayCommonClass {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.xtls) {
|
||||
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
|
||||
address = this.stream.tls.server;
|
||||
if (type === "tcp") {
|
||||
params.set("flow", this.settings.vlesses[clientIndex].flow);
|
||||
}
|
||||
if (this.XTLS) {
|
||||
params.set("security", "xtls");
|
||||
params.set("alpn", this.stream.tls.alpn);
|
||||
if(this.stream.tls.settings[0].allowInsecure){
|
||||
params.set("allowInsecure", "1");
|
||||
}
|
||||
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
|
||||
address = this.stream.tls.server;
|
||||
}
|
||||
}
|
||||
params.set("flow", this.settings.vlesses[clientIndex].flow);
|
||||
}
|
||||
|
||||
const link = `vless://${uuid}@${address}:${port}`;
|
||||
const url = new URL(link);
|
||||
@@ -1194,7 +1200,6 @@ class Inbound extends XrayCommonClass {
|
||||
const type = this.stream.network;
|
||||
const params = new Map();
|
||||
params.set("type", this.stream.network);
|
||||
params.set("security", this.stream.security);
|
||||
switch (type) {
|
||||
case "tcp":
|
||||
const tcp = this.stream.tcp;
|
||||
@@ -1241,24 +1246,31 @@ class Inbound extends XrayCommonClass {
|
||||
}
|
||||
|
||||
if (this.tls) {
|
||||
params.set("security", "tls");
|
||||
params.set("fp" , this.stream.tls.settings[0]['fingerprint']);
|
||||
params.set("alpn", this.stream.tls.alpn[0]);
|
||||
params.set("alpn", this.stream.tls.alpn);
|
||||
if(this.stream.tls.settings[0].allowInsecure){
|
||||
params.set("allowInsecure", "1");
|
||||
}
|
||||
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
|
||||
address = this.stream.tls.server;
|
||||
}
|
||||
if (this.stream.tls.settings[0]['serverName'] !== ''){
|
||||
params.set("sni", this.stream.tls.settings[0]['serverName']);
|
||||
}
|
||||
if (this.stream.tls.settings[0]['serverName'] !== ''){
|
||||
params.set("sni", this.stream.tls.settings[0]['serverName']);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.xtls) {
|
||||
if (this.XTLS) {
|
||||
params.set("security", "xtls");
|
||||
params.set("alpn", this.stream.tls.alpn);
|
||||
if(this.stream.tls.settings[0].allowInsecure){
|
||||
params.set("allowInsecure", "1");
|
||||
}
|
||||
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
|
||||
address = this.stream.tls.server;
|
||||
if (type === "tcp" && this.settings.trojans[clientIndex].flow.length > 0) {
|
||||
params.set("flow", this.settings.trojans[clientIndex].flow);
|
||||
}
|
||||
}
|
||||
}
|
||||
address = this.stream.tls.server;
|
||||
}
|
||||
params.set("flow", this.settings.trojans[clientIndex].flow);
|
||||
}
|
||||
|
||||
const link = `trojan://${settings.trojans[clientIndex].password}@${address}:${this.port}#${encodeURIComponent(remark)}`;
|
||||
const url = new URL(link);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"time"
|
||||
"x-ui/web/global"
|
||||
"x-ui/web/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ServerController struct {
|
||||
@@ -37,6 +38,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
|
||||
g.POST("/stopXrayService", a.stopXrayService)
|
||||
g.POST("/restartXrayService", a.restartXrayService)
|
||||
g.POST("/installXray/:version", a.installXray)
|
||||
g.POST("/logs", a.getLogs)
|
||||
}
|
||||
|
||||
func (a *ServerController) refreshStatus() {
|
||||
@@ -87,13 +89,13 @@ func (a *ServerController) installXray(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (a *ServerController) stopXrayService(c *gin.Context) {
|
||||
a.lastGetStatusTime = time.Now()
|
||||
a.lastGetStatusTime = time.Now()
|
||||
err := a.serverService.StopXrayService()
|
||||
if err != nil {
|
||||
jsonMsg(c, "", err)
|
||||
return
|
||||
}
|
||||
jsonMsg(c, "Xray stoped",err)
|
||||
jsonMsg(c, "Xray stoped", err)
|
||||
|
||||
}
|
||||
func (a *ServerController) restartXrayService(c *gin.Context) {
|
||||
@@ -102,6 +104,15 @@ func (a *ServerController) restartXrayService(c *gin.Context) {
|
||||
jsonMsg(c, "", err)
|
||||
return
|
||||
}
|
||||
jsonMsg(c, "Xray restarted",err)
|
||||
jsonMsg(c, "Xray restarted", err)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ServerController) getLogs(c *gin.Context) {
|
||||
logs, err := a.serverService.GetLogs()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "getLogs"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, logs, nil)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{{define "qrcodeModal"}}
|
||||
<a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title"
|
||||
:closable="true" width="300px" :ok-text="qrModal.okText"
|
||||
:closable="true"
|
||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||
cancel-text='{{ i18n "close" }}' :ok-button-props="{attrs:{id:'qr-modal-ok-btn'}}">
|
||||
<a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;" >{{ i18n "pages.inbounds.clickOnQRcode" }}</a-tag>
|
||||
:footer="null"
|
||||
width="300px">
|
||||
<a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;" >{{ i18n "pages.inbounds.clickOnQRcode" }}</a-tag>
|
||||
<canvas @click="copyToClipboard()" id="qrCode" style="width: 100%; height: 100%;"></canvas>
|
||||
</a-modal>
|
||||
|
||||
@@ -14,17 +15,15 @@
|
||||
content: '',
|
||||
inbound: new Inbound(),
|
||||
dbInbound: new DBInbound(),
|
||||
okText: '',
|
||||
copyText: '',
|
||||
qrcode: null,
|
||||
clipboard: null,
|
||||
visible: false,
|
||||
show: function (title='', content='', dbInbound=new DBInbound(),okText='{{ i18n "copy" }}', copyText='') {
|
||||
show: function (title='', content='', dbInbound=new DBInbound(), copyText='') {
|
||||
this.title = title;
|
||||
this.content = content;
|
||||
this.dbInbound = dbInbound;
|
||||
this.inbound = dbInbound.toInbound();
|
||||
this.okText = okText;
|
||||
if (ObjectUtil.isEmpty(copyText)) {
|
||||
this.copyText = content;
|
||||
} else {
|
||||
@@ -32,13 +31,6 @@
|
||||
}
|
||||
this.visible = true;
|
||||
qrModalApp.$nextTick(() => {
|
||||
this.clipboard = new ClipboardJS('#qr-modal-ok-btn', {
|
||||
text: () => this.copyText,
|
||||
});
|
||||
this.clipboard.on('success', () => {
|
||||
app.$message.success('{{ i18n "copied" }}')
|
||||
this.clipboard.destroy();
|
||||
});
|
||||
if (this.qrcode === null) {
|
||||
this.qrcode = new QRious({
|
||||
element: document.querySelector('#qrCode'),
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
<a-form-item label='{{ i18n "pages.client.method" }}'>
|
||||
<a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 350px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<a-select-option :value="0">Random</a-select-option>
|
||||
<a-select-option :value="1">Random_Prefix</a-select-option>
|
||||
<a-select-option :value="2">Random_Prefix+Num</a-select-option>
|
||||
<a-select-option :value="3">Random_Prefix+Num+Postfix</a-select-option>
|
||||
<a-select-option :value="4">Random_Prefix+Num@Telegram Username</a-select-option>
|
||||
<a-select-option :value="1">Random+Prefix</a-select-option>
|
||||
<a-select-option :value="2">Random+Prefix+Num</a-select-option>
|
||||
<a-select-option :value="3">Random+Prefix+Num+Postfix</a-select-option>
|
||||
<a-select-option :value="4">Random+Prefix+Num@Telegram Username</a-select-option>
|
||||
<a-select-option :value="5">Prefix+Num+Postfix [ BE CAREFUL! ]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item><br />
|
||||
<a-form-item v-if="clientsBulkModal.emailMethod>1">
|
||||
@@ -91,11 +92,12 @@
|
||||
start=0;
|
||||
end=clientsBulkModal.quantity;
|
||||
}
|
||||
prefix = (method>0 && clientsBulkModal.emailPrefix.length>0) ? "_" + clientsBulkModal.emailPrefix : "";
|
||||
prefix = (method>0 && clientsBulkModal.emailPrefix.length>0) ? clientsBulkModal.emailPrefix : "";
|
||||
useNum=(method>1);
|
||||
postfix = (method>2 && clientsBulkModal.emailPostfix.length>0) ? (method == 4 ? "@" : "") + clientsBulkModal.emailPostfix : "";
|
||||
for (let i = start; i < end; i++) {
|
||||
newClient = clientsBulkModal.newClient(clientsBulkModal.dbInbound.protocol);
|
||||
if(method==5) newClient.email = "";
|
||||
newClient.email += useNum ? prefix + i.toString() + postfix : prefix + postfix;
|
||||
newClient._totalGB = clientsBulkModal.totalGB;
|
||||
newClient._expiryTime = clientsBulkModal.expiryTime;
|
||||
|
||||
@@ -15,20 +15,6 @@
|
||||
<!-- <a-icon type="laptop"></a-icon>-->
|
||||
<!-- <span>Client</span>-->
|
||||
<!--</a-menu-item>-->
|
||||
<a-sub-menu>
|
||||
<template slot="title">
|
||||
<a-icon type="link"></a-icon>
|
||||
<span>{{ i18n "menu.link"}}</span>
|
||||
</template>
|
||||
<a-menu-item key="https://github.com/mhsanaei/3x-ui/">
|
||||
<a-icon type="github"></a-icon>
|
||||
<span>Github</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="https://t.me/panel3xui">
|
||||
<a-icon type="usergroup-add"></a-icon>
|
||||
<span>Telegram</span>
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
<a-menu-item key="{{ .base_path }}logout">
|
||||
<a-icon type="logout"></a-icon>
|
||||
<span>{{ i18n "menu.logout"}}</span>
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
</table>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<template v-if="inbound.isTcp && inbound.tls">
|
||||
<template v-if="inbound.isTcp && (inbound.tls || inbound.xtls)">
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="Fallbacks">
|
||||
<a-row>
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
</table>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<template v-if="inbound.isTcp && inbound.tls">
|
||||
<template v-if="inbound.isTcp && (inbound.tls || inbound.xtls)">
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="Fallbacks">
|
||||
<a-row>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<a-select-option value="ws">WS</a-select-option>
|
||||
<a-select-option value="http">HTTP</a-select-option>
|
||||
<a-select-option value="quic">QUIC</a-select-option>
|
||||
<a-select-option value="grpc">GRPC</a-select-option>
|
||||
<a-select-option value="grpc">gRPC</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
@@ -40,11 +40,13 @@
|
||||
<a-form-item label='{{ i18n "domainName" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tls.server"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Alpn" v-if="inbound.tls">
|
||||
<a-select v-model="inbound.stream.tls.alpn[0]" style="width:200px">
|
||||
<a-select-option value=''>auto</a-select-option>
|
||||
<a-select-option v-for="key in ALPN_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
<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-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="Allow insecure">
|
||||
<a-switch v-model="inbound.stream.tls.settings[0].allowInsecure"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "certificate" }}'>
|
||||
<a-radio-group v-model="inbound.stream.tls.certs[0].useFile" button-style="solid">
|
||||
|
||||
@@ -59,13 +59,11 @@
|
||||
</table>
|
||||
<template v-if="infoModal.clientSettings">
|
||||
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
|
||||
<table style="margin-bottom: 10px; width: 100%;">
|
||||
<tr>
|
||||
<th v-for="col in Object.keys(infoModal.clientSettings).slice(0, 3)">[[ col ]]</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td v-for="col in Object.values(infoModal.clientSettings).slice(0, 3)"><a-tag color="green">[[ col ]]</a-tag></td>
|
||||
</table>
|
||||
<table style="margin-bottom: 10px;">
|
||||
<tr v-for="col,index in Object.keys(infoModal.clientSettings).slice(0, 3)">
|
||||
<td>[[ col ]]</td>
|
||||
<td><a-tag color="green">[[ infoModal.clientSettings[col] ]]</a-tag></td>
|
||||
</table>
|
||||
<table style="margin-bottom: 10px; width: 100%;">
|
||||
<tr><th>{{ i18n "usage" }}</th><th>{{ i18n "pages.inbounds.totalFlow" }}</th><th>{{ i18n "pages.inbounds.expireDate" }}</th><th>{{ i18n "enable" }}</th></tr>
|
||||
<tr>
|
||||
|
||||
@@ -11,10 +11,6 @@
|
||||
.ant-col-sm-24 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.ant-table-row-expand-icon {
|
||||
color: rgba(0,0,0,.65);
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<a-layout id="app" v-cloak>
|
||||
@@ -56,6 +52,7 @@
|
||||
<div slot="title">
|
||||
<a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button>
|
||||
<a-button type="primary" icon="export" @click="exportAllLinks">{{ i18n "pages.inbounds.export" }}</a-button>
|
||||
<a-button type="primary" icon="reload" @click="resetAllTraffic">{{ i18n "pages.inbounds.resetAllTraffic" }}</a-button>
|
||||
</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"
|
||||
@@ -99,6 +96,9 @@
|
||||
</template>
|
||||
<a-menu-item key="resetTraffic">
|
||||
<a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="clone">
|
||||
<a-icon type="block"></a-icon> {{ i18n "pages.inbounds.Clone"}}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="delete">
|
||||
<span style="color: #FF4D4F">
|
||||
@@ -314,11 +314,47 @@
|
||||
break;
|
||||
case "resetTraffic":
|
||||
this.resetTraffic(dbInbound.id);
|
||||
break;
|
||||
case "clone":
|
||||
this.openCloneInbound(dbInbound);
|
||||
break;
|
||||
case "delete":
|
||||
this.delInbound(dbInbound.id);
|
||||
break;
|
||||
}
|
||||
},
|
||||
openCloneInbound(dbInbound) {
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.cloneInbound"}} ' + dbInbound.remark,
|
||||
content: '{{ i18n "pages.inbounds.cloneInboundContent"}}',
|
||||
okText: '{{ i18n "pages.inbounds.cloneInboundOk"}}',
|
||||
cancelText: '{{ i18n "cancel" }}',
|
||||
onOk: () => {
|
||||
const baseInbound = dbInbound.toInbound();
|
||||
dbInbound.up = 0;
|
||||
dbInbound.down = 0;
|
||||
this.cloneInbound(baseInbound, dbInbound);
|
||||
},
|
||||
});
|
||||
},
|
||||
async cloneInbound(baseInbound, dbInbound) {
|
||||
const inbound = new Inbound();
|
||||
const data = {
|
||||
up: dbInbound.up,
|
||||
down: dbInbound.down,
|
||||
total: dbInbound.total,
|
||||
remark: dbInbound.remark + " - Cloned",
|
||||
enable: dbInbound.enable,
|
||||
expiryTime: dbInbound.expiryTime,
|
||||
|
||||
listen: inbound.listen,
|
||||
port: inbound.port,
|
||||
protocol: baseInbound.protocol,
|
||||
settings: inbound.settings.toString(),
|
||||
streamSettings: baseInbound.stream.toString(),
|
||||
sniffing: baseInbound.canSniffing() ? baseInbound.sniffing.toString() : '{}',
|
||||
};
|
||||
await this.submit('/xui/inbound/add', data, inModal);
|
||||
},
|
||||
openAddInbound() {
|
||||
inModal.show({
|
||||
@@ -465,6 +501,22 @@
|
||||
this.updateInbound(inbound, dbInbound);
|
||||
},
|
||||
});
|
||||
},
|
||||
resetAllTraffic() {
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.resetAllTrafficTitle"}}',
|
||||
content: '{{ i18n "pages.inbounds.resetAllTrafficContent"}}',
|
||||
okText: '{{ i18n "pages.inbounds.resetAllTrafficOkText"}}',
|
||||
cancelText: '{{ i18n "pages.inbounds.resetAllTrafficCancelText"}}',
|
||||
onOk: async () => {
|
||||
for (const dbInbound of this.dbInbounds) {
|
||||
const inbound = dbInbound.toInbound();
|
||||
dbInbound.up = 0;
|
||||
dbInbound.down = 0;
|
||||
this.updateInbound(inbound, dbInbound);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
delInbound(dbInboundId) {
|
||||
this.$confirm({
|
||||
|
||||
@@ -84,16 +84,16 @@
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-tag color="green" @click="openSelectV2rayVersion">[[ status.xray.version ]]</a-tag>
|
||||
<a-tag color="blue" @click="stopXrayService">{{ i18n "pages.index.stopXray" }}</a-tag>
|
||||
<a-tag color="blue" @click="restartXrayService">{{ i18n "pages.index.restartXray" }}</a-tag>
|
||||
<a-tag color="blue" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch" }}</a-tag>
|
||||
<a-tag color="green" style="cursor: pointer;" @click="openSelectV2rayVersion">[[ status.xray.version ]]</a-tag>
|
||||
<a-tag color="blue" style="cursor: pointer;" @click="stopXrayService">{{ i18n "pages.index.stopXray" }}</a-tag>
|
||||
<a-tag color="blue" style="cursor: pointer;" @click="restartXrayService">{{ i18n "pages.index.restartXray" }}</a-tag>
|
||||
<a-tag color="blue" style="cursor: pointer;" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch" }}</a-tag>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
||||
{{ i18n "pages.index.operationHours" }}:
|
||||
<a-tag color="#87d068">[[ formatSecond(status.uptime) ]]</a-tag>
|
||||
<a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.operationHoursDesc" }}
|
||||
@@ -169,6 +169,13 @@
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
||||
3x-ui: <a href="https://github.com/MHSanaei/3x-ui/releases" target="_blank"><a-tag color="green">v{{ .cur_ver }}</a-tag></a>
|
||||
<a href="https://t.me/panel3xui" target="_blank"><a-tag color="green">Telegram</a-tag></a>
|
||||
<a-tag color="blue" style="cursor: pointer;" @click="openLogs">Log Reports</a-tag>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</transition>
|
||||
@@ -177,7 +184,7 @@
|
||||
<a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}'
|
||||
:closable="true" @ok="() => versionModal.visible = false"
|
||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||
ok-text='{{ i18n "confirm" }}' cancel-text='{{ i18n "cancel"}}'>
|
||||
footer="">
|
||||
<h2>{{ i18n "pages.index.xraySwitchClick"}}</h2>
|
||||
<h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2>
|
||||
<template v-for="version, index in versionModal.versions">
|
||||
@@ -187,6 +194,17 @@
|
||||
</a-tag>
|
||||
</template>
|
||||
</a-modal>
|
||||
<a-modal id="log-modal" v-model="logModal.visible" title="X-UI logs"
|
||||
:closable="true" @ok="() => logModal.visible = false" @cancel="() => logModal.visible = false"
|
||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||
width="800px"
|
||||
footer="">
|
||||
<table style="margin: 0px; width: 100%; background-color: black; color: hsla(0,0%,100%,.65);">
|
||||
<tr v-for="log , index in logModal.logs">
|
||||
<td style="vertical-align: top;">[[ index ]]</td><td>[[ log ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-modal>
|
||||
</a-layout>
|
||||
{{template "js" .}}
|
||||
<script>
|
||||
@@ -280,6 +298,18 @@
|
||||
},
|
||||
};
|
||||
|
||||
const logModal = {
|
||||
visible: false,
|
||||
logs: '',
|
||||
show(logs) {
|
||||
this.visible = true;
|
||||
this.logs = logs;
|
||||
},
|
||||
hide() {
|
||||
this.visible = false;
|
||||
},
|
||||
};
|
||||
|
||||
const app = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#app',
|
||||
@@ -287,6 +317,7 @@
|
||||
siderDrawer,
|
||||
status: new Status(),
|
||||
versionModal,
|
||||
logModal,
|
||||
spinning: false,
|
||||
loadingTip: '{{ i18n "loading"}}',
|
||||
},
|
||||
@@ -346,6 +377,15 @@
|
||||
return;
|
||||
}
|
||||
},
|
||||
async openLogs(){
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post('server/logs');
|
||||
this.loading(false);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
logModal.show(msg.obj);
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
while (true) {
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
|
||||
<setting-list-item type="text" title='{{ i18n "pages.setting.panelListeningIP"}}' desc='{{ i18n "pages.setting.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.setting.panelPort"}}' desc='{{ i18n "pages.setting.panelPortDesc"}}' v-model.number="allSetting.webPort"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.setting.panelPort"}}' desc='{{ i18n "pages.setting.panelPortDesc"}}' v-model.number="allSetting.webPort"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.setting.publicKeyPath"}}' desc='{{ i18n "pages.setting.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.setting.privateKeyPath"}}' desc='{{ i18n "pages.setting.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.setting.panelUrlPath"}}' desc='{{ i18n "pages.setting.panelUrlPathDesc"}}' v-model="allSetting.webBasePath"></setting-list-item>
|
||||
@@ -117,7 +117,7 @@
|
||||
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.setting.telegramBotEnable" }}' desc='{{ i18n "pages.setting.telegramBotEnableDesc" }}' v-model="allSetting.tgBotEnable"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramToken"}}' desc='{{ i18n "pages.setting.telegramTokenDesc"}}' v-model="allSetting.tgBotToken"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.setting.telegramChatId"}}' desc='{{ i18n "pages.setting.telegramChatIdDesc"}}' v-model.number="allSetting.tgBotChatId"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramChatId"}}' desc='{{ i18n "pages.setting.telegramChatIdDesc"}}' v-model="allSetting.tgBotChatId"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramNotifyTime"}}' desc='{{ i18n "pages.setting.telegramNotifyTimeDesc"}}' v-model="allSetting.tgRunTime"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.setting.tgNotifyBackup" }}' desc='{{ i18n "pages.setting.tgNotifyBackupDesc" }}' v-model="allSetting.tgBotBackup"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.setting.tgNotifyExpireTimeDiff" }}' desc='{{ i18n "pages.setting.tgNotifyExpireTimeDiffDesc" }}' v-model="allSetting.tgExpireDiff" :min="0"></setting-list-item>
|
||||
|
||||
@@ -154,14 +154,16 @@ func GetInboundClientIps(clientEmail string) (*model.InboundClientIps, error) {
|
||||
}
|
||||
return InboundClientIps, nil
|
||||
}
|
||||
func addInboundClientIps(clientEmail string,ips []string) error {
|
||||
func addInboundClientIps(clientEmail string, ips []string) error {
|
||||
inboundClientIps := &model.InboundClientIps{}
|
||||
jsonIps, err := json.Marshal(ips)
|
||||
jsonIps, err := json.Marshal(ips)
|
||||
checkError(err)
|
||||
|
||||
// Trim any leading/trailing whitespace from clientEmail
|
||||
clientEmail = strings.TrimSpace(clientEmail)
|
||||
|
||||
inboundClientIps.ClientEmail = clientEmail
|
||||
inboundClientIps.Ips = string(jsonIps)
|
||||
|
||||
|
||||
db := database.GetDB()
|
||||
tx := db.Begin()
|
||||
@@ -247,47 +249,46 @@ func GetInboundByEmail(clientEmail string) (*model.Inbound, error) {
|
||||
return inbounds, nil
|
||||
}
|
||||
|
||||
func LimitDevice(){
|
||||
|
||||
localIp,err := LocalIP()
|
||||
checkError(err)
|
||||
func LimitDevice() {
|
||||
var destIp, destPort, srcIp, srcPort string
|
||||
|
||||
localIp,err := LocalIP()
|
||||
checkError(err)
|
||||
|
||||
c := cmd.NewCmd("bash","-c","ss --tcp | grep -E '" + IPsToRegex(localIp) + "'| awk '{if($1==\"ESTAB\") print $4,$5;}'","| sort | uniq -c | sort -nr | head")
|
||||
c := cmd.NewCmd("bash","-c","ss --tcp | grep -E '" + IPsToRegex(localIp) + "'| awk '{if($1==\"ESTAB\") print $4,$5;}'","| sort | uniq -c | sort -nr | head")
|
||||
|
||||
<-c.Start()
|
||||
if len(c.Status().Stdout) > 0 {
|
||||
ipRegx, _ := regexp.Compile(`[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`)
|
||||
portRegx, _ := regexp.Compile(`(?:(:))([0-9]..[^.][0-9]+)`)
|
||||
<-c.Start()
|
||||
if len(c.Status().Stdout) > 0 {
|
||||
ipRegx, _ := regexp.Compile(`[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`)
|
||||
portRegx, _ := regexp.Compile(`(?:(:))([0-9]..[^.][0-9]+)`)
|
||||
|
||||
for _, row := range c.Status().Stdout {
|
||||
|
||||
data := strings.Split(row," ")
|
||||
|
||||
destIp,destPort,srcIp,srcPort := "","","",""
|
||||
|
||||
for _, row := range c.Status().Stdout {
|
||||
|
||||
destIp = string(ipRegx.FindString(data[0]))
|
||||
data := strings.Split(row," ")
|
||||
|
||||
destPort = portRegx.FindString(data[0])
|
||||
destPort = strings.Replace(destPort,":","",-1)
|
||||
|
||||
|
||||
srcIp = string(ipRegx.FindString(data[1]))
|
||||
if len(data) < 2 {
|
||||
continue // Skip this row if it doesn't have at least two elements
|
||||
}
|
||||
|
||||
srcPort = portRegx.FindString(data[1])
|
||||
srcPort = strings.Replace(srcPort,":","",-1)
|
||||
destIp = string(ipRegx.FindString(data[0]))
|
||||
destPort = portRegx.FindString(data[0])
|
||||
destPort = strings.Replace(destPort,":","",-1)
|
||||
|
||||
if(contains(disAllowedIps,srcIp)){
|
||||
dropCmd := cmd.NewCmd("bash","-c","ss -K dport = " + srcPort)
|
||||
dropCmd.Start()
|
||||
srcIp = string(ipRegx.FindString(data[1]))
|
||||
srcPort = portRegx.FindString(data[1])
|
||||
srcPort = strings.Replace(srcPort,":","",-1)
|
||||
|
||||
logger.Debug("request droped : ",srcIp,srcPort,"to",destIp,destPort)
|
||||
}
|
||||
}
|
||||
}
|
||||
if contains(disAllowedIps,srcIp){
|
||||
dropCmd := cmd.NewCmd("bash","-c","ss -K dport = " + srcPort)
|
||||
dropCmd.Start()
|
||||
|
||||
logger.Debug("request droped : ",srcIp,srcPort,"to",destIp,destPort)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func LocalIP() ([]string, error) {
|
||||
// get machine ips
|
||||
|
||||
|
||||
@@ -634,3 +634,13 @@ func (s *InboundService) ClearClientIps(clientEmail string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InboundService) SearchInbounds(query string) ([]*model.Inbound, error) {
|
||||
db := database.GetDB()
|
||||
var inbounds []*model.Inbound
|
||||
err := db.Model(model.Inbound{}).Preload("ClientStats").Where("remark like ?", "%"+query+"%").Find(&inbounds).Error
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return nil, err
|
||||
}
|
||||
return inbounds, nil
|
||||
}
|
||||
@@ -9,7 +9,9 @@ import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/sys"
|
||||
@@ -200,24 +202,24 @@ func (s *ServerService) GetXrayVersions() ([]string, error) {
|
||||
|
||||
func (s *ServerService) StopXrayService() (string error) {
|
||||
|
||||
err := s.xrayService.StopXray()
|
||||
if err != nil {
|
||||
logger.Error("stop xray failed:", err)
|
||||
return err
|
||||
}
|
||||
err := s.xrayService.StopXray()
|
||||
if err != nil {
|
||||
logger.Error("stop xray failed:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ServerService) RestartXrayService() (string error) {
|
||||
|
||||
s.xrayService.StopXray()
|
||||
defer func() {
|
||||
err := s.xrayService.RestartXray(true)
|
||||
if err != nil {
|
||||
logger.Error("start xray failed:", err)
|
||||
s.xrayService.StopXray()
|
||||
defer func() {
|
||||
err := s.xrayService.RestartXray(true)
|
||||
if err != nil {
|
||||
logger.Error("start xray failed:", err)
|
||||
}
|
||||
}()
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -324,3 +326,26 @@ func (s *ServerService) UpdateXray(version string) error {
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (s *ServerService) GetLogs() ([]string, error) {
|
||||
// Define the journalctl command and its arguments
|
||||
var cmdArgs []string
|
||||
if runtime.GOOS == "linux" {
|
||||
cmdArgs = []string{"journalctl", "-u", "x-ui", "--no-pager", "-n", "100"}
|
||||
} else {
|
||||
return []string{"Unsupported operating system"}, nil
|
||||
}
|
||||
|
||||
// Run the command
|
||||
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lines := strings.Split(out.String(), "\n")
|
||||
|
||||
return lines, nil
|
||||
}
|
||||
|
||||
@@ -105,8 +105,6 @@ func (t *Tgbot) OnReceive() {
|
||||
} else {
|
||||
if update.Message.IsCommand() {
|
||||
t.answerCommand(update.Message, chatId, isAdmin)
|
||||
} else {
|
||||
t.aswerChat(update.Message.Text, chatId, isAdmin)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,10 +126,20 @@ func (t *Tgbot) answerCommand(message *tgbotapi.Message, chatId int64, isAdmin b
|
||||
case "status":
|
||||
msg = "bot is ok ✅"
|
||||
case "usage":
|
||||
if isAdmin {
|
||||
t.searchClient(chatId, message.CommandArguments())
|
||||
if len(message.CommandArguments()) > 1 {
|
||||
if isAdmin {
|
||||
t.searchClient(chatId, message.CommandArguments())
|
||||
} else {
|
||||
t.searchForClient(chatId, message.CommandArguments())
|
||||
}
|
||||
} else {
|
||||
t.searchForClient(chatId, message.CommandArguments())
|
||||
msg = "❗Please provide a text for search!"
|
||||
}
|
||||
case "inbound":
|
||||
if isAdmin {
|
||||
t.searchInbound(chatId, message.CommandArguments())
|
||||
} else {
|
||||
msg = "❗ Unknown command"
|
||||
}
|
||||
default:
|
||||
msg = "❗ Unknown command"
|
||||
@@ -139,10 +147,6 @@ func (t *Tgbot) answerCommand(message *tgbotapi.Message, chatId int64, isAdmin b
|
||||
t.SendAnswer(chatId, msg, isAdmin)
|
||||
}
|
||||
|
||||
func (t *Tgbot) aswerChat(message string, chatId int64, isAdmin bool) {
|
||||
t.SendAnswer(chatId, "❗ Unknown message", isAdmin)
|
||||
}
|
||||
|
||||
func (t *Tgbot) asnwerCallback(callbackQuery *tgbotapi.CallbackQuery, isAdmin bool) {
|
||||
// Respond to the callback query, telling Telegram to show the user
|
||||
// a message with the data received.
|
||||
@@ -165,7 +169,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *tgbotapi.CallbackQuery, isAdmin bo
|
||||
case "client_commands":
|
||||
t.SendMsgToTgbot(callbackQuery.From.ID, "To search for statistics, just use folowing command:\r\n \r\n<code>/usage [UID|Passowrd]</code>\r\n \r\nUse UID for vmess and vless and Password for Trojan.")
|
||||
case "commands":
|
||||
t.SendMsgToTgbot(callbackQuery.From.ID, "To search for a client email, just use folowing command:\r\n \r\n<code>/usage email</code>")
|
||||
t.SendMsgToTgbot(callbackQuery.From.ID, "Search for a client email:\r\n<code>/usage email</code>\r\n \r\nSearch for inbounds (with client stats):\r\n<code>/inbound [remark]</code>")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,6 +276,7 @@ func (t *Tgbot) getServerUsage() string {
|
||||
name = ""
|
||||
}
|
||||
info = fmt.Sprintf("💻 Hostname: %s\r\n", name)
|
||||
info += fmt.Sprintf("🚀X-UI Version: %s\r\n", config.GetVersion())
|
||||
//get ip address
|
||||
var ip string
|
||||
var ipv6 string
|
||||
@@ -423,6 +428,45 @@ func (t *Tgbot) searchClient(chatId int64, email string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tgbot) searchInbound(chatId int64, remark string) {
|
||||
inbouds, err := t.inboundService.SearchInbounds(remark)
|
||||
if err != nil {
|
||||
logger.Warning(err)
|
||||
msg := "❌ Something went wrong!"
|
||||
t.SendMsgToTgbot(chatId, msg)
|
||||
return
|
||||
}
|
||||
for _, inbound := range inbouds {
|
||||
info := ""
|
||||
info += fmt.Sprintf("📍Inbound:%s\r\nPort:%d\r\n", inbound.Remark, inbound.Port)
|
||||
info += fmt.Sprintf("Traffic: %s (↑%s,↓%s)\r\n", common.FormatTraffic((inbound.Up + inbound.Down)), common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down))
|
||||
if inbound.ExpiryTime == 0 {
|
||||
info += "Expire date: ♾ Unlimited\r\n \r\n"
|
||||
} else {
|
||||
info += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
t.SendMsgToTgbot(chatId, info)
|
||||
for _, traffic := range inbound.ClientStats {
|
||||
expiryTime := ""
|
||||
if traffic.ExpiryTime == 0 {
|
||||
expiryTime = "♾Unlimited"
|
||||
} else {
|
||||
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||
}
|
||||
total := ""
|
||||
if traffic.Total == 0 {
|
||||
total = "♾Unlimited"
|
||||
} else {
|
||||
total = common.FormatTraffic((traffic.Total))
|
||||
}
|
||||
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
|
||||
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
|
||||
total, expiryTime)
|
||||
t.SendMsgToTgbot(chatId, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tgbot) searchForClient(chatId int64, query string) {
|
||||
traffic, err := t.inboundService.SearchClientTraffic(query)
|
||||
if err != nil {
|
||||
@@ -469,7 +513,7 @@ func (t *Tgbot) getExhausted() string {
|
||||
}
|
||||
ExpireThreshold, err := t.settingService.GetTgExpireDiff()
|
||||
if err == nil && ExpireThreshold > 0 {
|
||||
exDiff = int64(ExpireThreshold) * 84600
|
||||
exDiff = int64(ExpireThreshold) * 84600000
|
||||
}
|
||||
inbounds, err := t.inboundService.GetAllInbounds()
|
||||
if err != nil {
|
||||
@@ -477,14 +521,14 @@ func (t *Tgbot) getExhausted() string {
|
||||
}
|
||||
for _, inbound := range inbounds {
|
||||
if inbound.Enable {
|
||||
if (inbound.ExpiryTime > 0 && (now-inbound.ExpiryTime < exDiff)) ||
|
||||
if (inbound.ExpiryTime > 0 && (inbound.ExpiryTime-now < exDiff)) ||
|
||||
(inbound.Total > 0 && (inbound.Total-inbound.Up+inbound.Down < trDiff)) {
|
||||
exhaustedInbounds = append(exhaustedInbounds, *inbound)
|
||||
}
|
||||
if len(inbound.ClientStats) > 0 {
|
||||
for _, client := range inbound.ClientStats {
|
||||
if client.Enable {
|
||||
if (client.ExpiryTime > 0 && (now-client.ExpiryTime < exDiff)) ||
|
||||
if (client.ExpiryTime > 0 && (client.ExpiryTime-now < exDiff)) ||
|
||||
(client.Total > 0 && (client.Total-client.Up+client.Down < trDiff)) {
|
||||
exhaustedClients = append(exhaustedClients, client)
|
||||
}
|
||||
@@ -498,7 +542,7 @@ func (t *Tgbot) getExhausted() string {
|
||||
}
|
||||
}
|
||||
output += fmt.Sprintf("Exhausted Inbounds count:\r\n🛑 Disabled: %d\r\n🔜 Exhaust soon: %d\r\n \r\n", len(disabledInbounds), len(exhaustedInbounds))
|
||||
if len(disabledInbounds)+len(exhaustedInbounds) > 0 {
|
||||
if len(exhaustedInbounds) > 0 {
|
||||
output += "Exhausted Inbounds:\r\n"
|
||||
for _, inbound := range exhaustedInbounds {
|
||||
output += fmt.Sprintf("📍Inbound:%s\r\nPort:%d\r\nTraffic: %s (↑%s,↓%s)\r\n", inbound.Remark, inbound.Port, common.FormatTraffic((inbound.Up + inbound.Down)), common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down))
|
||||
@@ -510,7 +554,7 @@ func (t *Tgbot) getExhausted() string {
|
||||
}
|
||||
}
|
||||
output += fmt.Sprintf("Exhausted Clients count:\r\n🛑 Disabled: %d\r\n🔜 Exhaust soon: %d\r\n \r\n", len(disabledClients), len(exhaustedClients))
|
||||
if len(disabledClients)+len(exhaustedClients) > 0 {
|
||||
if len(exhaustedClients) > 0 {
|
||||
output += "Exhausted Clients:\r\n"
|
||||
for _, traffic := range exhaustedClients {
|
||||
expiryTime := ""
|
||||
@@ -525,7 +569,7 @@ func (t *Tgbot) getExhausted() string {
|
||||
} else {
|
||||
total = common.FormatTraffic((traffic.Total))
|
||||
}
|
||||
output += fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
|
||||
output += fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire date: %s\r\n \r\n",
|
||||
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
|
||||
total, expiryTime)
|
||||
}
|
||||
@@ -543,4 +587,10 @@ func (t *Tgbot) sendBackup(chatId int64) {
|
||||
if err != nil {
|
||||
logger.Warning("Error in uploading backup: ", err)
|
||||
}
|
||||
file = tgbotapi.FilePath(xray.GetConfigPath())
|
||||
msg = tgbotapi.NewDocument(chatId, file)
|
||||
_, err = bot.Send(msg)
|
||||
if err != nil {
|
||||
logger.Warning("Error in uploading config.json: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +129,15 @@
|
||||
"clickOnQRcode" = "Click on QR Code to Copy"
|
||||
"client" = "Client"
|
||||
"export" = "Export links"
|
||||
"Clone" = "Clone"
|
||||
"cloneInbound" = "Create"
|
||||
"cloneInboundContent" = "All items of this inbound except Port, Listening IP, Clients will be applied to the clone"
|
||||
"cloneInboundOk" = "Creating a clone from"
|
||||
"resetAllTraffic" = "Reset All Inbounds Traffic"
|
||||
"resetAllTrafficTitle" = "Reset all inbounds traffic"
|
||||
"resetAllTrafficContent" = "Are you sure to reset all inbounds traffic ?"
|
||||
"resetAllTrafficOkText" = "Confirm"
|
||||
"resetAllTrafficCancelText" = "Cancel"
|
||||
|
||||
[pages.client]
|
||||
"add" = "Add client"
|
||||
@@ -170,7 +179,7 @@
|
||||
"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"
|
||||
"panelConfig" = "Panel Configuration"
|
||||
"userSetting" = "User Setting"
|
||||
"xrayConfiguration" = "xray Configuration"
|
||||
"xrayConfiguration" = "Xray Configuration"
|
||||
"TGReminder" = "TG Reminder Related Settings"
|
||||
"otherSetting" = "Other Setting"
|
||||
"panelListeningIP" = "Panel listening IP"
|
||||
|
||||
@@ -74,8 +74,8 @@
|
||||
"xraySwitch" = "تغییر ورژن"
|
||||
"xraySwitchClick" = "ورژن مورد نظر را انتخاب کنید"
|
||||
"xraySwitchClickDesk" = "لطفا با دقت انتخاب کنید ، در صورت انتخاب اشتباه امکان قطعی سیستم وجود دارد ."
|
||||
"operationHours" = "ساعت فعال"
|
||||
"operationHoursDesc" = "ساعت فعال بعد از شروع سیستم"
|
||||
"operationHours" = "مدت فعالیت"
|
||||
"operationHoursDesc" = "مدت فعالیت سیستم بعد از روشن شدن"
|
||||
"systemLoad" = "بار روی سیستم"
|
||||
"connectionCount" = "تعداد کانکشن ها"
|
||||
"connectionCountDesc" = "تعداد کانکشن ها برای کل شبکه"
|
||||
@@ -83,7 +83,7 @@
|
||||
"downSpeed" = "سرعت دانلود در حال حاضر سیستم"
|
||||
"totalSent" = "جمع کل ترافیک آپلود مصرفی"
|
||||
"totalReceive" = "جمع کل ترافیک دانلود مصرفی"
|
||||
"xraySwitchVersionDialog" = "تغییر ورژن Xray"
|
||||
"xraySwitchVersionDialog" = "تغییر ورژن"
|
||||
"xraySwitchVersionDialogDesc" = "آیا از تغییر ورژن مطمئن هستین"
|
||||
"dontRefreshh" = "در حال نصب ، لطفا رفرش نکنید "
|
||||
|
||||
@@ -128,7 +128,17 @@
|
||||
"keyContent" = "محتوای Private.key"
|
||||
"clickOnQRcode" = "برای کپی بر روی کد تصویری کلیک کنید"
|
||||
"client" = "کاربر"
|
||||
"export" = "استخراج لینکها"
|
||||
"export" = "استخراج لینکها"
|
||||
"Clone" = "کلون"
|
||||
"cloneInbound" = "ایجاد"
|
||||
"cloneInboundContent" = "همه موارد این ورودی بجز پورت ، ای پی و کلاینت ها روی کلون اعمال خواهند شد"
|
||||
"cloneInboundOk" = "ساختن یک کلون از"
|
||||
"resetAllTraffic" = "ریست ترافیک کل ورودی ها"
|
||||
"resetAllTrafficTitle" = "ریست ترافیک کل ورودی ها"
|
||||
"resetAllTrafficContent" = "آیا مطمئن هستید که تمام ترافیک ورودی ها را ریست می کنید؟"
|
||||
"resetAllTrafficOkText" = "بله"
|
||||
"resetAllTrafficCancelText" = "انصراف"
|
||||
|
||||
|
||||
[pages.client]
|
||||
"add" = "کاربر جدید"
|
||||
|
||||
@@ -129,6 +129,15 @@
|
||||
"clickOnQRcode" = "点击二维码复制"
|
||||
"client" = "客户"
|
||||
"export" = "导出链接"
|
||||
"Clone" = "克隆"
|
||||
"cloneInbound" = "创造"
|
||||
"cloneInboundContent" = "此入站的所有项目除 Port、Listening IP、Clients 将应用于克隆"
|
||||
"cloneInboundOk" = "从创建克隆"
|
||||
"resetAllTraffic" = "重置所有入站流量"
|
||||
"resetAllTrafficTitle" = "重置所有入站流量"
|
||||
"resetAllTrafficContent" = "您确定要重置所有入站流量吗?"
|
||||
"resetAllTrafficOkText" = "确认"
|
||||
"resetAllTrafficCancelText" = "取消"
|
||||
|
||||
[pages.client]
|
||||
"add" = "添加客户端"
|
||||
|
||||
Reference in New Issue
Block a user