Add trojan multiuser

This commit is contained in:
Alireza Ahmadi
2023-02-07 00:37:01 +01:00
parent 117de52d7d
commit 9a2417db34
42 changed files with 1534 additions and 1615 deletions

View File

@@ -1,57 +0,0 @@
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 x-ui
value: |-
<details>
```console
$ x-ui version
# Paste output here
```
</details>
validations:
required: true
- type: textarea
id: log
attributes:
label: x-ui log or xray log
value: |-
<details>
```console
# paste log here
```
</details>
validations:
required: true

View File

@@ -6,7 +6,7 @@ on:
workflow_dispatch:
jobs:
release:
runs-on: ubuntu-18.04
runs-on: ubuntu-22.04
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
steps:
@@ -23,13 +23,13 @@ jobs:
linuxamd64build:
name: build x-ui amd64 version
needs: release
runs-on: ubuntu-18.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.18
go-version: '1.20'
- name: build linux amd64 version
run: |
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o xui-release -v main.go
@@ -41,7 +41,7 @@ jobs:
mv xui-release x-ui
mkdir bin
cd bin
wget https://github.com/hossinasaadi/Xray-core/releases/latest/download/Xray-linux-64.zip
wget https://github.com/xtls/xray-core/releases/latest/download/Xray-linux-64.zip
unzip Xray-linux-64.zip
rm -f Xray-linux-64.zip geoip.dat geosite.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
@@ -63,13 +63,13 @@ jobs:
linuxarm64build:
name: build x-ui arm64 version
needs: release
runs-on: ubuntu-18.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.18
go-version: '1.20'
- name: build linux arm64 version
run: |
sudo apt-get update
@@ -83,7 +83,7 @@ jobs:
mv xui-release x-ui
mkdir bin
cd bin
wget https://github.com/hossinasaadi/Xray-core/releases/latest/download/Xray-linux-arm64-v8a.zip
wget https://github.com/xtls/xray-core/releases/latest/download/Xray-linux-arm64-v8a.zip
unzip Xray-linux-arm64-v8a.zip
rm -f Xray-linux-arm64-v8a.zip geoip.dat geosite.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
@@ -105,13 +105,13 @@ jobs:
linuxs390xbuild:
name: build x-ui s390x version
needs: release
runs-on: ubuntu-18.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.18
go-version: '1.20'
- name: build linux s390x version
run: |
sudo apt-get update
@@ -125,7 +125,7 @@ jobs:
mv xui-release x-ui
mkdir bin
cd bin
wget https://github.com/hossinasaadi/Xray-core/releases/latest/download/Xray-linux-s390x.zip
wget https://github.com/xtls/xray-core/releases/latest/download/Xray-linux-s390x.zip
unzip Xray-linux-s390x.zip
rm -f Xray-linux-s390x.zip geoip.dat geosite.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.idea
.vscode
tmp
bin/xray-darwin-arm64
bin/config.json

View File

@@ -5,11 +5,11 @@ RUN go build main.go
FROM debian:11-slim
LABEL org.opencontainers.image.authors="hossin.asaadi77@gmail.com"
LABEL org.opencontainers.image.authors="alireza7@gmail.com"
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends -y ca-certificates \
&& apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
ENV TZ=Asia/Shanghai
ENV TZ=Asia/Tehran
WORKDIR /root
COPY --from=builder /root/main /root/x-ui
COPY ./bin/. /root/bin/.

View File

@@ -7,11 +7,10 @@ xray panel supporting multi-protocol, **Multi-lang (English,Chinese)**, **IP Res
| Features | Enable? |
| ------------- |:-------------:|
| Multi-lang | :heavy_check_mark: |
| [IP Restriction](https://github.com/HexaSoftwareTech/x-ui/#enable-ip-restrictions-per-inbound) | :heavy_check_mark: |
| [Inbound Multi User](https://github.com/HexaSoftwareTech/x-ui/#enable-multi-user-traffic--exprire-day) | :heavy_check_mark: |
| [Multi User Traffic & expire day](https://github.com/HexaSoftwareTech/x-ui/#enable-multi-user-traffic--exprire-day) | :heavy_check_mark: |
| [REST API](https://github.com/HexaSoftwareTech/x-ui/pull/51) | :heavy_check_mark: |
| [Telegram BOT](https://github.com/HexaSoftwareTech/x-ui/pull/110) | :heavy_check_mark: |
| [Inbound Multi User](https://github.com/alireza0/x-ui/#enable-multi-user-traffic--exprire-day) | :heavy_check_mark: |
| [Multi User Traffic & expire day](https://github.com/alireza0/x-ui/#enable-multi-user-traffic--exprire-day) | :heavy_check_mark: |
| [REST API](https://github.com/alireza0/x-ui/pull/51) | :heavy_check_mark: |
| [Telegram BOT](https://github.com/alireza0/x-ui/pull/110) | :heavy_check_mark: |
**If you think this project is helpful to you, you may wish to give a** :star2:
@@ -39,7 +38,7 @@ xray panel supporting multi-protocol, **Multi-lang (English,Chinese)**, **IP Res
```
- change access log path as you want
2 - add **IP limit and Unique Email** for inbound(vmess & vless)
2 - add **Unique Email** for inbound(vmess & vless & trojan)
# Enable Multi User Traffic & Exprire Day
![Screenshot from 2022-11-15 07-43-58](https://user-images.githubusercontent.com/16622377/201922652-111ff5b8-272b-49f5-a656-d6f57d79eaed.png)
@@ -85,12 +84,12 @@ find this in config :
# Install & Upgrade
```
bash <(curl -Ls https://raw.githubusercontent.com/HexaSoftwareTech/x-ui/master/install.sh)
bash <(curl -Ls https://raw.githubusercontent.com/alireza0/x-ui/master/install.sh)
```
## Manual install & upgrade
1. First download the latest compressed package from https://github.com/HexaSoftwareTech/x-ui/releases , generally choose Architecture `amd64`
1. First download the latest compressed package from https://github.com/alireza0/x-ui/releases , generally choose Architecture `amd64`
2. Then upload the compressed package to the server's `/root/` directory and `root` rootlog in to the server with user
> If your server cpu architecture is not `amd64` replace another architecture
@@ -110,7 +109,7 @@ systemctl restart x-ui
## Install using docker
> This docker tutorial and docker image are provided by [HexaSoftwareTech](https://github.com/HexaSoftwareTech)
> This docker tutorial and docker image are provided by [alireza0](https://github.com/alireza0)
1. install docker
@@ -126,7 +125,7 @@ docker run -itd --network=host \
-v $PWD/db/:/etc/x-ui/ \
-v $PWD/cert/:/root/cert/ \
--name x-ui --restart=unless-stopped \
HexaSoftwareTech/x-ui:latest
alireza0/x-ui:latest
```
> Build your own image
@@ -207,4 +206,4 @@ x-ui v2-ui
## Stargazers over time
[![Stargazers over time](https://starchart.cc/HexaSoftwareTech/x-ui.svg)](https://starchart.cc/HexaSoftwareTech/x-ui)
[![Stargazers over time](https://starchart.cc/alireza0/x-ui.svg)](https://starchart.cc/alireza0/x-ui)

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1 @@
0.5.3
0.1.0

View File

@@ -1,15 +1,16 @@
package database
import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"io/fs"
"os"
"path"
"x-ui/config"
"x-ui/xray"
"x-ui/database/model"
"x-ui/xray"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var db *gorm.DB
@@ -41,9 +42,7 @@ func initInbound() error {
func initSetting() error {
return db.AutoMigrate(&model.Setting{})
}
func initInboundClientIps() error {
return db.AutoMigrate(&model.InboundClientIps{})
}
func initClientTraffic() error {
return db.AutoMigrate(&xray.ClientTraffic{})
}
@@ -83,15 +82,12 @@ func InitDB(dbPath string) error {
if err != nil {
return err
}
err = initInboundClientIps()
if err != nil {
return err
}
err = initClientTraffic()
if err != nil {
return err
}
return nil
}

View File

@@ -24,15 +24,15 @@ type User struct {
}
type Inbound struct {
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
UserId int `json:"-"`
Up int64 `json:"up" form:"up"`
Down int64 `json:"down" form:"down"`
Total int64 `json:"total" form:"total"`
Remark string `json:"remark" form:"remark"`
Enable bool `json:"enable" form:"enable"`
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"`
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
UserId int `json:"-"`
Up int64 `json:"up" form:"up"`
Down int64 `json:"down" form:"down"`
Total int64 `json:"total" form:"total"`
Remark string `json:"remark" form:"remark"`
Enable bool `json:"enable" form:"enable"`
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"`
// config part
Listen string `json:"listen" form:"listen"`
@@ -43,11 +43,6 @@ type Inbound struct {
Tag string `json:"tag" form:"tag" gorm:"unique"`
Sniffing string `json:"sniffing" form:"sniffing"`
}
type InboundClientIps struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"`
Ips string `json:"ips" form:"ips"`
}
func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
listen := i.Listen
@@ -70,12 +65,12 @@ type Setting struct {
Key string `json:"key" form:"key"`
Value string `json:"value" form:"value"`
}
type Client struct {
ID string `json:"id"`
AlterIds uint16 `json:"alterId"`
Email string `json:"email"`
LimitIP int `json:"limitIp"`
Security string `json:"security"`
TotalGB int64 `json:"totalGB" form:"totalGB"`
ID string `json:"id"`
AlterIds uint16 `json:"alterId"`
Email string `json:"email"`
Security string `json:"security"`
TotalGB int64 `json:"totalGB" form:"totalGB"`
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
}
}

View File

@@ -1,7 +1,7 @@
version: '3.9'
services:
xui:
image: hossinasaadi/x-ui
image: alireza7/x-ui
container_name: x-ui
volumes:
- $PWD/db/:/etc/x-ui/

View File

@@ -107,20 +107,20 @@ install_x-ui() {
cd /usr/local/
if [ $# == 0 ]; then
last_version=$(curl -Ls "https://api.github.com/repos/hossinasaadi/x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
last_version=$(curl -Ls "https://api.github.com/repos/alireza0/x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
if [[ ! -n "$last_version" ]]; then
echo -e "${red}refresh x-ui version failed,it may due to Github API restriction,please try it later${plain}"
exit 1
fi
echo -e "get x-ui latest version succeed: ${last_version}, begin to install..."
wget -N --no-check-certificate -O /usr/local/x-ui-linux-${arch}.tar.gz https://github.com/hossinasaadi/x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz
wget -N --no-check-certificate -O /usr/local/x-ui-linux-${arch}.tar.gz https://github.com/alireza0/x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz
if [[ $? -ne 0 ]]; then
echo -e "${red}dowanload x-ui failed,please be sure that your server can access Github ${plain}"
exit 1
fi
else
last_version=$1
url="https://github.com/hossinasaadi/x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz"
url="https://github.com/alireza0/x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz"
echo -e "begin to install x-ui v$1"
wget -N --no-check-certificate -O /usr/local/x-ui-linux-${arch}.tar.gz ${url}
if [[ $? -ne 0 ]]; then
@@ -138,15 +138,15 @@ install_x-ui() {
cd x-ui
chmod +x x-ui bin/xray-linux-${arch}
cp -f x-ui.service /etc/systemd/system/
wget --no-check-certificate -O /usr/bin/x-ui https://raw.githubusercontent.com/hossinasaadi/x-ui/main/x-ui.sh
wget --no-check-certificate -O /usr/bin/x-ui https://raw.githubusercontent.com/alireza0/x-ui/main/x-ui.sh
chmod +x /usr/local/x-ui/x-ui.sh
chmod +x /usr/bin/x-ui
config_after_install
#echo -e "如果是全新安装,默认网页端口为 ${green}54321${plain},用户名和密码默认都是 ${green}admin${plain}"
#echo -e "请自行确保此端口没有被其他程序占用,${yellow}并且确保 54321 端口已放行${plain}"
# echo -e "若想将 54321 修改为其它端口,输入 x-ui 命令进行修改,同样也要确保你修改的端口也是放行的"
#echo -e "If it is a new installation, the default web port is ${green}54321${plain}, The username and password are ${green}admin${plain} by default"
#echo -e "Please make sure that this port is not occupied by other procedures,${yellow} And make sure that port 54321 has been released${plain}"
# echo -e "If you want to modify the 54321 to other ports and enter the x-ui command to modify it, you must also ensure that the port you modify is also released"
#echo -e ""
#echo -e "如果是更新面板,则按你之前的方式访问面板"
#echo -e "If it is updated panel, access the panel in your previous way"
#echo -e ""
systemctl daemon-reload
systemctl enable x-ui

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -4,6 +4,11 @@ supportLangs = [
value : "en-US",
icon : "🇺🇸"
},
{
name : "Farsi",
value : "fa_IR",
icon : "🇮🇷"
},
{
name : "汉语",
value : "zh-Hans",

View File

@@ -825,6 +825,10 @@ class Inbound extends XrayCommonClass {
if(this.settings.vlesses[index]._expiryTime != null)
return this.settings.vlesses[index]._expiryTime < new Date().getTime();
return false
case Protocols.TROJAN:
if(this.settings.trojans[index]._expiryTime != null)
return this.settings.trojans[index]._expiryTime < new Date().getTime();
return false
default:
return false;
}
@@ -1053,9 +1057,9 @@ class Inbound extends XrayCommonClass {
+ '#' + encodeURIComponent(remark);
}
genTrojanLink(address='', remark='') {
genTrojanLink(address='', remark='', clientIndex=0) {
let settings = this.settings;
return `trojan://${settings.clients[0].password}@${address}:${this.port}#${encodeURIComponent(remark)}`;
return `trojan://${settings.trojans[clientIndex].password}@${address}:${this.port}#${encodeURIComponent(remark+"-"+settings.trojans[clientIndex].email)}`;
}
genLink(address='', remark='', clientIndex=0) {
@@ -1063,7 +1067,7 @@ class Inbound extends XrayCommonClass {
case Protocols.VMESS: return this.genVmessLink(address, remark, clientIndex);
case Protocols.VLESS: return this.genVLESSLink(address, remark, clientIndex);
case Protocols.SHADOWSOCKS: return this.genSSLink(address, remark);
case Protocols.TROJAN: return this.genTrojanLink(address, remark);
case Protocols.TROJAN: return this.genTrojanLink(address, remark, clientIndex);
default: return '';
}
}
@@ -1181,12 +1185,11 @@ Inbound.VmessSettings = class extends Inbound.Settings {
}
};
Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
constructor(id=RandomUtil.randomUUID(), alterId=0, email='', limitIp=0, totalGB=0, expiryTime='') {
constructor(id=RandomUtil.randomUUID(), alterId=0, email='', totalGB=0, expiryTime='') {
super();
this.id = id;
this.alterId = alterId;
this.email = email;
this.limitIp = limitIp;
this.totalGB = totalGB;
this.expiryTime = expiryTime;
}
@@ -1196,7 +1199,6 @@ Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
json.id,
json.alterId,
json.email,
json.limitIp,
json.totalGB,
json.expiryTime,
@@ -1265,12 +1267,11 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
};
Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
constructor(id=RandomUtil.randomUUID(), flow=FLOW_CONTROL.DIRECT, email='', limitIp=0, totalGB=0, expiryTime='') {
constructor(id=RandomUtil.randomUUID(), flow=FLOW_CONTROL.DIRECT, email='', totalGB=0, expiryTime='') {
super();
this.id = id;
this.flow = flow;
this.email = email;
this.limitIp = limitIp;
this.totalGB = totalGB;
this.expiryTime = expiryTime;
@@ -1281,7 +1282,6 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
json.id,
json.flow,
json.email,
json.limitIp,
json.totalGB,
json.expiryTime,
@@ -1351,10 +1351,10 @@ Inbound.VLESSSettings.Fallback = class extends XrayCommonClass {
Inbound.TrojanSettings = class extends Inbound.Settings {
constructor(protocol,
clients=[new Inbound.TrojanSettings.Client()],
trojans=[new Inbound.TrojanSettings.Trojan()],
fallbacks=[],) {
super(protocol);
this.clients = clients;
this.trojans = trojans;
this.fallbacks = fallbacks;
}
@@ -1366,45 +1366,73 @@ Inbound.TrojanSettings = class extends Inbound.Settings {
this.fallbacks.splice(index, 1);
}
static fromJson(json={}) {
return new Inbound.TrojanSettings(
Protocols.TROJAN,
json.clients.map(client => Inbound.TrojanSettings.Trojan.fromJson(client)),
Inbound.TrojanSettings.Fallback.fromJson(json.fallbacks),);
}
toJson() {
return {
clients: Inbound.TrojanSettings.toJsonArray(this.clients),
clients: Inbound.TrojanSettings.toJsonArray(this.trojans),
fallbacks: Inbound.TrojanSettings.toJsonArray(this.fallbacks),
};
}
static fromJson(json={}) {
const clients = [];
for (const c of json.clients) {
clients.push(Inbound.TrojanSettings.Client.fromJson(c));
}
return new Inbound.TrojanSettings(
Protocols.TROJAN,
clients,
Inbound.TrojanSettings.Fallback.fromJson(json.fallbacks),);
}
};
Inbound.TrojanSettings.Client = class extends XrayCommonClass {
constructor(password=RandomUtil.randomSeq(10), flow=FLOW_CONTROL.DIRECT) {
Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
constructor(password=RandomUtil.randomSeq(10), flow=FLOW_CONTROL.DIRECT, email='', totalGB=0, expiryTime='') {
super();
this.password = password;
this.flow = flow;
this.email = email;
this.totalGB = totalGB;
this.expiryTime = expiryTime;
}
toJson() {
return {
password: this.password,
flow: this.flow,
email: this.email,
totalGB: this.totalGB,
expiryTime: this.expiryTime,
};
}
static fromJson(json={}) {
return new Inbound.TrojanSettings.Client(
return new Inbound.TrojanSettings.Trojan(
json.password,
json.flow,
json.email,
json.totalGB,
json.expiryTime,
);
}
get _expiryTime() {
if (this.expiryTime === 0 || this.expiryTime === "") {
return null;
}
return moment(this.expiryTime);
}
set _expiryTime(t) {
if (t == null || t === "") {
this.expiryTime = 0;
} else {
this.expiryTime = t.valueOf();
}
}
get _totalGB() {
return toFixed(this.totalGB / ONE_GB, 2);
}
set _totalGB(gb) {
this.totalGB = toFixed(gb * ONE_GB, 0);
}
};
Inbound.TrojanSettings.Fallback = class extends XrayCommonClass {

View File

@@ -2,13 +2,14 @@ package controller
import (
"fmt"
"github.com/gin-gonic/gin"
"strconv"
"x-ui/database/model"
"x-ui/logger"
"x-ui/web/global"
"x-ui/web/service"
"x-ui/web/session"
"github.com/gin-gonic/gin"
)
type InboundController struct {
@@ -30,11 +31,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.POST("/add", a.addInbound)
g.POST("/del/:id", a.delInbound)
g.POST("/update/:id", a.updateInbound)
g.POST("/clientIps/:email", a.getClientIps)
g.POST("/clearClientIps/:email", a.clearClientIps)
g.POST("/resetClientTraffic/:email", a.resetClientTraffic)
}
@@ -55,7 +52,7 @@ func (a *InboundController) getInbounds(c *gin.Context) {
user := session.GetLoginUser(c)
inbounds, err := a.inboundService.GetInbounds(user.Id)
if err != nil {
jsonMsg(c, I18n(c , "pages.inbounds.toasts.obtain"), err)
jsonMsg(c, I18n(c, "pages.inbounds.toasts.obtain"), err)
return
}
jsonObj(c, inbounds, nil)
@@ -63,12 +60,12 @@ func (a *InboundController) getInbounds(c *gin.Context) {
func (a *InboundController) getInbound(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18n(c , "get"), err)
jsonMsg(c, I18n(c, "get"), err)
return
}
inbound, err := a.inboundService.GetInbound(id)
if err != nil {
jsonMsg(c, I18n(c , "pages.inbounds.toasts.obtain"), err)
jsonMsg(c, I18n(c, "pages.inbounds.toasts.obtain"), err)
return
}
jsonObj(c, inbound, nil)
@@ -78,7 +75,7 @@ func (a *InboundController) addInbound(c *gin.Context) {
inbound := &model.Inbound{}
err := c.ShouldBind(inbound)
if err != nil {
jsonMsg(c, I18n(c , "pages.inbounds.addTo"), err)
jsonMsg(c, I18n(c, "pages.inbounds.addTo"), err)
return
}
user := session.GetLoginUser(c)
@@ -86,7 +83,7 @@ func (a *InboundController) addInbound(c *gin.Context) {
inbound.Enable = true
inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
inbound, err = a.inboundService.AddInbound(inbound)
jsonMsgObj(c, I18n(c , "pages.inbounds.addTo"), inbound, err)
jsonMsgObj(c, I18n(c, "pages.inbounds.addTo"), inbound, err)
if err == nil {
a.xrayService.SetToNeedRestart()
}
@@ -95,11 +92,11 @@ func (a *InboundController) addInbound(c *gin.Context) {
func (a *InboundController) delInbound(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18n(c , "delete"), err)
jsonMsg(c, I18n(c, "delete"), err)
return
}
err = a.inboundService.DelInbound(id)
jsonMsgObj(c, I18n(c , "delete"), id, err)
jsonMsgObj(c, I18n(c, "delete"), id, err)
if err == nil {
a.xrayService.SetToNeedRestart()
}
@@ -108,7 +105,7 @@ func (a *InboundController) delInbound(c *gin.Context) {
func (a *InboundController) updateInbound(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18n(c , "pages.inbounds.revise"), err)
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
return
}
inbound := &model.Inbound{
@@ -116,35 +113,16 @@ func (a *InboundController) updateInbound(c *gin.Context) {
}
err = c.ShouldBind(inbound)
if err != nil {
jsonMsg(c, I18n(c , "pages.inbounds.revise"), err)
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
return
}
inbound, err = a.inboundService.UpdateInbound(inbound)
jsonMsgObj(c, I18n(c , "pages.inbounds.revise"), inbound, err)
jsonMsgObj(c, I18n(c, "pages.inbounds.revise"), inbound, err)
if err == nil {
a.xrayService.SetToNeedRestart()
}
}
func (a *InboundController) getClientIps(c *gin.Context) {
email := c.Param("email")
ips , err := a.inboundService.GetInboundClientIps(email)
if err != nil {
jsonObj(c, "No IP Record", nil)
return
}
jsonObj(c, ips, nil)
}
func (a *InboundController) clearClientIps(c *gin.Context) {
email := c.Param("email")
err := a.inboundService.ClearClientIps(email)
if err != nil {
jsonMsg(c, "修改", err)
return
}
jsonMsg(c, "Log Cleared", nil)
}
func (a *InboundController) resetClientTraffic(c *gin.Context) {
email := c.Param("email")

View File

@@ -2,8 +2,8 @@
<a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title"
:closable="true" width="300px" :ok-text="qrModal.okText"
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;" >click on QR Code to Copy</a-tag>
<canvas v-if="qrModal.inbound.protocol != Protocols.VMESS && qrModal.inbound.protocol != Protocols.VLESS" id="qrCode" style="width: 100%; height: 100%;"></canvas>
<a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;" >{{ i18n "pages.inbounds.clickOnQRcode" }}</a-tag>
<canvas v-if="qrModal.inbound.protocol != Protocols.VMESS && qrModal.inbound.protocol != Protocols.VLESS && qrModal.inbound.protocol != Protocols.TROJAN" id="qrCode" style="width: 100%; height: 100%;"></canvas>
<template v-if="qrModal.inbound.protocol === Protocols.VMESS" v-for="(vmess, index) in qrModal.inbound.settings.vmesses">
<a-tag color="red" style="margin-bottom: 10px;display: block;text-align: center;" v-text="vmess.email"></a-tag>
@@ -16,6 +16,12 @@
<canvas @click="copyTextToClipboard(`qrCode-vless-${vless.id}`,index)" :id="`qrCode-vless-${vless.id}`" style="width: 100%; height: 100%;"></canvas>
<a-divider style="height: 2px; background-color: #7e7e7e" />
</template>
<template v-if="qrModal.inbound.protocol === Protocols.TROJAN" v-for="(trojan, index) in qrModal.inbound.settings.trojans">
<a-tag color="red" style="margin-bottom: 10px;display: block;text-align: center;" v-text="trojan.email"></a-tag>
<canvas @click="copyTextToClipboard(`qrCode-trojan-${trojan.password}`,index)" :id="`qrCode-trojan-${trojan.password}`" style="width: 100%; height: 100%;"></canvas>
<a-divider style="height: 2px; background-color: #7e7e7e" />
</template>
</a-modal>
<script>
@@ -110,6 +116,13 @@
this.setQrCode("qrCode-vless-" + vlesses[index].id ,index)
}
break;
case Protocols.TROJAN:
trojans = qrModal.inbound.settings.trojans
for (const index in trojans) {
this.setQrCode("qrCode-trojan-" + trojans[index].password ,index)
}
break;
default: return null;
}

View File

@@ -13,14 +13,14 @@
</a-menu-item>
<!--<a-menu-item key="{{ .base_path }}xui/clients">-->
<!-- <a-icon type="laptop"></a-icon>-->
<!-- <span>客户端</span>-->
<!-- <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/hossinasaadi/x-ui/">
<a-menu-item key="https://github.com/alireza0/x-ui/">
<a-icon type="github"></a-icon>
<span>Github</span>
</a-menu-item>

View File

@@ -1,66 +1,93 @@
{{define "inboundInfoStream"}}
<p>{{ i18n "transmission" }}: <a-tag color="green">[[ inbound.network ]]</a-tag></p>
<table>
<tr>
<td>{{ i18n "transmission" }}</td><td><a-tag color="green">[[ inbound.network ]]</a-tag></td>
</tr>
<template v-if="inbound.isTcp || inbound.isWs || inbound.isH2">
<p v-if="inbound.host">host: <a-tag color="green">[[ inbound.host ]]</a-tag></p>
<p v-else>{{ i18n "host" }}: <a-tag color="orange">{{ i18n "none" }}</a-tag></p>
<tr v-if="inbound.host"><td>{{ i18n "host" }}</td><td><a-tag color="green">[[ inbound.host ]]</a-tag></td></tr>
<tr v-else><td>{{ i18n "host" }}</td><td><a-tag color="orange">{{ i18n "none" }}</a-tag></td></tr>
<p v-if="inbound.path">path: <a-tag color="green">[[ inbound.path ]]</a-tag></p>
<p v-else>{{ i18n "path" }}: <a-tag color="orange">{{ i18n "none" }}</a-tag></p>
<tr v-if="inbound.path"><td>{{ i18n "path" }}</td><td><a-tag color="green">[[ inbound.path ]]</a-tag></td></tr>
<tr v-else><td>{{ i18n "path" }}</td><td><a-tag color="orange">{{ i18n "none" }}</a-tag></td></tr>
</template>
<template v-if="inbound.isQuic">
<p>quic {{ i18n "encryption" }}: <a-tag color="green">[[ inbound.quicSecurity ]]</a-tag></p>
<p>quic {{ i18n "password" }}: <a-tag color="green">[[ inbound.quicKey ]]</a-tag></p>
<p>quic {{ i18n "camouflage" }}: <a-tag color="green">[[ inbound.quicType ]]</a-tag></p>
<tr><td>quic {{ i18n "encryption" }}</td><td><a-tag color="green">[[ inbound.quicSecurity ]]</a-tag></td></tr>
<tr><td>quic {{ i18n "password" }}</td><td><a-tag color="green">[[ inbound.quicKey ]]</a-tag></td></tr>
<tr><td>quic {{ i18n "camouflage" }}</td><td><a-tag color="green">[[ inbound.quicType ]]</a-tag></td></tr>
</template>
<template v-if="inbound.isKcp">
<p>kcp {{ i18n "encryption" }}: <a-tag color="green">[[ inbound.kcpType ]]</a-tag></p>
<p>kcp {{ i18n "password" }}: <a-tag color="green">[[ inbound.kcpSeed ]]</a-tag></p>
<tr><td>kcp {{ i18n "encryption" }}</td><td><a-tag color="green">[[ inbound.kcpType ]]</a-tag></td></tr>
<tr><td>kcp {{ i18n "password" }}</td><td><a-tag color="green">[[ inbound.kcpSeed ]]</a-tag></td></tr>
</template>
<template v-if="inbound.isGrpc">
<p>grpc serviceName: <a-tag color="green">[[ inbound.serviceName ]]</a-tag></p>
<tr><td>grpc serviceName</td><td><a-tag color="green">[[ inbound.serviceName ]]</a-tag></td></tr>
</template>
<template v-if="inbound.tls || inbound.xtls">
<p v-if="inbound.tls">tls: <a-tag color="green">{{ i18n "turnOn" }}</a-tag></p>
<p v-if="inbound.xtls">xtls: <a-tag color="green">{{ i18n "turnOn" }}</a-tag></p>
</template>
<template v-else>
<p>tls: <a-tag color="red">{{ i18n "closure" }}</a-tag></p>
</template>
<p v-if="inbound.tls">
tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
</p>
<p v-if="inbound.xtls">
xtls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
</p>
</table>
</td></tr>
<tr colspan="2">
<td v-if="inbound.tls">
tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
</td>
<td v-else-if="inbound.xtls">
xtls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
xtls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
</td>
<td v-else>tls: <a-tag color="red">{{ i18n "disabled" }}</a-tag>
{{end}}
{{define "component/inboundInfoComponent"}}
<div>
<p>{{ i18n "protocol"}}: <a-tag color="green">[[ dbInbound.protocol ]]</a-tag></p>
<p>{{ i18n "pages.inbounds.address"}}: <a-tag color="blue">[[ dbInbound.address ]]</a-tag></p>
<p>{{ i18n "pages.inbounds.port"}}: <a-tag color="green">[[ dbInbound.port ]]</a-tag></p>
<table style="margin-bottom: 10px">
<tr><td>
<table>
<tr><td>{{ i18n "protocol" }}</td><td><a-tag color="green">[[ dbInbound.protocol ]]</a-tag></td></tr>
<tr><td>{{ i18n "pages.inbounds.address" }}</td><td><a-tag color="blue">[[ dbInbound.address ]]</a-tag></td></tr>
<tr><td>{{ i18n "pages.inbounds.port" }}</td><td><a-tag color="green">[[ dbInbound.port ]]</a-tag></td></tr>
</table>
</td>
<td>
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
{{template "inboundInfoStream"}}
</template>
</td></tr>
</table>
<a-divider>{{ i18n "clients" }}</a-divider>
<table v-if="dbInbound.isVMess">
<tr>
<th>email</th>
<th>uuid</th>
</tr>
<tr v-for="(vmess, index) in inbound.settings.vmesses">
<td><a-tag color="green">[[ vmess.email ]]</a-tag></td>
<td><a-tag color="green">[[ vmess.id ]]</a-tag></td>
</tr>
</table>
<template v-if="dbInbound.isVMess" v-for="(vmess, index) in inbound.settings.vmesses">
<p>uuid: <a-tag color="green">[[ vmess.id ]]</a-tag></p>
<p>alterId: <a-tag color="green">[[ vmess.alterId ]]</a-tag></p>
<a-divider style="height: 2px; background-color: #7e7e7e" />
</template>
<table v-if="dbInbound.isVLess">
<tr>
<th>email</th>
<th>uuid</th>
</tr>
<tr v-for="(vless, index) in inbound.settings.vlesses">
<td><a-tag color="green">[[ vless.email ]]</a-tag></td>
<td><a-tag color="green">[[ vless.id ]]</a-tag></td>
</tr>
</table>
<template v-if="dbInbound.isVLess" v-for="(vless, index) in inbound.settings.vlesses">
<p>uuid: <a-tag color="green">[[ vless.id ]]</a-tag></p>
<p v-if="inbound.isXTls">flow: <a-tag color="green">[[ vless.flow ]]</a-tag></p>
<a-divider style="height: 2px; background-color: #7e7e7e" />
</template>
<template v-if="dbInbound.isTrojan">
<p>{{ i18n "password"}}: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
</template>
<table v-if="dbInbound.isTrojan">
<tr>
<th>email</th>
<th>password</th>
</tr>
<tr v-for="(trojan, index) in inbound.settings.trojans">
<td><a-tag color="green">[[ trojan.email ]]</a-tag></td>
<td><a-tag color="green">[[ trojan.password ]]</a-tag></td>
</tr>
</table>
<template v-if="dbInbound.isSS">
<p>{{ i18n "encryption"}}: <a-tag color="green">[[ inbound.method ]]</a-tag></p>
@@ -77,9 +104,6 @@
<p>{{ i18n "password"}}: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
</template>
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
{{template "inboundInfoStream"}}
</template>
</div>
{{end}}
@@ -90,5 +114,6 @@
props: ["dbInbound", "inbound"],
template: `{{template "component/inboundInfoComponent"}}`,
});
</script>
{{end}}

View File

@@ -1,6 +1,6 @@
{{define "form/socks"}}
<a-form layout="inline">
<!-- <a-form-item label="密码认证">-->
<!-- <a-form-item label="Password authentication">-->
<a-form-item label='{{ i18n "password" }}'>
<a-switch :checked="inbound.settings.auth === 'password'"
@change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>

View File

@@ -1,15 +1,109 @@
{{define "form/trojan"}}
<a-form layout="inline">
<a-form-item label='{{ i18n "password" }}'>
<a-input v-model.trim="inbound.settings.clients[0].password"></a-input>
</a-form-item>
<a-form-item v-if="inbound.xtls" label="flow">
<a-select v-model="inbound.settings.clients[0].flow" style="width: 150px">
<a-select-option value="">{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
</a-form>
<label>{{ i18n "clients"}} </label>
<a-collapse activeKey="0" v-for="(trojan, index) in inbound.settings.trojans"
:key="`trojan-${index}`">
<a-collapse-panel :header="getHeaderText(trojan.email)">
<a-tag v-if="isExpiry(index) || ((getUpStats(trojan.email) + getDownStats(trojan.email)) > trojan.totalGB && trojan.totalGB != 0)" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
<a-form layout="inline">
<a-form-item>
<span slot="label">
Email
<a-tooltip>
<template slot="title">
The email must be completely unique
</template>
<!--Renew Svg Icon-->
<svg
@click="getNewEmail(trojan)"
xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="anticon anticon-question-circle" viewBox="0 0 16 16"> <path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z"/> <path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"/> </svg>
</a-tooltip>
</span>
<a-input v-model.trim="trojan.email"></a-input>
</a-form-item>
</a-form>
<a-form-item label="password">
<a-input v-model.trim="trojan.password"></a-input>
</a-form-item>
<a-form-item v-if="inbound.xtls" label="flow">
<a-select v-model="trojan.flow" style="width: 150px">
<a-select-option value="">{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<span slot="label">
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="trojan._totalGB" :min="0"></a-input-number>
</a-form-item>
<a-form-item>
<span slot="label">
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
v-model="trojan._expiryTime" style="width: 300px;"></a-date-picker>
</a-form-item>
<a-form layout="inline">
<a-tooltip v-if="trojan._totalGB > 0">
<template slot="title">
reset traffic
</template>
<span style="color: #FF4D4F">
<a-icon type="delete" @click="resetClientTraffic(trojan,$event)"></a-icon>
</span>
</a-tooltip>
<a-tag color="blue">[[ sizeFormat(getUpStats(trojan.email)) ]] / [[ sizeFormat(getDownStats(trojan.email)) ]]</a-tag>
<a-tag v-if="trojan._totalGB > 0" color="red">used : [[ sizeFormat(getUpStats(trojan.email) + getDownStats(trojan.email)) ]]</a-tag>
<a-tag v-show="inbound.settings.trojans.length > 1">
<svg
v-show="inbound.settings.trojans.length > 1"
@click="removeClient(index, inbound.settings.trojans)"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
class="ml-2 cursor-pointer"
>
<path fill="none" d="M0 0h24v24H0z" />
<path
fill="#EC4899"
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0-9.414l2.828-2.829 1.415 1.415L13.414 12l2.829 2.828-1.415 1.415L12 13.414l-2.828 2.829-1.415-1.415L10.586 12 7.757 9.172l1.415-1.415L12 10.586z"
/>
</svg>
</a-tag>
</a-form>
</a-collapse-panel>
</a-collapse>
<a-tag>
<svg
@click="addClient(inbound.protocol, inbound.settings.trojans)"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
class="ml-2 cursor-pointer"
>
<path fill="none" d="M0 0h24v24H0z" />
<path
fill="green"
d="M11 11V7h2v4h4v2h-4v4h-2v-4H7v-2h4zm1 11C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"
/>
</svg>
</a-tag>
<a-form layout="inline">
<a-form-item label="fallbacks">

View File

@@ -1,5 +1,6 @@
{{define "form/vless"}}
<a-form layout="inline">
<label>{{ i18n "clients"}}</label>
<a-collapse activeKey="0" v-for="(vless, index) in inbound.settings.vlesses"
:key="`vless-${index}`">
@@ -22,43 +23,6 @@
</span>
<a-input v-model.trim="vless.email"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
IP Count Limit
<a-tooltip>
<template slot="title">
disable inbound if more than entered count (0 for disable limit ip)
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input type="number" v-model.number="vless.limitIp" min="0" ></a-input>
</a-form-item>
<a-form-item v-if="vless.email && vless.limitIp > 0 && isEdit">
<span slot="label">
IP log
<a-tooltip>
<template slot="title">
IPs history Log (before enabling inbound after it has been disabled by IP limit, you should clear the log)
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title">
clear the log
</template>
<span style="color: #FF4D4F">
<a-icon type="delete" @click="clearDBClientIps(vless.email,$event)"></a-icon>
</span>
</a-tooltip>
</span>
<a-form layout="block">
<a-textarea readonly @click="getDBClientIps(vless.email,$event)" placeholder="Click To Get IPs" :auto-size="{ minRows: 0.5, maxRows: 10 }">
</a-textarea>
</a-form>
</a-form-item>
</a-form>
<a-form-item label="id">
<a-input v-model.trim="vless.id"></a-input>
@@ -105,23 +69,7 @@
</a-tooltip>
<a-tag color="blue">[[ sizeFormat(getUpStats(vless.email)) ]] / [[ sizeFormat(getDownStats(vless.email)) ]]</a-tag>
<a-tag v-if="vless._totalGB > 0" color="red">used : [[ sizeFormat(getUpStats(vless.email) + getDownStats(vless.email)) ]]</a-tag>
<a-tag>
<svg
@click="addClient(inbound.protocol,vless, inbound.settings.vlesses)"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 22 22"
width="22"
height="22"
class="mt-2 cursor-pointer"
>
<path fill="none" d="M0 0h24v24H0z" />
<path
fill="green"
d="M11 11V7h2v4h4v2h-4v4h-2v-4H7v-2h4zm1 11C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"
/>
</svg>
</a-tag>
<a-tag v-show="inbound.settings.vlesses.length > 1">
<svg
@click="removeClient(index, inbound.settings.vlesses)"
@@ -139,9 +87,24 @@
</svg>
</a-tag>
</a-form>
</a-form>
</a-collapse-panel>
</a-collapse>
<a-tag>
<svg
@click="addClient(inbound.protocol, inbound.settings.vlesses)"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
class="ml-2 cursor-pointer"
>
<path fill="none" d="M0 0h24v24H0z" />
<path
fill="green"
d="M11 11V7h2v4h4v2h-4v4h-2v-4H7v-2h4zm1 11C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"
/>
</svg>
</a-tag>
<a-form layout="inline">
<a-form-item label="fallbacks">

View File

@@ -1,5 +1,6 @@
{{define "form/vmess"}}
<a-form layout="inline">
<label>{{ i18n "clients"}}</label>
<a-collapse activeKey="0" v-for="(vmess, index) in inbound.settings.vmesses"
:key="`vmess-${index}`">
<a-collapse-panel :header="getHeaderText(vmess.email)">
@@ -21,42 +22,6 @@
</span>
<a-input v-model.trim="vmess.email"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
IP Count Limit
<a-tooltip>
<template slot="title">
disable inbound if more than entered count (0 for disable limit ip)
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input type="number" v-model.number="vmess.limitIp" min="0" ></a-input>
</a-form-item>
<a-form-item v-if="vmess.email && vmess.limitIp > 0 && isEdit">
<span slot="label">
IP Log
<a-tooltip>
<template slot="title">
IPs history Log (before enabling inbound after it has been disabled by IP limit, you should clear the log)
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title">
clear the log
</template>
<span style="color: #FF4D4F">
<a-icon type="delete" @click="clearDBClientIps(vmess.email,$event)"></a-icon>
</span>
</a-tooltip>
</span>
<a-textarea readonly @click="getDBClientIps(vmess.email,$event)" placeholder="Click To Get IPs" :auto-size="{ minRows: 0.5, maxRows: 10 }">
</a-textarea>
</a-form-item>
</a-form>
<a-form-item label="id">
<a-input v-model.trim="vmess.id"></a-input>
@@ -100,26 +65,7 @@
</a-tooltip>
<a-tag color="blue">[[ sizeFormat(getUpStats(vmess.email)) ]] / [[ sizeFormat(getDownStats(vmess.email)) ]]</a-tag>
<a-tag v-if="vmess._totalGB > 0" color="red">used : [[ sizeFormat(getUpStats(vmess.email) + getDownStats(vmess.email)) ]]</a-tag>
<a-tag>
<!--Add Svg Icon-->
<svg
@click="addClient(inbound.protocol,vmess, inbound.settings.vmesses)"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 22 22"
width="22"
height="22"
class="mt-2 cursor-pointer"
>
<path fill="none" d="M0 0h24v24H0z" />
<path
fill="green"
d="M11 11V7h2v4h4v2h-4v4h-2v-4H7v-2h4zm1 11C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"
/>
</svg>
</a-tag>
<a-tag v-show="inbound.settings.vmesses.length > 1">
<!--Remove Svg Icon-->
<svg
@click="removeClient(index, inbound.settings.vmesses)"
@@ -143,9 +89,23 @@
</a-collapse>
</a-form>
</a-form>
<a-tag>
<svg
@click="addClient(inbound.protocol, inbound.settings.vmesses)"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
class="ml-2 cursor-pointer"
>
<path fill="none" d="M0 0h24v24H0z" />
<path
fill="green"
d="M11 11V7h2v4h4v2h-4v4h-2v-4H7v-2h4zm1 11C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"
/>
</svg>
</a-tag>
<a-form layout="inline">
<a-form-item label='{{ i18n "pages.inbounds.disableInsecureEncryption" }}'>
<a-switch v-model.number="inbound.settings.disableInsecure"></a-switch>

View File

@@ -4,7 +4,7 @@
<a-form-item label="acceptProxyProtocol">
<a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch>
</a-form-item>
<a-form-item label="http 伪装">
<a-form-item label="http {{ i18n "camouflage" }}">
<a-switch
:checked="inbound.stream.tcp.type === 'http'"
@change="checked => inbound.stream.tcp.type = checked ? 'http' : 'none'">

View File

@@ -54,7 +54,7 @@
get inbound() {
return this.infoModal.inbound;
}
},
}
});
</script>

View File

@@ -77,41 +77,17 @@
this.inModal.inbound.tls = false;
}
},
addClient(protocol,value, clients) {
addClient(protocol, clients) {
switch (protocol) {
case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.Vmess());
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
default: return null;
}
},
removeClient(index, clients) {
clients.splice(index, 1);
},
async getDBClientIps(email,event) {
const msg = await HttpUtil.post('/xui/inbound/clientIps/'+ email);
if (!msg.success) {
return;
}
try {
ips = JSON.parse(msg.obj)
ips = ips.join(",")
event.target.value = ips
} catch (error) {
// text
event.target.value = msg.obj
}
},
async clearDBClientIps(email,event) {
const msg = await HttpUtil.post('/xui/inbound/clearClientIps/'+ email);
if (!msg.success) {
return;
}
event.target.value = ""
},
async resetClientTraffic(client,event) {
const msg = await HttpUtil.post('/xui/inbound/resetClientTraffic/'+ client.email);
if (!msg.success) {

View File

@@ -46,8 +46,8 @@
<div slot="title">
<a-button type="primary" icon="plus" @click="openAddInbound"></a-button>
</div>
<!-- <a-input v-model="searchKey" placeholder="搜索" autofocus style="max-width: 300px"></a-input>-->
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
<!-- <a-input v-model="searchKey" placeholder="search" autofocus style="max-width: 300px"></a-input>-->
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
:data-source="dbInbounds"
:loading="spinning" :scroll="{ x: 1500 }"
:pagination="false"
@@ -55,7 +55,7 @@
@change="() => getDBInbounds()">
<template slot="action" slot-scope="text, dbInbound">
<a-dropdown :trigger="['click']">
<a @click="e => e.preventDefault()">{{ i18n "pages.inbounds.operate" }}</a>
<a-icon @click="e => e.preventDefault()" type="menu"></a-icon>
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)">
<a-menu-item v-if="dbInbound.hasLink()" key="qrcode">
<a-icon type="qrcode"></a-icon>
@@ -112,6 +112,11 @@
</template>
<a-tag v-else color="green">{{ i18n "indefinitely" }}</a-tag>
</template>
<template #expandedRowRender="{ record }">
<p style="margin: 0">
{{ i18n "none" }}
</p>
</template>
</a-table>
</a-card>
</transition>

View File

@@ -1,351 +0,0 @@
package job
import (
"x-ui/logger"
"x-ui/web/service"
"x-ui/database"
"x-ui/database/model"
"os"
ss "strings"
"regexp"
"encoding/json"
// "strconv"
"strings"
"time"
"net"
"github.com/go-cmd/cmd"
"sort"
)
type CheckClientIpJob struct {
xrayService service.XrayService
inboundService service.InboundService
}
var job *CheckClientIpJob
var disAllowedIps []string
func NewCheckClientIpJob() *CheckClientIpJob {
job = new(CheckClientIpJob)
return job
}
func (j *CheckClientIpJob) Run() {
logger.Debug("Check Client IP Job...")
processLogFile()
// disAllowedIps = []string{"192.168.1.183","192.168.1.197"}
blockedIps := []byte(ss.Join(disAllowedIps,","))
err := os.WriteFile("./bin/blockedIPs", blockedIps, 0755)
checkError(err)
}
func processLogFile() {
accessLogPath := GetAccessLogPath()
if(accessLogPath == "") {
logger.Warning("xray log not init in config.json")
return
}
data, err := os.ReadFile(accessLogPath)
InboundClientIps := make(map[string][]string)
checkError(err)
// clean log
if err := os.Truncate(GetAccessLogPath(), 0); err != nil {
checkError(err)
}
lines := ss.Split(string(data), "\n")
for _, line := range lines {
ipRegx, _ := regexp.Compile(`[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`)
emailRegx, _ := regexp.Compile(`email:.+`)
matchesIp := ipRegx.FindString(line)
if(len(matchesIp) > 0) {
ip := string(matchesIp)
if( ip == "127.0.0.1" || ip == "1.1.1.1") {
continue
}
matchesEmail := emailRegx.FindString(line)
if(matchesEmail == "") {
continue
}
matchesEmail = ss.Split(matchesEmail, "email: ")[1]
if(InboundClientIps[matchesEmail] != nil) {
if(contains(InboundClientIps[matchesEmail],ip)){
continue
}
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail],ip)
}else{
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail],ip)
}
}
}
disAllowedIps = []string{}
for clientEmail, ips := range InboundClientIps {
inboundClientIps,err := GetInboundClientIps(clientEmail)
sort.Sort(sort.StringSlice(ips))
if(err != nil){
addInboundClientIps(clientEmail,ips)
}else{
updateInboundClientIps(inboundClientIps,clientEmail,ips)
}
}
// check if inbound connection is more than limited ip and drop connection
LimitDevice := func() { LimitDevice() }
stop := schedule(LimitDevice, 1000 *time.Millisecond)
time.Sleep(10 * time.Second)
stop <- true
}
func GetAccessLogPath() string {
config, err := os.ReadFile("bin/config.json")
checkError(err)
jsonConfig := map[string]interface{}{}
err = json.Unmarshal([]byte(config), &jsonConfig)
checkError(err)
if(jsonConfig["log"] != nil) {
jsonLog := jsonConfig["log"].(map[string]interface{})
if(jsonLog["access"] != nil) {
accessLogPath := jsonLog["access"].(string)
return accessLogPath
}
}
return ""
}
func checkError(e error) {
if e != nil {
logger.Warning("client ip job err:", e)
}
}
func contains(s []string, str string) bool {
for _, v := range s {
if v == str {
return true
}
}
return false
}
func GetInboundClientIps(clientEmail string) (*model.InboundClientIps, error) {
db := database.GetDB()
InboundClientIps := &model.InboundClientIps{}
err := db.Model(model.InboundClientIps{}).Where("client_email = ?", clientEmail).First(InboundClientIps).Error
if err != nil {
return nil, err
}
return InboundClientIps, nil
}
func addInboundClientIps(clientEmail string,ips []string) error {
inboundClientIps := &model.InboundClientIps{}
jsonIps, err := json.Marshal(ips)
checkError(err)
inboundClientIps.ClientEmail = clientEmail
inboundClientIps.Ips = string(jsonIps)
db := database.GetDB()
tx := db.Begin()
defer func() {
if err == nil {
tx.Commit()
} else {
tx.Rollback()
}
}()
err = tx.Save(inboundClientIps).Error
if err != nil {
return err
}
return nil
}
func updateInboundClientIps(inboundClientIps *model.InboundClientIps,clientEmail string,ips []string) error {
jsonIps, err := json.Marshal(ips)
checkError(err)
inboundClientIps.ClientEmail = clientEmail
inboundClientIps.Ips = string(jsonIps)
// check inbound limitation
inbound, err := GetInboundByEmail(clientEmail)
checkError(err)
if inbound.Settings == "" {
logger.Debug("wrong data ",inbound)
return nil
}
settings := map[string][]model.Client{}
json.Unmarshal([]byte(inbound.Settings), &settings)
clients := settings["clients"]
for _, client := range clients {
if client.Email == clientEmail {
limitIp := client.LimitIP
if(limitIp < len(ips) && limitIp != 0 && inbound.Enable) {
disAllowedIps = append(disAllowedIps,ips[limitIp:]...)
}
}
}
logger.Debug("disAllowedIps ",disAllowedIps)
sort.Sort(sort.StringSlice(disAllowedIps))
db := database.GetDB()
err = db.Save(inboundClientIps).Error
if err != nil {
return err
}
return nil
}
func DisableInbound(id int) error{
db := database.GetDB()
result := db.Model(model.Inbound{}).
Where("id = ? and enable = ?", id, true).
Update("enable", false)
err := result.Error
logger.Warning("disable inbound with id:",id)
if err == nil {
job.xrayService.SetToNeedRestart()
}
return err
}
func GetInboundByEmail(clientEmail string) (*model.Inbound, error) {
db := database.GetDB()
var inbounds *model.Inbound
err := db.Model(model.Inbound{}).Where("settings LIKE ?", "%" + clientEmail + "%").Find(&inbounds).Error
if err != nil {
return nil, err
}
return inbounds, nil
}
func LimitDevice(){
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.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 := "","","",""
destIp = string(ipRegx.FindString(data[0]))
destPort = portRegx.FindString(data[0])
destPort = strings.Replace(destPort,":","",-1)
srcIp = string(ipRegx.FindString(data[1]))
srcPort = portRegx.FindString(data[1])
srcPort = strings.Replace(srcPort,":","",-1)
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
ifaces, err := net.Interfaces()
ips := []string{}
if err != nil {
return ips, err
}
for _, i := range ifaces {
addrs, err := i.Addrs()
if err != nil {
return ips, err
}
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
ips = append(ips,ip.String())
}
}
logger.Debug("System IPs : ",ips)
return ips, nil
}
func IPsToRegex(ips []string) (string){
regx := ""
for _, ip := range ips {
regx += "(" + strings.Replace(ip, ".", "\\.", -1) + ")"
}
regx = "(" + strings.Replace(regx, ")(", ")|(.", -1) + ")"
return regx
}
func schedule(LimitDevice func(), delay time.Duration) chan bool {
stop := make(chan bool)
go func() {
for {
LimitDevice()
select {
case <-time.After(delay):
case <-stop:
return
}
}
}()
return stop
}

View File

@@ -1,14 +1,14 @@
package service
import (
"encoding/json"
"fmt"
"time"
"x-ui/database"
"encoding/json"
"x-ui/database/model"
"x-ui/logger"
"x-ui/util/common"
"x-ui/xray"
"x-ui/logger"
"gorm.io/gorm"
)
@@ -64,11 +64,11 @@ func (s *InboundService) getClients(inbound *model.Inbound) ([]model.Client, err
return clients, nil
}
func (s *InboundService) checkEmailsExist(emails map[string] bool, ignoreId int) (string, error) {
func (s *InboundService) checkEmailsExist(emails map[string]bool, ignoreId int) (string, error) {
db := database.GetDB()
var inbounds []*model.Inbound
db = db.Model(model.Inbound{}).Where("Protocol in ?", []model.Protocol{model.VMess, model.VLESS})
if (ignoreId > 0) {
var inbounds []*model.Inbound
db = db.Model(model.Inbound{}).Where("Protocol in ?", []model.Protocol{model.VMess, model.VLESS, model.Trojan})
if ignoreId > 0 {
db = db.Where("id != ?", ignoreId)
}
db = db.Find(&inbounds)
@@ -96,25 +96,25 @@ func (s *InboundService) checkEmailExistForInbound(inbound *model.Inbound) (stri
if err != nil {
return "", err
}
emails := make(map[string] bool)
emails := make(map[string]bool)
for _, client := range clients {
if (client.Email != "") {
if client.Email != "" {
if emails[client.Email] {
return client.Email, nil
}
emails[client.Email] = true;
emails[client.Email] = true
}
}
return s.checkEmailsExist(emails, inbound.Id)
}
func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound,error) {
func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, error) {
exist, err := s.checkPortExist(inbound.Port, 0)
if err != nil {
return inbound, err
}
if exist {
return inbound, common.NewError("端口已存在:", inbound.Port)
return inbound, common.NewError("Port already exists:", inbound.Port)
}
existEmail, err := s.checkEmailExistForInbound(inbound)
@@ -129,7 +129,7 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound,erro
err = db.Save(inbound).Error
if err == nil {
s.UpdateClientStat(inbound.Id,inbound.Settings)
s.UpdateClientStat(inbound.Id, inbound.Settings)
}
return inbound, err
}
@@ -141,7 +141,7 @@ func (s *InboundService) AddInbounds(inbounds []*model.Inbound) error {
return err
}
if exist {
return common.NewError("端口已存在:", inbound.Port)
return common.NewError("Port already exists:", inbound.Port)
}
}
@@ -187,9 +187,9 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
return inbound, err
}
if exist {
return inbound, common.NewError("端口已存在:", inbound.Port)
return inbound, common.NewError("Port already exists:", inbound.Port)
}
existEmail, err := s.checkEmailExistForInbound(inbound)
if err != nil {
return inbound, err
@@ -216,7 +216,7 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
oldInbound.Sniffing = inbound.Sniffing
oldInbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
s.UpdateClientStat(inbound.Id,inbound.Settings)
s.UpdateClientStat(inbound.Id, inbound.Settings)
db := database.GetDB()
return inbound, db.Save(oldInbound).Error
}
@@ -276,13 +276,13 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
for _, traffic := range traffics {
inbound := &model.Inbound{}
err := txInbound.Where("settings like ?", "%" + traffic.Email + "%").First(inbound).Error
err := txInbound.Where("settings like ?", "%"+traffic.Email+"%").First(inbound).Error
traffic.InboundId = inbound.Id
if err != nil {
if err == gorm.ErrRecordNotFound {
// delete removed client record
clientErr := s.DelClientStat(tx, traffic.Email)
logger.Warning(err, traffic.Email,clientErr)
logger.Warning(err, traffic.Email, clientErr)
}
continue
@@ -298,19 +298,19 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
}
}
if tx.Where("inbound_id = ?", inbound.Id).Where("email = ?", traffic.Email).
UpdateColumn("enable", true).
UpdateColumn("expiry_time", traffic.ExpiryTime).
UpdateColumn("total",traffic.Total).
UpdateColumn("up", gorm.Expr("up + ?", traffic.Up)).
UpdateColumn("down", gorm.Expr("down + ?", traffic.Down)).RowsAffected == 0 {
UpdateColumn("enable", true).
UpdateColumn("expiry_time", traffic.ExpiryTime).
UpdateColumn("total", traffic.Total).
UpdateColumn("up", gorm.Expr("up + ?", traffic.Up)).
UpdateColumn("down", gorm.Expr("down + ?", traffic.Down)).RowsAffected == 0 {
err = tx.Create(traffic).Error
}
if err != nil {
logger.Warning("AddClientTraffic update data ", err)
continue
}
}
return
}
@@ -335,7 +335,7 @@ func (s *InboundService) DisableInvalidClients() (int64, error) {
count := result.RowsAffected
return count, err
}
func (s *InboundService) UpdateClientStat(inboundId int, inboundSettings string) (error) {
func (s *InboundService) UpdateClientStat(inboundId int, inboundSettings string) error {
db := database.GetDB()
// get settings clients
@@ -344,8 +344,8 @@ func (s *InboundService) UpdateClientStat(inboundId int, inboundSettings string)
clients := settings["clients"]
for _, client := range clients {
result := db.Model(xray.ClientTraffic{}).
Where("inbound_id = ? and email = ?", inboundId, client.Email).
Updates(map[string]interface{}{"enable": true, "total": client.TotalGB, "expiry_time": client.ExpiryTime})
Where("inbound_id = ? and email = ?", inboundId, client.Email).
Updates(map[string]interface{}{"enable": true, "total": client.TotalGB, "expiry_time": client.ExpiryTime})
if result.RowsAffected == 0 {
clientTraffic := xray.ClientTraffic{}
clientTraffic.InboundId = inboundId
@@ -361,7 +361,7 @@ func (s *InboundService) UpdateClientStat(inboundId int, inboundSettings string)
if err != nil {
return err
}
}
return nil
}
@@ -369,30 +369,7 @@ func (s *InboundService) DelClientStat(tx *gorm.DB, email string) error {
return tx.Where("email = ?", email).Delete(xray.ClientTraffic{}).Error
}
func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error) {
db := database.GetDB()
InboundClientIps := &model.InboundClientIps{}
err := db.Model(model.InboundClientIps{}).Where("client_email = ?", clientEmail).First(InboundClientIps).Error
if err != nil {
return "", err
}
return InboundClientIps.Ips, nil
}
func (s *InboundService) ClearClientIps(clientEmail string) (error) {
db := database.GetDB()
result := db.Model(model.InboundClientIps{}).
Where("client_email = ?", clientEmail).
Update("ips", "")
err := result.Error
if err != nil {
return err
}
return nil
}
func (s *InboundService) ResetClientTraffic(clientEmail string) (error) {
func (s *InboundService) ResetClientTraffic(clientEmail string) error {
db := database.GetDB()
result := db.Model(xray.ClientTraffic{}).
@@ -402,7 +379,6 @@ func (s *InboundService) ResetClientTraffic(clientEmail string) (error) {
err := result.Error
if err != nil {
return err
}
@@ -413,7 +389,7 @@ func (s *InboundService) GetClientTrafficById(uuid string) (traffic *xray.Client
inbound := &model.Inbound{}
traffic = &xray.ClientTraffic{}
err = db.Model(model.Inbound{}).Where("settings like ?", "%" + uuid + "%").First(inbound).Error
err = db.Model(model.Inbound{}).Where("settings like ?", "%"+uuid+"%").First(inbound).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
logger.Warning(err)

View File

@@ -5,12 +5,6 @@ import (
"bytes"
"encoding/json"
"fmt"
"github.com/shirou/gopsutil/cpu"
"github.com/shirou/gopsutil/disk"
"github.com/shirou/gopsutil/host"
"github.com/shirou/gopsutil/load"
"github.com/shirou/gopsutil/mem"
"github.com/shirou/gopsutil/net"
"io"
"io/fs"
"net/http"
@@ -20,6 +14,13 @@ import (
"x-ui/logger"
"x-ui/util/sys"
"x-ui/xray"
"github.com/shirou/gopsutil/cpu"
"github.com/shirou/gopsutil/disk"
"github.com/shirou/gopsutil/host"
"github.com/shirou/gopsutil/load"
"github.com/shirou/gopsutil/mem"
"github.com/shirou/gopsutil/net"
)
type ProcessState string
@@ -171,7 +172,7 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
}
func (s *ServerService) GetXrayVersions() ([]string, error) {
url := "https://api.github.com/repos/hossinasaadi/Xray-core/releases"
url := "https://api.github.com/repos/XTLS/Xray-core/releases"
resp, err := http.Get(url)
if err != nil {
return nil, err
@@ -214,7 +215,7 @@ func (s *ServerService) downloadXRay(version string) (string, error) {
}
fileName := fmt.Sprintf("Xray-%s-%s.zip", osName, arch)
url := fmt.Sprintf("https://github.com/hossinasaadi/Xray-core/releases/download/%s/%s", version, fileName)
url := fmt.Sprintf("https://github.com/XTLS/Xray-core/releases/download/%s/%s", version, fileName)
resp, err := http.Get(url)
if err != nil {
return "", err

View File

@@ -6,6 +6,7 @@ import (
"sync"
"x-ui/logger"
"x-ui/xray"
"go.uber.org/atomic"
)
@@ -50,6 +51,7 @@ func (s *XrayService) GetXrayVersion() string {
}
return p.GetVersion()
}
func RemoveIndex(s []interface{}, index int) []interface{} {
return append(s[:index], s[index+1:]...)
}
@@ -79,25 +81,24 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
// get settings clients
settings := map[string]interface{}{}
json.Unmarshal([]byte(inbound.Settings), &settings)
clients, ok := settings["clients"].([]interface{})
clients, ok := settings["clients"].([]interface{})
if ok {
// check users active or not
clientStats := inbound.ClientStats
for _, clientTraffic := range clientStats {
for index, client := range clients {
c := client.(map[string]interface{})
if c["email"] == clientTraffic.Email {
if ! clientTraffic.Enable {
clients = RemoveIndex(clients,index)
logger.Info("Remove Inbound User",c["email"] ,"due the expire or traffic limit")
if !clientTraffic.Enable {
clients = RemoveIndex(clients, index)
logger.Info("Remove Inbound User", c["email"], "due the expire or traffic limit")
}
}
}
}
settings["clients"] = clients
@@ -105,7 +106,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
if err != nil {
return nil, err
}
inbound.Settings = string(modifiedSettings)
}
inboundConfig := inbound.GenXrayInboundConfig()

View File

@@ -31,8 +31,8 @@
"host" = "host"
"path" = "path"
"camouflage" = "camouflage"
"turnOn" = "turn on"
"closure" = "closure"
"enabled" = "enabled"
"disabled" = "disabled"
"domainName" = "domain name"
"additional" = "alter"
"monitor" = "Listen IP"
@@ -41,6 +41,7 @@
"success" = " success"
"getVersion" = "get version"
"install" = "install"
"clients" = "Clients"
[menu]
"dashboard" = "System Status"
@@ -55,9 +56,9 @@
[pages.login.toasts]
"invalidFormData" = "Input Data Format Is Invalid"
"emptyUsername" = "please Enter Username"
"emptyPassword" = "please Enter Password"
"wrongUsernameOrPassword" = "invalid username or password"
"emptyUsername" = "Please Enter Username"
"emptyPassword" = "Please Enter Password"
"wrongUsernameOrPassword" = "Invalid username or password"
"successLogin" = "Login"
@@ -122,6 +123,7 @@
"publicKeyContent" = "public key content"
"keyPath" = "key file path"
"keyContent" = "key content"
"clickOnQRcode" = "click on QR Code to Copy"
[pages.inbounds.toasts]
"obtain" = "Obtain"

View File

@@ -0,0 +1,191 @@
"username" = "نام کاربری"
"password" = "رمز عبور"
"login" = "ورود"
"confirm" = "تایید"
"cancel" = "انصراف"
"close" = "بستن"
"copy" = "کپی"
"copied" = "کپی شد"
"download" = "دانلود"
"remark" = "نام"
"enable" = "فعال"
"protocol" = "پروتکل"
"loading" = "در حال بروزرسانی..."
"second" = "ثانیه"
"minute" = "دقیقه"
"hour" = "ساعت"
"day" = "روز"
"check" = "چک کردن"
"indefinitely" = "نامحدود"
"unlimited" = "نامحدود"
"none" = "هیچ"
"qrCode" = "QR کد"
"edit" = "ویرایش"
"delete" = "حذف"
"reset" = "ریست"
"copySuccess" = "با موفقیت کپی شد"
"sure" = "مطمئن"
"encryption" = "رمزگذاری"
"transmission" = "راه اتصال"
"host" = "آدرس"
"path" = "مسیر"
"camouflage" = "استتار"
"enabled" = "فعال"
"disabled" = "disabled"
"domainName" = "آدرس دامنه"
"additional" = "آی دی جایگزین"
"monitor" = "آی پی اتصال"
"certificate" = "سرتیفیکیت"
"fail" = "خطا"
"success" = " موفق"
"getVersion" = "دریافت ورژن"
"install" = "نصب"
"clients" = "کاربران"
[menu]
"dashboard" = "وضعیت سیستم"
"inbounds" = "سروریس ها"
"setting" = "تنظیمات پنل"
"logout" = "خروج"
"link" = "دیگر"
[pages.login]
"title" = "ورود به سیستم X-UI"
"loginAgain" = "مدت زمان استفاده به اتمام رسیده ، لطفا دوباره وارد شوید"
[pages.login.toasts]
"invalidFormData" = "اطلاعات وارد شده به صورت درست وارد نشده است"
"emptyUsername" = "نام کاربری خالی میباشد"
"emptyPassword" = "رمز عبور خالی میباشد"
"wrongUsernameOrPassword" = "نام کاربری و رمز عبور اشتباه میباشد"
"successLogin" = "خوش آمدید"
[pages.index]
"title" = "وضعیت سیستم"
"memory" = "حافظه رم"
"hard" = "حافظه دیسک"
"xrayStatus" = "وضعیت Xray"
"xraySwitch" = "تغییر ورژن"
"xraySwitchClick" = "ورژن مورد نظر را انتخاب کنید"
"xraySwitchClickDesk" = "لطفا با دقت انتخاب کنید ، در صورت انتخاب اشتباه امکان قطعی سیستم وجود دارد ."
"operationHours" = "ساعت فعال"
"operationHoursDesc" = "ساعت فعال بعد از شروع سیستم"
"systemLoad" = "سرعت لود سیستم"
"connectionCount" = "تعداد کانکشن ها"
"connectionCountDesc" = "تعداد کانکشن ها برای کل شبکه"
"upSpeed" = "سرعت آپلود در حال حاضر سیستم"
"downSpeed" = "سرعت دانلود در حال حاضر سیستم"
"totalSent" = "جمع کل ترافیک آپلود مصرفی"
"totalReceive" = "جمع کل ترافیک دانلود مصرفی"
"xraySwitchVersionDialog" = "تغییر ورژن Xray"
"xraySwitchVersionDialogDesc" = "آیا از تغییر ورژن مطمئن هستین"
"dontRefreshh" = "در حال نصب ، لطفا رفرش نکنید "
[pages.inbounds]
"title" = "کاربران"
"totalDownUp" = "جمع آپلود/دانلود"
"totalUsage" = "جمع کل"
"inboundCount" = "تعداد سرویس ها"
"operate" = "عملیات"
"enable" = "فعال"
"remark" = "نام"
"protocol" = "پروتکل"
"port" = "پورت"
"traffic" = "ترافیک"
"details" = "توضیحات"
"transportConfig" = "نحوه اتصال"
"expireDate" = "تاریخ انقضا"
"resetTraffic" = "ریست ترافیک"
"addInbound" = "اضافه کردن سرویس"
"addTo" = "اضافه کردن"
"revise" = "ویرایش"
"modifyInbound" = "ویرایش سرویس"
"deleteInbound" = "حذف سرویس"
"deleteInboundContent" = "آیا مطمئن به حذف سرویس هستید ؟"
"resetTrafficContent" = "آیا مطمئن به ریست ترافیک هستید ؟"
"copyLink" = "کپی لینک"
"address" = "آدرس"
"network" = "شبکه"
"destinationPort" = "پورت مقصد"
"targetAddress" = "آدرس مقصد"
"disableInsecureEncryption" = "رمزگذاری ناامن را غیرفعال کنید"
"monitorDesc" = "به طور پیش فرض خالی بگذارید"
"meansNoLimit" = "یعنی بدون محدودیت"
"totalFlow" = "کل ترافیک"
"leaveBlankToNeverExpire" = "خالی بگذارید تا هرگز منقضی نشود"
"noRecommendKeepDefault" = "توصیه می شود به عنوان پیش فرض حفظ شود"
"certificatePath" = "مسیر فایل گواهی"
"certificateContent" = "محتوای فایل گواهی"
"publicKeyPath" = "مسیر فایل Certificate.crt"
"publicKeyContent" = "محتوای Certificate.crt"
"keyPath" = "مسیر فایل Private.key"
"keyContent" = "محتوای Private.key"
"clickOnQRcode" = "برای کپی بر روی کد تصویری کلیک کنید"
[pages.inbounds.toasts]
"obtain" = "Obtain"
[pages.inbounds.stream.general]
"requestHeader" = "درخواست سربرگ"
"name" = "نام"
"value" = "مقدار"
[pages.inbounds.stream.tcp]
"requestVersion" = "ورژن درخواست"
"requestMethod" = "متد درخواست"
"requestPath" = "مسیر درخواست"
"responseVersion" = "ورژن پاسخ"
"responseStatus" = "وضعیت پاسخ"
"responseStatusDescription" = "توضیحات وضعیت پاسخ"
"responseHeader" = "سربرگ پاسخ"
[pages.inbounds.stream.quic]
"encryption" = "رمزنگاری"
[pages.setting]
"title" = "تنظیمات"
"save" = "ذخیره"
"restartPanel" = "ریستارت پنل"
"restartPanelDesc" = "آیا مطمئن هستید که می خواهید پنل را دوباره راه اندازی کنید؟ برای راه اندازی مجدد روی OK کلیک کنید. اگر بعد از 3 ثانیه نمی توانید به پنل دسترسی پیدا کنید، لطفاً برای مشاهده اطلاعات گزارش پانل به سرور برگردید"
"panelConfig" = "تنظیمات پنل"
"userSetting" = "تنظیمات مدیر"
"xrayConfiguration" = "تنظیمات Xray"
"TGReminder" = "تنظیمات ربات تلگرام"
"otherSetting" = "دیگر تنظیمات"
"panelListeningIP" = "محدودیت آی پی پنل"
"panelListeningIPDesc" = "برای استفاده از تمام IP ها به طور پیش فرض خالی بگذارید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"panelPort" = "پورت پنل"
"panelPortDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
"publicKeyPath" = "مسیر فایل پنل Certificate.crt"
"publicKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود . پنل را مجدداً راه اندازی کنید تا اعمال شود"
"privateKeyPath" = "مسیر فایل پنل private.key"
"privateKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود . پنل را مجدداً راه اندازی کنید تا اعمال شود"
"panelUrlPath" = "آدرس روت پنل"
"panelUrlPathDesc" = "باید با '/' شروع شود و با '/' تمام شود. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"oldUsername" = "نام کاربری فعلی"
"currentPassword" = "رمز عبور فعلی"
"newUsername" = "نام کاربری جدید"
"newPassword" = "رمز عبور جدید"
"xrayConfigTemplate" = "تنظیمات قالب Xray"
"xrayConfigTemplateDesc" = "فایل پیکربندی xray نهایی را بر اساس این الگو ایجاد کنید. لطفاً این را تغییر ندهید مگر اینکه دقیقاً بدانید که چه کاری انجام می دهید! پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramBotEnable" = "فعالسازی ربات تلگرام"
"telegramBotEnableDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramToken" = "توکن تلگرام"
"telegramTokenDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramChatId" = "آی دی تلگرام مدیریت . از ربات @getidsbot آی دی خود را دریافت کنید"
"telegramChatIdDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام"
"telegramNotifyTimeDesc" = "از فرمت زمان بندی Crontab استفاده کنید . پنل را مجدداً راه اندازی کنید تا اعمال شود"
"timeZonee" = "منظقه زمانی"
"timeZoneDesc" = "وظایف برنامه ریزی شده بر اساس این منطقه زمانی اجرا می شوند. پنل را مجدداً راه اندازی می کند تا اعمال شود"
[pages.setting.toasts]
"modifySetting" = "ویرایش تنظیمات"
"getSetting" = "دریافت تنظیمات"
"modifyUser" = "ویرایش کاربر"
"originalUserPassIncorrect" = "نام کاربری و رمز عبور فعلی اشتباه می باشد ."
"userPassMustBeNotEmpty" = "نام کاربری و رمز عبور جدید نمیتواند خالی باشد ."

View File

@@ -31,8 +31,8 @@
"host" = "主持人"
"path" = "小路"
"camouflage" = "伪装"
"turnOn" = "开启"
"closure" = "关闭"
"enabled" = "开启"
"disabled" = "关闭"
"domainName" = "域名"
"additional" = "额外"
"monitor" = "监听"
@@ -41,6 +41,7 @@
"success" = "成功"
"getVersion" = "获取版本"
"install" = "安装"
"clients" = "客户端"
[menu]
"dashboard" = "系统状态"
@@ -121,6 +122,7 @@
"publicKeyContent" = "公钥内容"
"keyPath" = "密钥文件路径"
"keyContent" = "密钥内容"
"clickOnQRcode" = "click on QR Code to Copy"
[pages.inbounds.toasts]
"obtain" = "获取"

View File

@@ -1,55 +0,0 @@
"username" = "用戶名"
"password" = "密碼"
"login" = "登錄"
"confirm" = "確定"
"cancel" = "取消"
"close" = "關閉"
"copy" = "複製"
"copied" = "已複製"
"download" = "下載"
"remark" = "備註"
"enable" = "啟用"
"protocol" = "協議"
[menu]
"dashboard" = "系统状态"
"inbounds" = "入站列表"
"setting" = "面板设置"
"logout" = "退出登录"
"link" = "其他"
[pages.login]
"title" = "登錄"
[pages.login.toasts]
"invalidFormData" = "数据格式错误"
"emptyUsername" = "请输入用户名"
"emptyPassword" = "请输入密码"
"wrongUsernameOrPassword" = "用户名或密码错误"
"successLogin" = "登录"
[pages.index]
"title" = "系统状态"
[pages.inbounds]
"title" = "入站列表"
[pages.inbounds.stream.general]
"requestHeader" = "request header"
"name" = "name"
"value" = "value"
[pages.inbounds.stream.tcp]
"requestVersion" = "request version"
"requestMethod" = "request method"
"requestPath" = "request path"
"responseVersion" = "response version"
"responseStatus" = "response status"
"responseStatusDescription" = "response status description"
"responseHeader" = "response header"
[pages.inbounds.stream.quic]
"encryption" = "encryption"
[pages.setting]
"title" = "设置"

View File

@@ -271,7 +271,7 @@ func (s *Server) initI18n(engine *gin.Engine) error {
})
}
engine.FuncMap["i18n"] = I18n;
engine.FuncMap["i18n"] = I18n
engine.Use(func(c *gin.Context) {
//accept := c.GetHeader("Accept-Language")
@@ -286,7 +286,7 @@ func (s *Server) initI18n(engine *gin.Engine) error {
localizer = i18n.NewLocalizer(bundle, lang)
c.Set("localizer", localizer)
c.Set("I18n" , I18n)
c.Set("I18n", I18n)
c.Next()
})
@@ -310,9 +310,6 @@ func (s *Server) startTask() {
// 每 30 秒检查一次 inbound 流量超出和到期的情况
s.cron.AddJob("@every 30s", job.NewCheckInboundJob())
// check client ips from log file every 10 sec
s.cron.AddJob("@every 10s", job.NewCheckClientIpJob())
// 每一天提示一次流量情况,上海时间8点30
var entry cron.EntryID
isTgbotenabled, err := s.settingService.GetTgbotenabled()

View File

@@ -8,6 +8,7 @@ Environment="XRAY_VMESS_AEAD_FORCED=false"
Type=simple
WorkingDirectory=/usr/local/x-ui/
ExecStart=/usr/local/x-ui/x-ui
Restart=on-failure
[Install]
WantedBy=multi-user.target

View File

@@ -94,7 +94,7 @@ before_show_menu() {
}
install() {
bash <(curl -Ls https://raw.githubusercontent.com/hossinasaadi/x-ui/main/install.sh)
bash <(curl -Ls https://raw.githubusercontent.com/alireza0/x-ui/main/install.sh)
if [[ $? == 0 ]]; then
if [[ $# == 0 ]]; then
start
@@ -113,7 +113,7 @@ update() {
fi
return 0
fi
bash <(curl -Ls https://raw.githubusercontent.com/hossinasaadi/x-ui/main/install.sh)
bash <(curl -Ls https://raw.githubusercontent.com/alireza0/x-ui/main/install.sh)
if [[ $? == 0 ]]; then
LOGI "Update is complete, Panel has automatically restarted "
exit 0
@@ -302,7 +302,7 @@ install_bbr() {
}
update_shell() {
wget -O /usr/bin/x-ui -N --no-check-certificate https://github.com/hossinasaadi/x-ui/raw/main/x-ui.sh
wget -O /usr/bin/x-ui -N --no-check-certificate https://github.com/alireza0/x-ui/raw/main/x-ui.sh
if [[ $? != 0 ]]; then
echo ""
LOGE "Failed to download scriptPlease check whether the machine can connect Github"

View File

@@ -154,15 +154,15 @@ func (p *process) Start() (err error) {
data, err := json.MarshalIndent(p.config, "", " ")
if err != nil {
return common.NewErrorf("生成 xray 配置文件失败: %v", err)
return common.NewErrorf("Failure to generate XRAY configuration files: %v", err)
}
configPath := GetConfigPath()
err = os.WriteFile(configPath, data, fs.ModePerm)
if err != nil {
return common.NewErrorf("写入配置文件失败: %v", err)
return common.NewErrorf("Write the configuration file failed: %v", err)
}
cmd := exec.Command(GetBinaryPath(), "-c", configPath, "-restrictedIPsPath", "./bin/blockedIPs")
cmd := exec.Command(GetBinaryPath(), "-c", configPath)
p.cmd = cmd
stdReader, err := cmd.StdoutPipe()
@@ -262,18 +262,18 @@ func (p *process) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) {
matchs := ClientTrafficRegex.FindStringSubmatch(stat.Name)
if len(matchs) < 3 {
continue
}else {
} else {
isUser := matchs[1] == "user"
email := matchs[2]
isDown := matchs[3] == "downlink"
if ! isUser {
if !isUser {
continue
}
traffic, ok := emailTrafficMap[email]
if !ok {
traffic = &ClientTraffic{
Email: email,
Email: email,
}
emailTrafficMap[email] = traffic
clientTraffics = append(clientTraffics, traffic)
@@ -283,7 +283,7 @@ func (p *process) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) {
} else {
traffic.Up = stat.Value
}
}
continue
}