Add trojan multiuser

This commit is contained in:
Alireza Ahmadi
2023-02-07 00:37:01 +01:00
parent 117de52d7d
commit 8114e3fc17
42 changed files with 1534 additions and 1633 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: workflow_dispatch:
jobs: jobs:
release: release:
runs-on: ubuntu-18.04 runs-on: ubuntu-22.04
outputs: outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
steps: steps:
@@ -23,13 +23,13 @@ jobs:
linuxamd64build: linuxamd64build:
name: build x-ui amd64 version name: build x-ui amd64 version
needs: release needs: release
runs-on: ubuntu-18.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: 1.18 go-version: '1.20'
- name: build linux amd64 version - name: build linux amd64 version
run: | run: |
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o xui-release -v main.go 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 mv xui-release x-ui
mkdir bin mkdir bin
cd 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 unzip Xray-linux-64.zip
rm -f Xray-linux-64.zip geoip.dat geosite.dat rm -f Xray-linux-64.zip geoip.dat geosite.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
@@ -63,13 +63,13 @@ jobs:
linuxarm64build: linuxarm64build:
name: build x-ui arm64 version name: build x-ui arm64 version
needs: release needs: release
runs-on: ubuntu-18.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: 1.18 go-version: '1.20'
- name: build linux arm64 version - name: build linux arm64 version
run: | run: |
sudo apt-get update sudo apt-get update
@@ -83,7 +83,7 @@ jobs:
mv xui-release x-ui mv xui-release x-ui
mkdir bin mkdir bin
cd 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 unzip Xray-linux-arm64-v8a.zip
rm -f Xray-linux-arm64-v8a.zip geoip.dat geosite.dat rm -f Xray-linux-arm64-v8a.zip geoip.dat geosite.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
@@ -105,13 +105,13 @@ jobs:
linuxs390xbuild: linuxs390xbuild:
name: build x-ui s390x version name: build x-ui s390x version
needs: release needs: release
runs-on: ubuntu-18.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: 1.18 go-version: '1.20'
- name: build linux s390x version - name: build linux s390x version
run: | run: |
sudo apt-get update sudo apt-get update
@@ -125,7 +125,7 @@ jobs:
mv xui-release x-ui mv xui-release x-ui
mkdir bin mkdir bin
cd 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 unzip Xray-linux-s390x.zip
rm -f Xray-linux-s390x.zip geoip.dat geosite.dat rm -f Xray-linux-s390x.zip geoip.dat geosite.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat

1
.gitignore vendored
View File

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

View File

@@ -5,11 +5,11 @@ RUN go build main.go
FROM debian:11-slim 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 ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends -y ca-certificates \ 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/* && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
ENV TZ=Asia/Shanghai ENV TZ=Asia/Tehran
WORKDIR /root WORKDIR /root
COPY --from=builder /root/main /root/x-ui COPY --from=builder /root/main /root/x-ui
COPY ./bin/. /root/bin/. COPY ./bin/. /root/bin/.

View File

@@ -2,16 +2,15 @@
> **Disclaimer: This project is only for personal learning and communication, please do not use it for illegal purposes, please do not use it in a production environment** > **Disclaimer: This project is only for personal learning and communication, please do not use it for illegal purposes, please do not use it in a production environment**
xray panel supporting multi-protocol, **Multi-lang (English,Chinese)**, **IP Restrication Per Inbound** xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese)**
| Features | Enable? | | Features | Enable? |
| ------------- |:-------------:| | ------------- |:-------------:|
| Multi-lang | :heavy_check_mark: | | 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/alireza0/x-ui/#enable-multi-user-traffic--exprire-day) | :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/alireza0/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/alireza0/x-ui/pull/51) | :heavy_check_mark: |
| [REST API](https://github.com/HexaSoftwareTech/x-ui/pull/51) | :heavy_check_mark: | | [Telegram BOT](https://github.com/alireza0/x-ui/pull/110) | :heavy_check_mark: |
| [Telegram BOT](https://github.com/HexaSoftwareTech/x-ui/pull/110) | :heavy_check_mark: |
**If you think this project is helpful to you, you may wish to give a** :star2: **If you think this project is helpful to you, you may wish to give a** :star2:
@@ -27,24 +26,6 @@ xray panel supporting multi-protocol, **Multi-lang (English,Chinese)**, **IP Res
- Support one-click SSL certificate application and automatic renewal - Support one-click SSL certificate application and automatic renewal
- For more advanced configuration items, please refer to the panel - For more advanced configuration items, please refer to the panel
# Enable IP Restrictions Per Inbound
`!!! NO NEED TO DO THIS IF YOU HAVE FRESH INSTALL`
1 - open panel settings and tab xray related settings find `"api": ` and put bellow code just before it :
```json
"log": {
"loglevel": "warning",
"access": "./access.log"
},
```
- change access log path as you want
2 - add **IP limit and Unique Email** for inbound(vmess & vless)
# 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)
`!!! NO NEED TO DO THIS IF YOU HAVE FRESH INSTALL`
**for enable traffic for users you should do :** **for enable traffic for users you should do :**
@@ -85,12 +66,12 @@ find this in config :
# Install & Upgrade # 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 ## 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 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 > If your server cpu architecture is not `amd64` replace another architecture
@@ -110,7 +91,7 @@ systemctl restart x-ui
## Install using docker ## 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 1. install docker
@@ -126,7 +107,7 @@ docker run -itd --network=host \
-v $PWD/db/:/etc/x-ui/ \ -v $PWD/db/:/etc/x-ui/ \
-v $PWD/cert/:/root/cert/ \ -v $PWD/cert/:/root/cert/ \
--name x-ui --restart=unless-stopped \ --name x-ui --restart=unless-stopped \
HexaSoftwareTech/x-ui:latest alireza0/x-ui:latest
``` ```
> Build your own image > Build your own image
@@ -207,4 +188,4 @@ x-ui v2-ui
## Stargazers over time ## 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 package database
import ( import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"io/fs" "io/fs"
"os" "os"
"path" "path"
"x-ui/config" "x-ui/config"
"x-ui/xray"
"x-ui/database/model" "x-ui/database/model"
"x-ui/xray"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
) )
var db *gorm.DB var db *gorm.DB
@@ -41,9 +42,7 @@ func initInbound() error {
func initSetting() error { func initSetting() error {
return db.AutoMigrate(&model.Setting{}) return db.AutoMigrate(&model.Setting{})
} }
func initInboundClientIps() error {
return db.AutoMigrate(&model.InboundClientIps{})
}
func initClientTraffic() error { func initClientTraffic() error {
return db.AutoMigrate(&xray.ClientTraffic{}) return db.AutoMigrate(&xray.ClientTraffic{})
} }
@@ -83,15 +82,12 @@ func InitDB(dbPath string) error {
if err != nil { if err != nil {
return err return err
} }
err = initInboundClientIps()
if err != nil {
return err
}
err = initClientTraffic() err = initClientTraffic()
if err != nil { if err != nil {
return err return err
} }
return nil return nil
} }

View File

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

View File

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

View File

@@ -107,20 +107,20 @@ install_x-ui() {
cd /usr/local/ cd /usr/local/
if [ $# == 0 ]; then 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 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}" echo -e "${red}refresh x-ui version failed,it may due to Github API restriction,please try it later${plain}"
exit 1 exit 1
fi fi
echo -e "get x-ui latest version succeed: ${last_version}, begin to install..." 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 if [[ $? -ne 0 ]]; then
echo -e "${red}dowanload x-ui failed,please be sure that your server can access Github ${plain}" echo -e "${red}dowanload x-ui failed,please be sure that your server can access Github ${plain}"
exit 1 exit 1
fi fi
else else
last_version=$1 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" echo -e "begin to install x-ui v$1"
wget -N --no-check-certificate -O /usr/local/x-ui-linux-${arch}.tar.gz ${url} wget -N --no-check-certificate -O /usr/local/x-ui-linux-${arch}.tar.gz ${url}
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
@@ -138,15 +138,15 @@ install_x-ui() {
cd x-ui cd x-ui
chmod +x x-ui bin/xray-linux-${arch} chmod +x x-ui bin/xray-linux-${arch}
cp -f x-ui.service /etc/systemd/system/ 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/local/x-ui/x-ui.sh
chmod +x /usr/bin/x-ui chmod +x /usr/bin/x-ui
config_after_install config_after_install
#echo -e "如果是全新安装,默认网页端口为 ${green}54321${plain},用户名和密码默认都是 ${green}admin${plain}" #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 "请自行确保此端口没有被其他程序占用,${yellow}并且确保 54321 端口已放行${plain}" #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 "若想将 54321 修改为其它端口,输入 x-ui 命令进行修改,同样也要确保你修改的端口也是放行的" # 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 "如果是更新面板,则按你之前的方式访问面板" #echo -e "If it is updated panel, access the panel in your previous way"
#echo -e "" #echo -e ""
systemctl daemon-reload systemctl daemon-reload
systemctl enable x-ui 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", value : "en-US",
icon : "🇺🇸" icon : "🇺🇸"
}, },
{
name : "Farsi",
value : "fa_IR",
icon : "🇮🇷"
},
{ {
name : "汉语", name : "汉语",
value : "zh-Hans", value : "zh-Hans",

View File

@@ -825,6 +825,10 @@ class Inbound extends XrayCommonClass {
if(this.settings.vlesses[index]._expiryTime != null) if(this.settings.vlesses[index]._expiryTime != null)
return this.settings.vlesses[index]._expiryTime < new Date().getTime(); return this.settings.vlesses[index]._expiryTime < new Date().getTime();
return false return false
case Protocols.TROJAN:
if(this.settings.trojans[index]._expiryTime != null)
return this.settings.trojans[index]._expiryTime < new Date().getTime();
return false
default: default:
return false; return false;
} }
@@ -1053,9 +1057,9 @@ class Inbound extends XrayCommonClass {
+ '#' + encodeURIComponent(remark); + '#' + encodeURIComponent(remark);
} }
genTrojanLink(address='', remark='') { genTrojanLink(address='', remark='', clientIndex=0) {
let settings = this.settings; 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) { genLink(address='', remark='', clientIndex=0) {
@@ -1063,7 +1067,7 @@ class Inbound extends XrayCommonClass {
case Protocols.VMESS: return this.genVmessLink(address, remark, clientIndex); case Protocols.VMESS: return this.genVmessLink(address, remark, clientIndex);
case Protocols.VLESS: return this.genVLESSLink(address, remark, clientIndex); case Protocols.VLESS: return this.genVLESSLink(address, remark, clientIndex);
case Protocols.SHADOWSOCKS: return this.genSSLink(address, remark); 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 ''; default: return '';
} }
} }
@@ -1181,12 +1185,11 @@ Inbound.VmessSettings = class extends Inbound.Settings {
} }
}; };
Inbound.VmessSettings.Vmess = class extends XrayCommonClass { 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(); super();
this.id = id; this.id = id;
this.alterId = alterId; this.alterId = alterId;
this.email = email; this.email = email;
this.limitIp = limitIp;
this.totalGB = totalGB; this.totalGB = totalGB;
this.expiryTime = expiryTime; this.expiryTime = expiryTime;
} }
@@ -1196,7 +1199,6 @@ Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
json.id, json.id,
json.alterId, json.alterId,
json.email, json.email,
json.limitIp,
json.totalGB, json.totalGB,
json.expiryTime, json.expiryTime,
@@ -1265,12 +1267,11 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
}; };
Inbound.VLESSSettings.VLESS = class extends XrayCommonClass { 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(); super();
this.id = id; this.id = id;
this.flow = flow; this.flow = flow;
this.email = email; this.email = email;
this.limitIp = limitIp;
this.totalGB = totalGB; this.totalGB = totalGB;
this.expiryTime = expiryTime; this.expiryTime = expiryTime;
@@ -1281,7 +1282,6 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
json.id, json.id,
json.flow, json.flow,
json.email, json.email,
json.limitIp,
json.totalGB, json.totalGB,
json.expiryTime, json.expiryTime,
@@ -1351,10 +1351,10 @@ Inbound.VLESSSettings.Fallback = class extends XrayCommonClass {
Inbound.TrojanSettings = class extends Inbound.Settings { Inbound.TrojanSettings = class extends Inbound.Settings {
constructor(protocol, constructor(protocol,
clients=[new Inbound.TrojanSettings.Client()], trojans=[new Inbound.TrojanSettings.Trojan()],
fallbacks=[],) { fallbacks=[],) {
super(protocol); super(protocol);
this.clients = clients; this.trojans = trojans;
this.fallbacks = fallbacks; this.fallbacks = fallbacks;
} }
@@ -1366,45 +1366,73 @@ Inbound.TrojanSettings = class extends Inbound.Settings {
this.fallbacks.splice(index, 1); 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() { toJson() {
return { return {
clients: Inbound.TrojanSettings.toJsonArray(this.clients), clients: Inbound.TrojanSettings.toJsonArray(this.trojans),
fallbacks: Inbound.TrojanSettings.toJsonArray(this.fallbacks), 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 { Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
constructor(password=RandomUtil.randomSeq(10), flow=FLOW_CONTROL.DIRECT) { constructor(password=RandomUtil.randomSeq(10), flow=FLOW_CONTROL.DIRECT, email='', totalGB=0, expiryTime='') {
super(); super();
this.password = password; this.password = password;
this.flow = flow; this.flow = flow;
this.email = email;
this.totalGB = totalGB;
this.expiryTime = expiryTime;
} }
toJson() { toJson() {
return { return {
password: this.password, password: this.password,
flow: this.flow, flow: this.flow,
email: this.email,
totalGB: this.totalGB,
expiryTime: this.expiryTime,
}; };
} }
static fromJson(json={}) { static fromJson(json={}) {
return new Inbound.TrojanSettings.Client( return new Inbound.TrojanSettings.Trojan(
json.password, json.password,
json.flow, 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 { Inbound.TrojanSettings.Fallback = class extends XrayCommonClass {

View File

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

View File

@@ -2,8 +2,8 @@
<a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title" <a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title"
:closable="true" width="300px" :ok-text="qrModal.okText" :closable="true" width="300px" :ok-text="qrModal.okText"
cancel-text='{{ i18n "close" }}' :ok-button-props="{attrs:{id:'qr-modal-ok-btn'}}"> 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> <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" id="qrCode" style="width: 100%; height: 100%;"></canvas> <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"> <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> <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> <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" /> <a-divider style="height: 2px; background-color: #7e7e7e" />
</template> </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> </a-modal>
<script> <script>
@@ -110,6 +116,13 @@
this.setQrCode("qrCode-vless-" + vlesses[index].id ,index) this.setQrCode("qrCode-vless-" + vlesses[index].id ,index)
} }
break; 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; default: return null;
} }

View File

@@ -13,14 +13,14 @@
</a-menu-item> </a-menu-item>
<!--<a-menu-item key="{{ .base_path }}xui/clients">--> <!--<a-menu-item key="{{ .base_path }}xui/clients">-->
<!-- <a-icon type="laptop"></a-icon>--> <!-- <a-icon type="laptop"></a-icon>-->
<!-- <span>客户端</span>--> <!-- <span>Client</span>-->
<!--</a-menu-item>--> <!--</a-menu-item>-->
<a-sub-menu> <a-sub-menu>
<template slot="title"> <template slot="title">
<a-icon type="link"></a-icon> <a-icon type="link"></a-icon>
<span>{{ i18n "menu.link"}}</span> <span>{{ i18n "menu.link"}}</span>
</template> </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> <a-icon type="github"></a-icon>
<span>Github</span> <span>Github</span>
</a-menu-item> </a-menu-item>

View File

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

View File

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

View File

@@ -1,15 +1,109 @@
{{define "form/trojan"}} {{define "form/trojan"}}
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label='{{ i18n "password" }}'> <label>{{ i18n "clients"}} </label>
<a-input v-model.trim="inbound.settings.clients[0].password"></a-input> <a-collapse activeKey="0" v-for="(trojan, index) in inbound.settings.trojans"
</a-form-item> :key="`trojan-${index}`">
<a-form-item v-if="inbound.xtls" label="flow">
<a-select v-model="inbound.settings.clients[0].flow" style="width: 150px"> <a-collapse-panel :header="getHeaderText(trojan.email)">
<a-select-option value="">{{ i18n "none" }}</a-select-option> <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-select-option v-for="key in FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> <a-form layout="inline">
</a-select> <a-form-item>
</a-form-item> <span slot="label">
</a-form> 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 layout="inline">
<a-form-item label="fallbacks"> <a-form-item label="fallbacks">

View File

@@ -1,5 +1,6 @@
{{define "form/vless"}} {{define "form/vless"}}
<a-form layout="inline"> <a-form layout="inline">
<label>{{ i18n "clients"}}</label>
<a-collapse activeKey="0" v-for="(vless, index) in inbound.settings.vlesses" <a-collapse activeKey="0" v-for="(vless, index) in inbound.settings.vlesses"
:key="`vless-${index}`"> :key="`vless-${index}`">
@@ -22,43 +23,6 @@
</span> </span>
<a-input v-model.trim="vless.email"></a-input> <a-input v-model.trim="vless.email"></a-input>
</a-form-item> </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>
<a-form-item label="id"> <a-form-item label="id">
<a-input v-model.trim="vless.id"></a-input> <a-input v-model.trim="vless.id"></a-input>
@@ -105,23 +69,7 @@
</a-tooltip> </a-tooltip>
<a-tag color="blue">[[ sizeFormat(getUpStats(vless.email)) ]] / [[ sizeFormat(getDownStats(vless.email)) ]]</a-tag> <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 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"> <a-tag v-show="inbound.settings.vlesses.length > 1">
<svg <svg
@click="removeClient(index, inbound.settings.vlesses)" @click="removeClient(index, inbound.settings.vlesses)"
@@ -139,9 +87,24 @@
</svg> </svg>
</a-tag> </a-tag>
</a-form> </a-form>
</a-collapse-panel>
</a-collapse>
</a-form> <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 layout="inline">
<a-form-item label="fallbacks"> <a-form-item label="fallbacks">

View File

@@ -1,5 +1,6 @@
{{define "form/vmess"}} {{define "form/vmess"}}
<a-form layout="inline"> <a-form layout="inline">
<label>{{ i18n "clients"}}</label>
<a-collapse activeKey="0" v-for="(vmess, index) in inbound.settings.vmesses" <a-collapse activeKey="0" v-for="(vmess, index) in inbound.settings.vmesses"
:key="`vmess-${index}`"> :key="`vmess-${index}`">
<a-collapse-panel :header="getHeaderText(vmess.email)"> <a-collapse-panel :header="getHeaderText(vmess.email)">
@@ -21,42 +22,6 @@
</span> </span>
<a-input v-model.trim="vmess.email"></a-input> <a-input v-model.trim="vmess.email"></a-input>
</a-form-item> </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>
<a-form-item label="id"> <a-form-item label="id">
<a-input v-model.trim="vmess.id"></a-input> <a-input v-model.trim="vmess.id"></a-input>
@@ -100,26 +65,7 @@
</a-tooltip> </a-tooltip>
<a-tag color="blue">[[ sizeFormat(getUpStats(vmess.email)) ]] / [[ sizeFormat(getDownStats(vmess.email)) ]]</a-tag> <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 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"> <a-tag v-show="inbound.settings.vmesses.length > 1">
<!--Remove Svg Icon--> <!--Remove Svg Icon-->
<svg <svg
@click="removeClient(index, inbound.settings.vmesses)" @click="removeClient(index, inbound.settings.vmesses)"
@@ -143,9 +89,23 @@
</a-collapse> </a-collapse>
<a-tag>
</a-form> <svg
</a-form> @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 layout="inline">
<a-form-item label='{{ i18n "pages.inbounds.disableInsecureEncryption" }}'> <a-form-item label='{{ i18n "pages.inbounds.disableInsecureEncryption" }}'>
<a-switch v-model.number="inbound.settings.disableInsecure"></a-switch> <a-switch v-model.number="inbound.settings.disableInsecure"></a-switch>

View File

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

View File

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

View File

@@ -77,41 +77,17 @@
this.inModal.inbound.tls = false; this.inModal.inbound.tls = false;
} }
}, },
addClient(protocol,value, clients) { addClient(protocol, clients) {
switch (protocol) { switch (protocol) {
case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.Vmess()); case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.Vmess());
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS()); case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
default: return null; default: return null;
} }
}, },
removeClient(index, clients) { removeClient(index, clients) {
clients.splice(index, 1); 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) { async resetClientTraffic(client,event) {
const msg = await HttpUtil.post('/xui/inbound/resetClientTraffic/'+ client.email); const msg = await HttpUtil.post('/xui/inbound/resetClientTraffic/'+ client.email);
if (!msg.success) { if (!msg.success) {

View File

@@ -46,8 +46,8 @@
<div slot="title"> <div slot="title">
<a-button type="primary" icon="plus" @click="openAddInbound"></a-button> <a-button type="primary" icon="plus" @click="openAddInbound"></a-button>
</div> </div>
<!-- <a-input v-model="searchKey" placeholder="搜索" autofocus style="max-width: 300px"></a-input>--> <!-- <a-input v-model="searchKey" placeholder="search" autofocus style="max-width: 300px"></a-input>-->
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id" <a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
:data-source="dbInbounds" :data-source="dbInbounds"
:loading="spinning" :scroll="{ x: 1500 }" :loading="spinning" :scroll="{ x: 1500 }"
:pagination="false" :pagination="false"
@@ -55,7 +55,7 @@
@change="() => getDBInbounds()"> @change="() => getDBInbounds()">
<template slot="action" slot-scope="text, dbInbound"> <template slot="action" slot-scope="text, dbInbound">
<a-dropdown :trigger="['click']"> <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 slot="overlay" @click="a => clickAction(a, dbInbound)">
<a-menu-item v-if="dbInbound.hasLink()" key="qrcode"> <a-menu-item v-if="dbInbound.hasLink()" key="qrcode">
<a-icon type="qrcode"></a-icon> <a-icon type="qrcode"></a-icon>
@@ -112,6 +112,11 @@
</template> </template>
<a-tag v-else color="green">{{ i18n "indefinitely" }}</a-tag> <a-tag v-else color="green">{{ i18n "indefinitely" }}</a-tag>
</template> </template>
<template #expandedRowRender="{ record }">
<p style="margin: 0">
{{ i18n "none" }}
</p>
</template>
</a-table> </a-table>
</a-card> </a-card>
</transition> </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 package service
import ( import (
"encoding/json"
"fmt" "fmt"
"time" "time"
"x-ui/database" "x-ui/database"
"encoding/json"
"x-ui/database/model" "x-ui/database/model"
"x-ui/logger"
"x-ui/util/common" "x-ui/util/common"
"x-ui/xray" "x-ui/xray"
"x-ui/logger"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -64,11 +64,11 @@ func (s *InboundService) getClients(inbound *model.Inbound) ([]model.Client, err
return clients, nil 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() db := database.GetDB()
var inbounds []*model.Inbound var inbounds []*model.Inbound
db = db.Model(model.Inbound{}).Where("Protocol in ?", []model.Protocol{model.VMess, model.VLESS}) db = db.Model(model.Inbound{}).Where("Protocol in ?", []model.Protocol{model.VMess, model.VLESS, model.Trojan})
if (ignoreId > 0) { if ignoreId > 0 {
db = db.Where("id != ?", ignoreId) db = db.Where("id != ?", ignoreId)
} }
db = db.Find(&inbounds) db = db.Find(&inbounds)
@@ -96,25 +96,25 @@ func (s *InboundService) checkEmailExistForInbound(inbound *model.Inbound) (stri
if err != nil { if err != nil {
return "", err return "", err
} }
emails := make(map[string] bool) emails := make(map[string]bool)
for _, client := range clients { for _, client := range clients {
if (client.Email != "") { if client.Email != "" {
if emails[client.Email] { if emails[client.Email] {
return client.Email, nil return client.Email, nil
} }
emails[client.Email] = true; emails[client.Email] = true
} }
} }
return s.checkEmailsExist(emails, inbound.Id) 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) exist, err := s.checkPortExist(inbound.Port, 0)
if err != nil { if err != nil {
return inbound, err return inbound, err
} }
if exist { if exist {
return inbound, common.NewError("端口已存在:", inbound.Port) return inbound, common.NewError("Port already exists:", inbound.Port)
} }
existEmail, err := s.checkEmailExistForInbound(inbound) existEmail, err := s.checkEmailExistForInbound(inbound)
@@ -129,7 +129,7 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound,erro
err = db.Save(inbound).Error err = db.Save(inbound).Error
if err == nil { if err == nil {
s.UpdateClientStat(inbound.Id,inbound.Settings) s.UpdateClientStat(inbound.Id, inbound.Settings)
} }
return inbound, err return inbound, err
} }
@@ -141,7 +141,7 @@ func (s *InboundService) AddInbounds(inbounds []*model.Inbound) error {
return err return err
} }
if exist { 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 return inbound, err
} }
if exist { if exist {
return inbound, common.NewError("端口已存在:", inbound.Port) return inbound, common.NewError("Port already exists:", inbound.Port)
} }
existEmail, err := s.checkEmailExistForInbound(inbound) existEmail, err := s.checkEmailExistForInbound(inbound)
if err != nil { if err != nil {
return inbound, err return inbound, err
@@ -216,7 +216,7 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
oldInbound.Sniffing = inbound.Sniffing oldInbound.Sniffing = inbound.Sniffing
oldInbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port) oldInbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
s.UpdateClientStat(inbound.Id,inbound.Settings) s.UpdateClientStat(inbound.Id, inbound.Settings)
db := database.GetDB() db := database.GetDB()
return inbound, db.Save(oldInbound).Error return inbound, db.Save(oldInbound).Error
} }
@@ -276,13 +276,13 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
for _, traffic := range traffics { for _, traffic := range traffics {
inbound := &model.Inbound{} 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 traffic.InboundId = inbound.Id
if err != nil { if err != nil {
if err == gorm.ErrRecordNotFound { if err == gorm.ErrRecordNotFound {
// delete removed client record // delete removed client record
clientErr := s.DelClientStat(tx, traffic.Email) clientErr := s.DelClientStat(tx, traffic.Email)
logger.Warning(err, traffic.Email,clientErr) logger.Warning(err, traffic.Email, clientErr)
} }
continue 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). if tx.Where("inbound_id = ?", inbound.Id).Where("email = ?", traffic.Email).
UpdateColumn("enable", true). UpdateColumn("enable", true).
UpdateColumn("expiry_time", traffic.ExpiryTime). UpdateColumn("expiry_time", traffic.ExpiryTime).
UpdateColumn("total",traffic.Total). UpdateColumn("total", traffic.Total).
UpdateColumn("up", gorm.Expr("up + ?", traffic.Up)). UpdateColumn("up", gorm.Expr("up + ?", traffic.Up)).
UpdateColumn("down", gorm.Expr("down + ?", traffic.Down)).RowsAffected == 0 { UpdateColumn("down", gorm.Expr("down + ?", traffic.Down)).RowsAffected == 0 {
err = tx.Create(traffic).Error err = tx.Create(traffic).Error
} }
if err != nil { if err != nil {
logger.Warning("AddClientTraffic update data ", err) logger.Warning("AddClientTraffic update data ", err)
continue continue
} }
} }
return return
} }
@@ -335,7 +335,7 @@ func (s *InboundService) DisableInvalidClients() (int64, error) {
count := result.RowsAffected count := result.RowsAffected
return count, err return count, err
} }
func (s *InboundService) UpdateClientStat(inboundId int, inboundSettings string) (error) { func (s *InboundService) UpdateClientStat(inboundId int, inboundSettings string) error {
db := database.GetDB() db := database.GetDB()
// get settings clients // get settings clients
@@ -344,8 +344,8 @@ func (s *InboundService) UpdateClientStat(inboundId int, inboundSettings string)
clients := settings["clients"] clients := settings["clients"]
for _, client := range clients { for _, client := range clients {
result := db.Model(xray.ClientTraffic{}). result := db.Model(xray.ClientTraffic{}).
Where("inbound_id = ? and email = ?", inboundId, client.Email). Where("inbound_id = ? and email = ?", inboundId, client.Email).
Updates(map[string]interface{}{"enable": true, "total": client.TotalGB, "expiry_time": client.ExpiryTime}) Updates(map[string]interface{}{"enable": true, "total": client.TotalGB, "expiry_time": client.ExpiryTime})
if result.RowsAffected == 0 { if result.RowsAffected == 0 {
clientTraffic := xray.ClientTraffic{} clientTraffic := xray.ClientTraffic{}
clientTraffic.InboundId = inboundId clientTraffic.InboundId = inboundId
@@ -361,7 +361,7 @@ func (s *InboundService) UpdateClientStat(inboundId int, inboundSettings string)
if err != nil { if err != nil {
return err return err
} }
} }
return nil 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 return tx.Where("email = ?", email).Delete(xray.ClientTraffic{}).Error
} }
func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error) { func (s *InboundService) ResetClientTraffic(clientEmail 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) {
db := database.GetDB() db := database.GetDB()
result := db.Model(xray.ClientTraffic{}). result := db.Model(xray.ClientTraffic{}).
@@ -402,7 +379,6 @@ func (s *InboundService) ResetClientTraffic(clientEmail string) (error) {
err := result.Error err := result.Error
if err != nil { if err != nil {
return err return err
} }
@@ -413,7 +389,7 @@ func (s *InboundService) GetClientTrafficById(uuid string) (traffic *xray.Client
inbound := &model.Inbound{} inbound := &model.Inbound{}
traffic = &xray.ClientTraffic{} 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 != nil {
if err == gorm.ErrRecordNotFound { if err == gorm.ErrRecordNotFound {
logger.Warning(err) logger.Warning(err)

View File

@@ -5,12 +5,6 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "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"
"io/fs" "io/fs"
"net/http" "net/http"
@@ -20,6 +14,13 @@ import (
"x-ui/logger" "x-ui/logger"
"x-ui/util/sys" "x-ui/util/sys"
"x-ui/xray" "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 type ProcessState string
@@ -171,7 +172,7 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
} }
func (s *ServerService) GetXrayVersions() ([]string, error) { 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) resp, err := http.Get(url)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -214,7 +215,7 @@ func (s *ServerService) downloadXRay(version string) (string, error) {
} }
fileName := fmt.Sprintf("Xray-%s-%s.zip", osName, arch) 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) resp, err := http.Get(url)
if err != nil { if err != nil {
return "", err return "", err

View File

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

View File

@@ -31,8 +31,8 @@
"host" = "host" "host" = "host"
"path" = "path" "path" = "path"
"camouflage" = "camouflage" "camouflage" = "camouflage"
"turnOn" = "turn on" "enabled" = "enabled"
"closure" = "closure" "disabled" = "disabled"
"domainName" = "domain name" "domainName" = "domain name"
"additional" = "alter" "additional" = "alter"
"monitor" = "Listen IP" "monitor" = "Listen IP"
@@ -41,6 +41,7 @@
"success" = " success" "success" = " success"
"getVersion" = "get version" "getVersion" = "get version"
"install" = "install" "install" = "install"
"clients" = "Clients"
[menu] [menu]
"dashboard" = "System Status" "dashboard" = "System Status"
@@ -55,9 +56,9 @@
[pages.login.toasts] [pages.login.toasts]
"invalidFormData" = "Input Data Format Is Invalid" "invalidFormData" = "Input Data Format Is Invalid"
"emptyUsername" = "please Enter Username" "emptyUsername" = "Please Enter Username"
"emptyPassword" = "please Enter Password" "emptyPassword" = "Please Enter Password"
"wrongUsernameOrPassword" = "invalid username or password" "wrongUsernameOrPassword" = "Invalid username or password"
"successLogin" = "Login" "successLogin" = "Login"
@@ -122,6 +123,7 @@
"publicKeyContent" = "public key content" "publicKeyContent" = "public key content"
"keyPath" = "key file path" "keyPath" = "key file path"
"keyContent" = "key content" "keyContent" = "key content"
"clickOnQRcode" = "click on QR Code to Copy"
[pages.inbounds.toasts] [pages.inbounds.toasts]
"obtain" = "Obtain" "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" = "主持人" "host" = "主持人"
"path" = "小路" "path" = "小路"
"camouflage" = "伪装" "camouflage" = "伪装"
"turnOn" = "开启" "enabled" = "开启"
"closure" = "关闭" "disabled" = "关闭"
"domainName" = "域名" "domainName" = "域名"
"additional" = "额外" "additional" = "额外"
"monitor" = "监听" "monitor" = "监听"
@@ -41,6 +41,7 @@
"success" = "成功" "success" = "成功"
"getVersion" = "获取版本" "getVersion" = "获取版本"
"install" = "安装" "install" = "安装"
"clients" = "客户端"
[menu] [menu]
"dashboard" = "系统状态" "dashboard" = "系统状态"
@@ -121,6 +122,7 @@
"publicKeyContent" = "公钥内容" "publicKeyContent" = "公钥内容"
"keyPath" = "密钥文件路径" "keyPath" = "密钥文件路径"
"keyContent" = "密钥内容" "keyContent" = "密钥内容"
"clickOnQRcode" = "click on QR Code to Copy"
[pages.inbounds.toasts] [pages.inbounds.toasts]
"obtain" = "获取" "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) { engine.Use(func(c *gin.Context) {
//accept := c.GetHeader("Accept-Language") //accept := c.GetHeader("Accept-Language")
@@ -286,7 +286,7 @@ func (s *Server) initI18n(engine *gin.Engine) error {
localizer = i18n.NewLocalizer(bundle, lang) localizer = i18n.NewLocalizer(bundle, lang)
c.Set("localizer", localizer) c.Set("localizer", localizer)
c.Set("I18n" , I18n) c.Set("I18n", I18n)
c.Next() c.Next()
}) })
@@ -310,9 +310,6 @@ func (s *Server) startTask() {
// 每 30 秒检查一次 inbound 流量超出和到期的情况 // 每 30 秒检查一次 inbound 流量超出和到期的情况
s.cron.AddJob("@every 30s", job.NewCheckInboundJob()) 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 // 每一天提示一次流量情况,上海时间8点30
var entry cron.EntryID var entry cron.EntryID
isTgbotenabled, err := s.settingService.GetTgbotenabled() isTgbotenabled, err := s.settingService.GetTgbotenabled()

View File

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

View File

@@ -94,7 +94,7 @@ before_show_menu() {
} }
install() { 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
if [[ $# == 0 ]]; then if [[ $# == 0 ]]; then
start start
@@ -113,7 +113,7 @@ update() {
fi fi
return 0 return 0
fi 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 if [[ $? == 0 ]]; then
LOGI "Update is complete, Panel has automatically restarted " LOGI "Update is complete, Panel has automatically restarted "
exit 0 exit 0
@@ -302,7 +302,7 @@ install_bbr() {
} }
update_shell() { 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 if [[ $? != 0 ]]; then
echo "" echo ""
LOGE "Failed to download scriptPlease check whether the machine can connect Github" 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, "", " ") data, err := json.MarshalIndent(p.config, "", " ")
if err != nil { if err != nil {
return common.NewErrorf("生成 xray 配置文件失败: %v", err) return common.NewErrorf("Failure to generate XRAY configuration files: %v", err)
} }
configPath := GetConfigPath() configPath := GetConfigPath()
err = os.WriteFile(configPath, data, fs.ModePerm) err = os.WriteFile(configPath, data, fs.ModePerm)
if err != nil { 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 p.cmd = cmd
stdReader, err := cmd.StdoutPipe() stdReader, err := cmd.StdoutPipe()
@@ -262,18 +262,18 @@ func (p *process) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) {
matchs := ClientTrafficRegex.FindStringSubmatch(stat.Name) matchs := ClientTrafficRegex.FindStringSubmatch(stat.Name)
if len(matchs) < 3 { if len(matchs) < 3 {
continue continue
}else { } else {
isUser := matchs[1] == "user" isUser := matchs[1] == "user"
email := matchs[2] email := matchs[2]
isDown := matchs[3] == "downlink" isDown := matchs[3] == "downlink"
if ! isUser { if !isUser {
continue continue
} }
traffic, ok := emailTrafficMap[email] traffic, ok := emailTrafficMap[email]
if !ok { if !ok {
traffic = &ClientTraffic{ traffic = &ClientTraffic{
Email: email, Email: email,
} }
emailTrafficMap[email] = traffic emailTrafficMap[email] = traffic
clientTraffics = append(clientTraffics, traffic) clientTraffics = append(clientTraffics, traffic)
@@ -283,7 +283,7 @@ func (p *process) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) {
} else { } else {
traffic.Up = stat.Value traffic.Up = stat.Value
} }
} }
continue continue
} }