Compare commits

..

1 Commits

Author SHA1 Message Date
MHSanaei
5ef8a5a37e old design 2023-03-23 23:22:50 +03:30
130 changed files with 3787 additions and 17998 deletions

24
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,24 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the 3x-ui bug**
A clear and concise description of what the bug is.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Server (please complete the following information):**
- OS: [e.g. iOS]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

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

View File

@@ -1,41 +0,0 @@
name: Release X-ui dockerhub
on:
push:
tags:
- "*"
workflow_dispatch:
jobs:
build_and_push:
runs-on: ubuntu-latest
steps:
- name: Check out the code
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.2.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.7.0
- name: Log in to GitHub Container Registry
uses: docker/login-action@v2.2.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v4.6.0
with:
images: ghcr.io/${{ github.repository }}
- name: Build and push Docker image
uses: docker/build-push-action@v4.1.1
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64, linux/arm64/v8
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -1,76 +1,43 @@
name: Release 3X-ui name: Release 3X-ui
on: on:
push:
tags:
- "*"
workflow_dispatch: workflow_dispatch:
jobs: jobs:
build: linuxamd64build:
strategy: name: build x-ui amd64 version
matrix:
platform: [amd64, arm64]
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- name: Checkout repository - uses: actions/checkout@v3.4.0
uses: actions/checkout@v3.5.2 - name: Set up Go
uses: actions/setup-go@v4.0.0
- name: Setup Go
uses: actions/setup-go@v4.0.1
with: with:
go-version: 'stable' go-version: 'stable'
- name: build linux amd64 version
- name: Install dependencies for arm64
if: matrix.platform == 'arm64'
run: | run: |
sudo apt-get update CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o xui-release -v main.go
sudo apt install gcc-aarch64-linux-gnu
- name: Build x-ui
run: |
export CGO_ENABLED=1
export GOOS=linux
export GOARCH=${{ matrix.platform }}
if [ "${{ matrix.platform }}" == "arm64" ]; then
export CC=aarch64-linux-gnu-gcc
fi
go build -o xui-release -v main.go
mkdir x-ui mkdir x-ui
cp xui-release x-ui/ cp xui-release x-ui/xui-release
cp x-ui.service x-ui/ cp x-ui.service x-ui/x-ui.service
cp x-ui.sh x-ui/ cp x-ui.sh x-ui/x-ui.sh
mv x-ui/xui-release x-ui/x-ui cd x-ui
mkdir x-ui/bin mv xui-release x-ui
cd x-ui/bin mkdir bin
cd bin
# Download dependencies wget https://github.com/mhsanaei/Xray-core/releases/latest/download/Xray-linux-64.zip
if [ "${{ matrix.platform }}" == "amd64" ]; then unzip Xray-linux-64.zip
wget https://github.com/mhsanaei/Xray-core/releases/latest/download/Xray-linux-64.zip rm -f Xray-linux-64.zip geoip.dat geosite.dat
unzip Xray-linux-64.zip
rm -f Xray-linux-64.zip
else
wget https://github.com/mhsanaei/Xray-core/releases/latest/download/Xray-linux-arm64-v8a.zip
unzip Xray-linux-arm64-v8a.zip
rm -f Xray-linux-arm64-v8a.zip
fi
rm -f geoip.dat geosite.dat iran.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
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
wget https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat mv xray xray-linux-amd64
mv xray xray-linux-${{ matrix.platform }} cd ..
cd ../.. cd ..
- name: package
- name: Package run: tar -zcvf x-ui-linux-amd64.tar.gz x-ui
run: tar -zcvf x-ui-linux-${{ matrix.platform }}.tar.gz x-ui - name: upload
uses: svenstaro/upload-release-action@2.5.0
- name: Upload
uses: svenstaro/upload-release-action@2.6.1
with: with:
repo_token: ${{ secrets.GITHUB_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }} tag: ${{ github.ref }}
file: x-ui-linux-${{ matrix.platform }}.tar.gz file: x-ui-linux-amd64.tar.gz
asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz asset_name: x-ui-linux-amd64.tar.gz
prerelease: true prerelease: true
overwrite: true overwrite: true

17
.gitignore vendored
View File

@@ -1,15 +1,12 @@
.idea .idea
.vscode
.cache
.sync*
*.tar.gz
access.log
error.log
tmp tmp
main
backup/
bin/ bin/
dist/ dist/
release/ x-ui-*.tar.gz
/release.sh
/x-ui /x-ui
/release.sh
.sync*
main
release/
access.log
.cache

View File

@@ -1,22 +0,0 @@
#!/bin/sh
if [ $1 == "amd64" ]; then
ARCH="64";
FNAME="amd64";
elif [ $1 == "arm64" ]; then
ARCH="arm64-v8a"
FNAME="arm64";
else
ARCH="64";
FNAME="amd64";
fi
mkdir -p build/bin
cd build/bin
wget "https://github.com/mhsanaei/xray-core/releases/latest/download/Xray-linux-${ARCH}.zip"
unzip "Xray-linux-${ARCH}.zip"
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat iran.dat
mv xray "xray-linux-${FNAME}"
wget "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat"
wget "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat"
wget "https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat"
cd ../../

View File

@@ -1,20 +0,0 @@
#Build latest x-ui from source
FROM --platform=$BUILDPLATFORM golang:1.20.4-alpine AS builder
WORKDIR /app
ARG TARGETARCH
RUN apk --no-cache --update add build-base gcc wget unzip
COPY . .
RUN env CGO_ENABLED=1 go build -o build/x-ui main.go
RUN ./DockerInit.sh "$TARGETARCH"
#Build app image using latest x-ui
FROM alpine
ENV TZ=Asia/Tehran
WORKDIR /app
RUN apk add ca-certificates tzdata
COPY --from=builder /app/build/ /app/
VOLUME [ "/etc/x-ui" ]
ENTRYPOINT [ "/app/x-ui" ]

265
README.md
View File

@@ -1,43 +1,51 @@
# 3x-ui # 3x-ui
![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)
![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)
![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)
![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)
> **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**
[![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases) xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese)**
[![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](#)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](#)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)
3x-ui panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese,Russian)**
**If you think this project is helpful to you, you may wish to give a** :star2:
**Buy Me a Coffee :**
- Tron USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
# Install & Upgrade # Install & Upgrade
``` ```
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
``` ```
## Install custom version
# Install custom version To install your desired version you can add the version to the end of install command. Example for ver `v1.0.9`:
To install your desired version you can add the version to the end of install command. Example for ver `v1.6.1`:
``` ```
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v1.6.1 bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v1.0.9
``` ```
# SSL # SSL
``` ```
apt-get install certbot -y apt-get install certbot -y
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
certbot renew --dry-run certbot renew --dry-run
``` ```
You also can use `x-ui` menu then select `16. SSL Certificate Management` **If you think this project is helpful to you, you may wish to give a** :star2:
# Default settings
- Port: 2053
- username and password will be generated randomly if you skip to modify your own security(x-ui "7")
- database path: /etc/x-ui/x-ui.db
before you set ssl on settings
- http:// ip or domain:2053/xui
After you set ssl on settings
- https://yourdomain:2053/xui
# Enable Traffic For Users:
**copy and paste to xray Configuration :** (you don't need to do this if you have a fresh install)
- [for enable traffic](https://raw.githubusercontent.com/mhsanaei/3x-ui/main/media/for%20enable%20traffic.txt)
- [for enable traffic+block all iran ip address](https://raw.githubusercontent.com/mhsanaei/3x-ui/main/media/for%20enable%20traffic%2Bblock%20all%20iran%20ip.txt)
# Features # Features
@@ -52,149 +60,8 @@ You also can use `x-ui` menu then select `16. SSL Certificate Management`
- Support https access panel (self-provided domain name + ssl certificate) - Support https access panel (self-provided domain name + ssl certificate)
- 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
- Fix api routes (user setting will create with api)
- Support to change configs by different items provided in panel
- Support export/import database from panel
# Manual Install & Upgrade # Tg robot use
<details>
<summary>Click for Manual Install details</summary>
1. To download the latest version of the compressed package directly to your server, run the following command:
```sh
ARCH=$(uname -m)
[[ "${ARCH}" == "aarch64" || "${ARCH}" == "arm64" ]] && XUI_ARCH="arm64" || XUI_ARCH="amd64"
wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-${XUI_ARCH}.tar.gz
```
2. Once the compressed package is downloaded, execute the following commands to install or upgrade x-ui:
```sh
ARCH=$(uname -m)
[[ "${ARCH}" == "aarch64" || "${ARCH}" == "arm64" ]] && XUI_ARCH="arm64" || XUI_ARCH="amd64"
cd /root/
rm -rf x-ui/ /usr/local/x-ui/ /usr/bin/x-ui
tar zxvf x-ui-linux-${XUI_ARCH}.tar.gz
chmod +x x-ui/x-ui x-ui/bin/xray-linux-* x-ui/x-ui.sh
cp x-ui/x-ui.sh /usr/bin/x-ui
cp -f x-ui/x-ui.service /etc/systemd/system/
mv x-ui/ /usr/local/
systemctl daemon-reload
systemctl enable x-ui
systemctl restart x-ui
```
</details>
# Install with Docker
<details>
<summary>Click for Docker details</summary>
1. Install Docker:
```sh
bash <(curl -sSL https://get.docker.com)
```
2. Clone the Project Repository:
```sh
git clone https://github.com/MHSanaei/3x-ui.git
cd 3x-ui
```
3. Start the Service
```sh
docker compose up -d
```
OR
```sh
docker run -itd \
-e XRAY_VMESS_AEAD_FORCED=false \
-v $PWD/db/:/etc/x-ui/ \
-v $PWD/cert/:/root/cert/ \
--network=host \
--restart=unless-stopped \
--name 3x-ui \
ghcr.io/mhsanaei/3x-ui:latest
```
</details>
# Default settings
<details>
<summary>Click for Default settings details</summary>
- Port: 2053
- username and password will be generated randomly if you skip to modify your own security(x-ui "7")
- database path: /etc/x-ui/x-ui.db
- xray config path: /usr/local/x-ui/bin/config.json
Before you set ssl on settings
- http://ip:2053/panel
- http://domain:2053/panel
After you set ssl on settings
- https://yourdomain:2053/panel
</details>
# Xray Configurations:
<details>
<summary>Click for Xray Configurations details</summary>
**copy and paste to xray Configuration :** (you don't need to do this if you have a fresh install)
- [traffic](./media/configs/traffic.json)
- [traffic + Block all Iran IP address](./media/configs/traffic+block-iran-ip.json)
- [traffic + Block all Iran Domains](./media/configs/traffic+block-iran-domains.json)
- [traffic + Block Ads + Use IPv4 for Google](./media/configs/traffic+block-ads+ipv4-google.json)
- [traffic + Block Ads + Route Google + Netflix + Spotify + OpenAI (ChatGPT) to WARP](./media/configs/traffic+block-ads+warp.json)
</details>
# [WARP Configuration](https://github.com/fscarmen/warp) (Optional)
<details>
<summary>Click for WARP Configuration details</summary>
If you want to use routing to WARP follow steps as below:
1. If you already installed warp, you can uninstall using below command:
```sh
warp u
```
2. Install WARP on **socks proxy mode**:
```sh
bash <(curl -sSL https://gist.githubusercontent.com/hamid-gh98/dc5dd9b0cc5b0412af927b1ccdb294c7/raw/install_warp_proxy.sh)
```
3. Turn on the config you need in panel or [Copy and paste this file to Xray Configuration](./media/configs/traffic+block-ads+warp.json)
Config Features:
- Block Ads
- Route Google + Netflix + Spotify + OpenAI (ChatGPT) to WARP
- Fix Google 403 error
</details>
# Telegram Bot
<details>
<summary>Click for Telegram Bot details</summary>
X-UI supports daily traffic notification, panel login reminder and other functions through the Tg robot. To use the Tg robot, you need to apply for the specific application tutorial. You can refer to the [blog](https://coderfan.net/how-to-use-telegram-bot-to-alarm-you-when-someone-login-into-your-vps.html) X-UI supports daily traffic notification, panel login reminder and other functions through the Tg robot. To use the Tg robot, you need to apply for the specific application tutorial. You can refer to the [blog](https://coderfan.net/how-to-use-telegram-bot-to-alarm-you-when-someone-login-into-your-vps.html)
Set the robot-related parameters in the panel background, including: Set the robot-related parameters in the panel background, including:
@@ -209,11 +76,8 @@ Set the robot-related parameters in the panel background, including:
Reference syntax: Reference syntax:
- 30 \* \* \* \* \* //Notify at the 30s of each point
- 0 \*/10 \* \* \* \* //Notify at the first second of each 10 minutes
- @hourly // hourly notification - @hourly // hourly notification
- @daily // Daily notification (00:00 in the morning) - @daily // Daily notification (00:00 in the morning)
- @weekly // weekly notification
- @every 8h // notify every 8 hours - @every 8h // notify every 8 hours
# Telegram Bot Features # Telegram Bot Features
@@ -222,79 +86,20 @@ Reference syntax:
- Login notification - Login notification
- CPU threshold notification - CPU threshold notification
- Threshold for Expiration time and Traffic to report in advance - Threshold for Expiration time and Traffic to report in advance
- Support client report menu if client's telegram username added to the user's configurations - Support client report if client's telegram username is added to the end of `email` like 'test123@telegram_username'
- Support telegram traffic report searched with UUID (VMESS/VLESS) or Password (TROJAN) - anonymously - Support telegram traffic report searched with UID (VMESS/VLESS) or Password (TROJAN) - anonymously
- Menu based bot - Menu based bot
- Search client by email ( only admin ) - Search client by email ( only admin )
- Check all inbounds - Check all inbounds
- Check server status - Check server status
- Check depleted users - Check Exhausted users
- Receive backup by request and in periodic reports - Receive backup by request and in periodic reports
- Multi language bot
</details>
# API routes
<details>
<summary>Click for API routes details</summary>
- `/login` with `PUSH` user data: `{username: '', password: ''}` for login
- `/panel/api/inbounds` base for following actions:
| Method | Path | Action |
| :----: | ---------------------------------- | ------------------------------------------- |
| `GET` | `"/list"` | Get all inbounds |
| `GET` | `"/get/:id"` | Get inbound with inbound.id |
| `GET` | `"/getClientTraffics/:email"` | Get Client Traffics with email |
| `GET` | `"/createbackup"` | Telegram bot sends backup to admins |
| `POST` | `"/add"` | Add inbound |
| `POST` | `"/del/:id"` | Delete Inbound |
| `POST` | `"/update/:id"` | Update Inbound |
| `POST` | `"/clientIps/:email"` | Client Ip address |
| `POST` | `"/clearClientIps/:email"` | Clear Client Ip address |
| `POST` | `"/addClient"` | Add Client to inbound |
| `POST` | `"/:id/delClient/:clientId"` | Delete Client by clientId\* |
| `POST` | `"/updateClient/:clientId"` | Update Client by clientId\* |
| `POST` | `"/:id/resetClientTraffic/:email"` | Reset Client's Traffic |
| `POST` | `"/resetAllTraffics"` | Reset traffics of all inbounds |
| `POST` | `"/resetAllClientTraffics/:id"` | Reset traffics of all clients in an inbound |
| `POST` | `"/delDepletedClients/:id"` | Delete inbound depleted clients (-1: all) |
\*- The field `clientId` should be filled by:
- `client.id` for VMESS and VLESS
- `client.password` for TROJAN
- `client.email` for Shadowsocks
- [Postman Collection](https://gist.github.com/mehdikhody/9a862801a2e41f6b5fb6bbc7e1326044)
</details>
# Environment Variables
<details>
<summary>Click for Environment Variables details</summary>
| Variable | Type | Default |
| -------------- | :--------------------------------------------: | :------------ |
| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` |
| XUI_DEBUG | `boolean` | `false` |
| XUI_BIN_FOLDER | `string` | `"bin"` |
| XUI_DB_FOLDER | `string` | `"/etc/x-ui"` |
Example:
```sh
XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
```
</details>
# A Special Thanks To # A Special Thanks To
- [alireza0](https://github.com/alireza0/) - [alireza0](https://github.com/alireza0/)
- [HexaSoftwareTech](https://github.com/HexaSoftwareTech/)
# Suggestion System # Suggestion System
- Ubuntu 20.04+ - Ubuntu 20.04+
- Debian 10+ - Debian 10+
- CentOS 8+ - CentOS 8+
@@ -306,8 +111,6 @@ XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
![2](./media/2.png) ![2](./media/2.png)
![3](./media/3.png) ![3](./media/3.png)
![4](./media/4.png) ![4](./media/4.png)
![5](./media/5.png)
![6](./media/6.png)
## Stargazers over time ## Stargazers over time

View File

@@ -16,11 +16,10 @@ var name string
type LogLevel string type LogLevel string
const ( const (
Debug LogLevel = "debug" Debug LogLevel = "debug"
Info LogLevel = "info" Info LogLevel = "info"
Notice LogLevel = "notice" Warn LogLevel = "warn"
Warn LogLevel = "warn" Error LogLevel = "error"
Error LogLevel = "error"
) )
func GetVersion() string { func GetVersion() string {
@@ -46,22 +45,6 @@ func IsDebug() bool {
return os.Getenv("XUI_DEBUG") == "true" return os.Getenv("XUI_DEBUG") == "true"
} }
func GetBinFolderPath() string {
binFolderPath := os.Getenv("XUI_BIN_FOLDER")
if binFolderPath == "" {
binFolderPath = "bin"
}
return binFolderPath
}
func GetDBFolderPath() string {
dbFolderPath := os.Getenv("XUI_DB_FOLDER")
if dbFolderPath == "" {
dbFolderPath = "/etc/x-ui"
}
return dbFolderPath
}
func GetDBPath() string { func GetDBPath() string {
return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName()) return fmt.Sprintf("/etc/%s/%s.db", GetName(), GetName())
} }

View File

@@ -1 +1 @@
1.7.0 1.0.9

View File

@@ -1,8 +1,6 @@
package database package database
import ( import (
"bytes"
"io"
"io/fs" "io/fs"
"os" "os"
"path" "path"
@@ -17,16 +15,7 @@ import (
var db *gorm.DB var db *gorm.DB
var initializers = []func() error{
initUser,
initInbound,
initSetting,
initInboundClientIps,
initClientTraffic,
}
func initUser() error { func initUser() error {
err := db.AutoMigrate(&model.User{}) err := db.AutoMigrate(&model.User{})
if err != nil { if err != nil {
return err return err
@@ -38,9 +27,8 @@ func initUser() error {
} }
if count == 0 { if count == 0 {
user := &model.User{ user := &model.User{
Username: "admin", Username: "admin",
Password: "admin", Password: "admin",
LoginSecret: "",
} }
return db.Create(user).Error return db.Create(user).Error
} }
@@ -63,7 +51,7 @@ func initClientTraffic() error {
func InitDB(dbPath string) error { func InitDB(dbPath string) error {
dir := path.Dir(dbPath) dir := path.Dir(dbPath)
err := os.MkdirAll(dir, fs.ModePerm) err := os.MkdirAll(dir, fs.ModeDir)
if err != nil { if err != nil {
return err return err
} }
@@ -84,10 +72,25 @@ func InitDB(dbPath string) error {
return err return err
} }
for _, initialize := range initializers { err = initUser()
if err := initialize(); err != nil { if err != nil {
return err return err
} }
err = initInbound()
if err != nil {
return err
}
err = initSetting()
if err != nil {
return err
}
err = initInboundClientIps()
if err != nil {
return err
}
err = initClientTraffic()
if err != nil {
return err
} }
return nil return nil
@@ -100,13 +103,3 @@ func GetDB() *gorm.DB {
func IsNotFound(err error) bool { func IsNotFound(err error) bool {
return err == gorm.ErrRecordNotFound return err == gorm.ErrRecordNotFound
} }
func IsSQLiteDB(file io.ReaderAt) (bool, error) {
signature := []byte("SQLite format 3\x00")
buf := make([]byte, len(signature))
_, err := file.ReadAt(buf, 0)
if err != nil {
return false, err
}
return bytes.Equal(buf, signature), nil
}

View File

@@ -18,10 +18,9 @@ const (
) )
type User struct { type User struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"` Id int `json:"id" gorm:"primaryKey;autoIncrement"`
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
LoginSecret string `json:"loginSecret"`
} }
type Inbound struct { type Inbound struct {
@@ -45,9 +44,9 @@ type Inbound struct {
Sniffing string `json:"sniffing" form:"sniffing"` Sniffing string `json:"sniffing" form:"sniffing"`
} }
type InboundClientIps struct { type InboundClientIps struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"` Id int `json:"id" gorm:"primaryKey;autoIncrement"`
ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"` ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"`
Ips string `json:"ips" form:"ips"` Ips string `json:"ips" form:"ips"`
} }
func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig { func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
@@ -74,13 +73,10 @@ type Setting struct {
type Client struct { type Client struct {
ID string `json:"id"` ID string `json:"id"`
Password string `json:"password"` AlterIds uint16 `json:"alterId"`
Flow string `json:"flow"`
Email string `json:"email"` Email string `json:"email"`
LimitIP int `json:"limitIp"` LimitIP int `json:"limitIp"`
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"`
Enable bool `json:"enable" form:"enable"`
TgID string `json:"tgId" form:"tgId"`
SubID string `json:"subId" form:"subId"`
} }

View File

@@ -1,16 +0,0 @@
---
version: "3.9"
services:
3x-ui:
image: ghcr.io/mhsanaei/3x-ui:latest
container_name: 3x-ui
hostname: yourhostname
volumes:
- $PWD/db/:/etc/x-ui/
- $PWD/cert/:/root/cert/
environment:
XRAY_VMESS_AEAD_FORCED: "false"
tty: true
network_mode: host
restart: unless-stopped

79
go.mod
View File

@@ -3,38 +3,34 @@ module x-ui
go 1.20 go 1.20
require ( require (
github.com/Calidity/gin-sessions v1.3.1 github.com/Workiva/go-datastructures v1.0.53
github.com/Workiva/go-datastructures v1.1.0 github.com/gin-contrib/sessions v0.0.4
github.com/gin-gonic/gin v1.9.1 github.com/gin-gonic/gin v1.9.0
github.com/goccy/go-json v0.10.2 github.com/go-cmd/cmd v1.4.1
github.com/mymmrac/telego v0.25.1 github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
github.com/nicksnyder/go-i18n/v2 v2.2.1 github.com/nicksnyder/go-i18n/v2 v2.2.1
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pelletier/go-toml/v2 v2.0.8 github.com/pelletier/go-toml/v2 v2.0.7
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v3 v3.23.5 github.com/shirou/gopsutil/v3 v3.23.2
github.com/xtls/xray-core v1.8.3 github.com/xtls/xray-core v1.8.0
go.uber.org/atomic v1.11.0 go.uber.org/atomic v1.10.0
golang.org/x/text v0.10.0 golang.org/x/text v0.8.0
google.golang.org/grpc v1.56.1 google.golang.org/grpc v1.54.0
gorm.io/driver/sqlite v1.5.2 gorm.io/driver/sqlite v1.4.4
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55 gorm.io/gorm v1.24.6
) )
require ( require (
github.com/BurntSushi/toml v1.3.2 // indirect github.com/BurntSushi/toml v1.2.1 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect github.com/bytedance/sonic v1.8.2 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect
github.com/fasthttp/router v1.4.19 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gaukas/godicttls v0.0.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.1 // indirect github.com/go-playground/validator/v10 v10.11.2 // indirect
github.com/goccy/go-json v0.10.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/golang/protobuf v1.5.3 // indirect
github.com/gorilla/context v1.1.1 // indirect github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect github.com/gorilla/securecookie v1.1.1 // indirect
@@ -42,36 +38,25 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.16.6 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/leodido/go-urn v1.2.1 // indirect
github.com/leodido/go-urn v1.2.4 // indirect github.com/lufia/plan9stats v0.0.0-20230110061619-bbe2e5e100de // indirect
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-sqlite3 v1.14.16 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pires/go-proxyproto v0.7.0 // indirect github.com/pires/go-proxyproto v0.6.2 // indirect
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
github.com/refraction-networking/utls v1.3.2 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/sagernet/sing v0.2.6 // indirect
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/tklauser/go-sysconf v0.3.11 // indirect github.com/tklauser/go-sysconf v0.3.11 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect github.com/tklauser/numcpus v0.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect github.com/ugorji/go/codec v1.2.10 // indirect
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect golang.org/x/arch v0.2.0 // indirect
github.com/valyala/fasthttp v1.48.0 // indirect golang.org/x/crypto v0.7.0 // indirect
github.com/xtls/reality v0.0.0-20230613075828-e07c3b04b983 // indirect golang.org/x/net v0.8.0 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect golang.org/x/sys v0.6.0 // indirect
golang.org/x/arch v0.3.0 // indirect google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect
golang.org/x/crypto v0.10.0 // indirect google.golang.org/protobuf v1.29.1 // indirect
golang.org/x/net v0.11.0 // indirect
golang.org/x/sys v0.9.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

230
go.sum
View File

@@ -1,248 +1,260 @@
github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/Calidity/gin-sessions v1.3.1 h1:nF3dCBWa7TZ4j26iYLwGRmzZy9YODhWoOS3fmi+snyE= github.com/Workiva/go-datastructures v1.0.53 h1:J6Y/52yX10Xc5JjXmGtWoSSxs3mZnGSaq37xZZh7Yig=
github.com/Calidity/gin-sessions v1.3.1/go.mod h1:I0+QE6qkO50TeN/n6If6novvxHk4Isvr23U8EdvPdns= github.com/Workiva/go-datastructures v1.0.53/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A=
github.com/Workiva/go-datastructures v1.1.0 h1:hu20UpgZneBhQ3ZvwiOGlqJSKIosin2Rd5wAKUHEO/k=
github.com/Workiva/go-datastructures v1.1.0/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antonlindstrom/pgstore v0.0.0-20200229204646-b08ebf1105e0/go.mod h1:2Ti6VUHVxpC0VSmTZzEvpzysnaGAfGBOoMIz5ykPyyw=
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.8.2 h1:Eq1oE3xWIBE3tj2ZtJFK1rDAx7+uA4bRytozVhXMHKY=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/bytedance/sonic v1.8.2/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0= github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0=
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/fasthttp/router v1.4.19 h1:RLE539IU/S4kfb4MP56zgP0TIBU9kEg0ID9GpWO0vqk=
github.com/fasthttp/router v1.4.19/go.mod h1:+Fh3YOd8x1+he6ZS+d2iUDBH9MGGZ1xQFUor0DE9rKE=
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gaukas/godicttls v0.0.3 h1:YNDIf0d9adcxOijiLrEzpfZGAkNwLRzPaG6OjU7EITk=
github.com/gaukas/godicttls v0.0.3/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4= github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
github.com/gin-contrib/sessions v0.0.4 h1:gq4fNa1Zmp564iHP5G6EBuktilEos8VKhe2sza1KMgo=
github.com/gin-contrib/sessions v0.0.4/go.mod h1:pQ3sIyviBBGcxgyR8mkeJuXbeV3h3NYmhJADQTq5+Vo=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-cmd/cmd v1.4.1 h1:JUcEIE84v8DSy02XTZpUDeGKExk2oW3DA10hTjbQwmc=
github.com/go-cmd/cmd v1.4.1/go.mod h1:tbBenttXtZU4c5djS1o7PWL5pd2xAr5sIqH1kGdNiRc=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M=
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs= github.com/google/pprof v0.0.0-20230228050547-1710fef4ab10 h1:CqYfpuYIjnlNxM3msdyPRKabhXZWbKjf3Q8BWROFBso=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk= github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a h1:N9zuLhTvBSRt0gWSiJswwQ2HqDmtX/ZCDJURnKUt1Ik= github.com/lufia/plan9stats v0.0.0-20230110061619-bbe2e5e100de h1:V53FWzU6KAZVi1tPp5UIsMoUWJ2/PNwYIDXnu7QuBCE=
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE= github.com/lufia/plan9stats v0.0.0-20230110061619-bbe2e5e100de/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
github.com/miekg/dns v1.1.51 h1:0+Xg7vObnhrz/4ZCZcZh7zPXlmU0aveS2HDBd0m0qSo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mymmrac/telego v0.25.1 h1:tMNmrRm0YGyLS56CBi0NDHwO1ZI6V7QMgX4KWSWuT1U=
github.com/mymmrac/telego v0.25.1/go.mod h1:nBO4SUqRV8j60JOS7trIr6bHPofwYCGJxYeqtQWgu2c=
github.com/nicksnyder/go-i18n/v2 v2.2.1 h1:aOzRCdwsJuoExfZhoiXHy4bjruwCMdt5otbYojM/PaA= github.com/nicksnyder/go-i18n/v2 v2.2.1 h1:aOzRCdwsJuoExfZhoiXHy4bjruwCMdt5otbYojM/PaA=
github.com/nicksnyder/go-i18n/v2 v2.2.1/go.mod h1:fF2++lPHlo+/kPaj3nB0uxtPwzlPm+BlgwGX7MkeGj0= github.com/nicksnyder/go-i18n/v2 v2.2.1/go.mod h1:fF2++lPHlo+/kPaj3nB0uxtPwzlPm+BlgwGX7MkeGj0=
github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= github.com/onsi/ginkgo/v2 v2.9.0 h1:Tugw2BKlNHTMfG+CheOITkYvk4LAh6MFOvikhGVnhE8=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs= github.com/pires/go-proxyproto v0.6.2 h1:KAZ7UteSOt6urjme6ZldyFm4wDe/z0ZUP0Yv0Dos0d8=
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4= github.com/pires/go-proxyproto v0.6.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig= github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig=
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U= github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E= github.com/quic-go/qtls-go1-19 v0.2.1 h1:aJcKNMkH5ASEJB9FXNeZCyTEIHU1J7MmHyz1Q1TSG1A=
github.com/quic-go/quic-go v0.35.1 h1:b0kzj6b/cQAf05cT0CkQubHM31wiA+xH3IBkxP62poo= github.com/quic-go/qtls-go1-20 v0.1.1 h1:KbChDlg82d3IHqaj2bn6GfKRj84Per2VGf5XV3wSwQk=
github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8= github.com/quic-go/quic-go v0.33.0 h1:ItNoTDN/Fm/zBlq769lLJc8ECe9gYaW40veHCCco7y0=
github.com/refraction-networking/utls v1.3.2/go.mod h1:fmoaOww2bxzzEpIKOebIsnBvjQpqP7L2vcm/9KUfm/E= github.com/refraction-networking/utls v1.2.3-0.20230308205431-4f1df6c200db h1:ULRv/GPW5KYDafE0FACN2no+HTCyQLUtfyOIeyp3GNc=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/sagernet/sing v0.2.6 h1:Fvqv7/Bwc72ERT6dE8yQLLY6SMc/syO3VMCtxVO4DNw= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/sagernet/sing v0.2.6/go.mod h1:Ta8nHnDLAwqySzKhGoKk4ZIB+vJ3GTKj7UPrWYvM+4w= github.com/sagernet/sing v0.1.7 h1:g4vjr3q8SUlBZSx97Emz5OBfSMBxxW5Q8C2PfdoSo08=
github.com/sagernet/sing-shadowsocks v0.2.2 h1:ezSdVhrmIcwDXmCZF3bOJVMuVtTQWpda+1Op+Ie2TA4= github.com/sagernet/sing-shadowsocks v0.1.1 h1:uFK2rlVeD/b1xhDwSMbUI2goWc6fOKxp+ZeKHZq6C9Q=
github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo= github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U= github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U=
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg= github.com/shirou/gopsutil/v3 v3.23.2 h1:PAWSuiAszn7IhPMBtXsbSCafej7PqUOvY6YywlQUExU=
github.com/shirou/gopsutil/v3 v3.23.5 h1:5SgDCeQ0KW0S4N0znjeM/eFHXXOKyv2dVNgRq/c9P6Y= github.com/shirou/gopsutil/v3 v3.23.2/go.mod h1:gv0aQw33GLo3pG8SiWKiQrbDzbRY1K80RyZJ7V4Th1M=
github.com/shirou/gopsutil/v3 v3.23.5/go.mod h1:Ng3Maa27Q2KARVJ0SPZF5NdrQSC3XHKP8IIWrHgMeLY=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg= github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg=
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q= github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.2.10 h1:eimT6Lsr+2lzmSZxPhLFoOWFmQqwk0fllJJ5hEbTXtQ=
github.com/ugorji/go/codec v1.2.10/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI= github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU= github.com/xtls/reality v0.0.0-20230309125256-0d0713b108c8 h1:LLtLxEe3S0Ko+ckqt4t29RLskpNdOZfgjZCC2/Byr50=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/xtls/xray-core v1.8.0 h1:/OD0sDv6YIBqvE+cVfnqlKrtbMs0Fm9IP5BR5d8Eu4k=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/xtls/xray-core v1.8.0/go.mod h1:i9KWgbLyxg/NT+3+g4nE74Zp3DgTCP3X04YkSfsJeDI=
github.com/valyala/fasthttp v1.48.0 h1:oJWvHb9BIZToTQS3MuQ2R3bJZiNSa2KiNdeI8A+79Tc=
github.com/valyala/fasthttp v1.48.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
github.com/xtls/reality v0.0.0-20230613075828-e07c3b04b983 h1:AMyzgjkh54WocjQSlCnT1LhDc/BKiUqtNOv40AkpURs=
github.com/xtls/reality v0.0.0-20230613075828-e07c3b04b983/go.mod h1:rkuAY1S9F8eI8gDiPDYvACE8e2uwkyg8qoOTuwWov7Y=
github.com/xtls/xray-core v1.8.3 h1:lxaVklPjLKqUU4ua4qH8SBaRcAaNHlH+LmXOx0U/Ejg=
github.com/xtls/xray-core v1.8.3/go.mod h1:i7t4JFnq828P2+XK0XjGQ8W9x78iu+EJ7jI4l3sonIw=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfGcjnmfEJOBgSbemCtg=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.2.0 h1:W1sUEHXiJTfjaFJ5SLo0N6lZn+0eO5gWD1MFeTGqQEY=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.2.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= golang.org/x/exp v0.0.0-20230307190834-24139beb5833 h1:SChBja7BCQewoTAU7IgvucQKMIXrEpFxNMs0spT3/5s=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg= golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc h1:XSJ8Vk1SWuNr8S18z1NZSziL0CPIXLCCMDOEFtHBOFc= google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 h1:DdoeryqhaXp1LtT/emMP1BRJPHHKFi5akj/nbx/zNTA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s=
google.golang.org/grpc v1.56.1 h1:z0dNfjIl0VpaZ9iSVjA6daGatAYwPGstTjt5vkRMFkQ= google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=
google.golang.org/grpc v1.56.1/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.5.2 h1:TpQ+/dqCY4uCigCFyrfnrJnrW9zjpelWVoEVNy5qJkc= gorm.io/driver/sqlite v1.4.4 h1:gIufGoR0dQzjkyqDyYSCvsYR6fba1Gw5YKDqKeChxFc=
gorm.io/driver/sqlite v1.5.2/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4= gorm.io/driver/sqlite v1.4.4/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55 h1:sC1Xj4TYrLqg1n3AN10w871An7wJM0gzgcm8jkIkECQ= gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/gorm v1.24.6 h1:wy98aq9oFEetsc4CAbKD2SoBCdMzsbSIvSUUFJuHi5s=
gorm.io/gorm v1.24.6/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c h1:m5lcgWnL3OElQNVyp3qcncItJ2c0sQlSGjYK2+nJTA4= gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c h1:m5lcgWnL3OElQNVyp3qcncItJ2c0sQlSGjYK2+nJTA4=
lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -23,14 +23,23 @@ else
fi fi
echo "The OS release is: $release" echo "The OS release is: $release"
arch3xui() { arch=$(arch)
case "$(uname -m)" in
x86_64 | x64 | amd64) echo 'amd64' ;; if [[ $arch == "x86_64" || $arch == "x64" || $arch == "amd64" ]]; then
armv8 | arm64 | aarch64) echo 'arm64' ;; arch="amd64"
*) echo -e "${green}Unsupported CPU architecture! ${plain}" && rm -f install.sh && exit 1 ;; elif [[ $arch == "aarch64" || $arch == "arm64" ]]; then
esac arch="arm64"
} else
echo "arch: $(arch3xui)" arch="amd64"
echo -e "${red} Failed to check system arch, will use default arch: ${arch}${plain}"
fi
echo "arch: ${arch}"
if [ $(getconf WORD_BIT) != '32' ] && [ $(getconf LONG_BIT) != '64' ]; then
echo "x-ui dosen't support 32-bit(x86) system, please use 64 bit operating system(x86_64) instead, if there is something wrong, please get in touch with me!"
exit -1
fi
os_version="" os_version=""
os_version=$(grep -i version_id /etc/os-release | cut -d \" -f2 | cut -d . -f1) os_version=$(grep -i version_id /etc/os-release | cut -d \" -f2 | cut -d . -f1)
@@ -39,7 +48,7 @@ if [[ "${release}" == "centos" ]]; then
if [[ ${os_version} -lt 8 ]]; then if [[ ${os_version} -lt 8 ]]; then
echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1 echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1
fi fi
elif [[ "${release}" == "ubuntu" ]]; then elif [[ "${release}" == "ubuntu" ]]; then
if [[ ${os_version} -lt 20 ]]; then if [[ ${os_version} -lt 20 ]]; then
echo -e "${red}please use Ubuntu 20 or higher version${plain}\n" && exit 1 echo -e "${red}please use Ubuntu 20 or higher version${plain}\n" && exit 1
fi fi
@@ -57,22 +66,19 @@ else
echo -e "${red}Failed to check the OS version, please contact the author!${plain}" && exit 1 echo -e "${red}Failed to check the OS version, please contact the author!${plain}" && exit 1
fi fi
install_base() {
case "${release}" in
centos | fedora)
yum install -y -q wget curl tar
;;
*)
apt install -y -q wget curl tar
;;
esac
}
install_base() {
if [[ "${release}" == "centos" ]]; then
yum install wget curl tar -y
else
apt install wget curl tar -y
fi
}
#This function will be called when user installed x-ui out of sercurity #This function will be called when user installed x-ui out of sercurity
config_after_install() { config_after_install() {
echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}" echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}"
read -p "Do you want to continue with the modification [y/n]? ": config_confirm read -p "Do you want to continue with the modification [y/n]? ": config_confirm
if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then if [[ x"${config_confirm}" == x"y" || x"${config_confirm}" == x"Y" ]]; then
read -p "Please set up your username:" config_account read -p "Please set up your username:" config_account
echo -e "${yellow}Your username will be:${config_account}${plain}" echo -e "${yellow}Your username will be:${config_account}${plain}"
read -p "Please set up your password:" config_password read -p "Please set up your password:" config_password
@@ -100,7 +106,6 @@ config_after_install() {
echo -e "${red} this is your upgrade,will keep old settings,if you forgot your login info,you can type x-ui and then type 7 to check${plain}" echo -e "${red} this is your upgrade,will keep old settings,if you forgot your login info,you can type x-ui and then type 7 to check${plain}"
fi fi
fi fi
/usr/local/x-ui/x-ui migrate
} }
install_x-ui() { install_x-ui() {
@@ -114,18 +119,18 @@ install_x-ui() {
exit 1 exit 1
fi fi
echo -e "Got x-ui latest version: ${last_version}, beginning the installation..." echo -e "Got x-ui latest version: ${last_version}, beginning the installation..."
wget -N --no-check-certificate -O /usr/local/x-ui-linux-$(arch3xui).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${last_version}/x-ui-linux-$(arch3xui).tar.gz wget -N --no-check-certificate -O /usr/local/x-ui-linux-${arch}.tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
echo -e "${red}Downloading x-ui failed, please be sure that your server can access Github ${plain}" echo -e "${red}Downloading 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/MHSanaei/3x-ui/releases/download/${last_version}/x-ui-linux-$(arch3xui).tar.gz" url="https://github.com/MHSanaei/3x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz"
echo -e "Begining to install x-ui $1" echo -e "Begining to install x-ui $1"
wget -N --no-check-certificate -O /usr/local/x-ui-linux-$(arch3xui).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
echo -e "${red}Download x-ui $1 failed,please check the version exists ${plain}" echo -e "${red}Download x-ui $1 failed,please check the version exists${plain}"
exit 1 exit 1
fi fi
fi fi
@@ -134,10 +139,10 @@ install_x-ui() {
rm /usr/local/x-ui/ -rf rm /usr/local/x-ui/ -rf
fi fi
tar zxvf x-ui-linux-$(arch3xui).tar.gz tar zxvf x-ui-linux-${arch}.tar.gz
rm x-ui-linux-$(arch3xui).tar.gz -f rm x-ui-linux-${arch}.tar.gz -f
cd x-ui cd x-ui
chmod +x x-ui bin/xray-linux-$(arch3xui) 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/MHSanaei/3x-ui/main/x-ui.sh wget --no-check-certificate -O /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh
chmod +x /usr/local/x-ui/x-ui.sh chmod +x /usr/local/x-ui/x-ui.sh

View File

@@ -1,29 +1,17 @@
package logger package logger
import ( import (
"os"
"sync"
"github.com/op/go-logging" "github.com/op/go-logging"
"os"
) )
var ( var logger *logging.Logger
logger *logging.Logger
mu sync.Mutex
)
func init() { func init() {
InitLogger(logging.INFO) InitLogger(logging.INFO)
} }
func InitLogger(level logging.Level) { func InitLogger(level logging.Level) {
mu.Lock()
defer mu.Unlock()
if logger != nil {
return
}
format := logging.MustStringFormatter( format := logging.MustStringFormatter(
`%{time:2006/01/02 15:04:05} %{level} - %{message}`, `%{time:2006/01/02 15:04:05} %{level} - %{message}`,
) )
@@ -32,67 +20,39 @@ func InitLogger(level logging.Level) {
backendFormatter := logging.NewBackendFormatter(backend, format) backendFormatter := logging.NewBackendFormatter(backend, format)
backendLeveled := logging.AddModuleLevel(backendFormatter) backendLeveled := logging.AddModuleLevel(backendFormatter)
backendLeveled.SetLevel(level, "") backendLeveled.SetLevel(level, "")
newLogger.SetBackend(logging.MultiLogger(backendLeveled)) newLogger.SetBackend(backendLeveled)
logger = newLogger logger = newLogger
} }
func Debug(args ...interface{}) { func Debug(args ...interface{}) {
if logger != nil { logger.Debug(args...)
logger.Debug(args...)
}
} }
func Debugf(format string, args ...interface{}) { func Debugf(format string, args ...interface{}) {
if logger != nil { logger.Debugf(format, args...)
logger.Debugf(format, args...)
}
} }
func Info(args ...interface{}) { func Info(args ...interface{}) {
if logger != nil { logger.Info(args...)
logger.Info(args...)
}
} }
func Infof(format string, args ...interface{}) { func Infof(format string, args ...interface{}) {
if logger != nil { logger.Infof(format, args...)
logger.Infof(format, args...)
}
} }
func Warning(args ...interface{}) { func Warning(args ...interface{}) {
if logger != nil { logger.Warning(args...)
logger.Warning(args...)
}
} }
func Warningf(format string, args ...interface{}) { func Warningf(format string, args ...interface{}) {
if logger != nil { logger.Warningf(format, args...)
logger.Warningf(format, args...)
}
} }
func Error(args ...interface{}) { func Error(args ...interface{}) {
if logger != nil { logger.Error(args...)
logger.Error(args...)
}
} }
func Errorf(format string, args ...interface{}) { func Errorf(format string, args ...interface{}) {
if logger != nil { logger.Errorf(format, args...)
logger.Errorf(format, args...)
}
}
func Notice(args ...interface{}) {
if logger != nil {
logger.Notice(args...)
}
}
func Noticef(format string, args ...interface{}) {
if logger != nil {
logger.Noticef(format, args...)
}
} }

111
main.go
View File

@@ -11,7 +11,7 @@ import (
"x-ui/config" "x-ui/config"
"x-ui/database" "x-ui/database"
"x-ui/logger" "x-ui/logger"
"x-ui/sub" "x-ui/v2ui"
"x-ui/web" "x-ui/web"
"x-ui/web/global" "x-ui/web/global"
"x-ui/web/service" "x-ui/web/service"
@@ -27,8 +27,6 @@ func runWebServer() {
logger.InitLogger(logging.DEBUG) logger.InitLogger(logging.DEBUG)
case config.Info: case config.Info:
logger.InitLogger(logging.INFO) logger.InitLogger(logging.INFO)
case config.Notice:
logger.InitLogger(logging.NOTICE)
case config.Warn: case config.Warn:
logger.InitLogger(logging.WARNING) logger.InitLogger(logging.WARNING)
case config.Error: case config.Error:
@@ -52,19 +50,9 @@ func runWebServer() {
return return
} }
var subServer *sub.Server
subServer = sub.NewServer()
global.SetSubServer(subServer)
err = subServer.Start()
if err != nil {
log.Println(err)
return
}
sigCh := make(chan os.Signal, 1) sigCh := make(chan os.Signal, 1)
// Trap shutdown signals //信号量捕获处理
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM) signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGKILL)
for { for {
sig := <-sigCh sig := <-sigCh
@@ -74,11 +62,6 @@ func runWebServer() {
if err != nil { if err != nil {
logger.Warning("stop server err:", err) logger.Warning("stop server err:", err)
} }
err = subServer.Stop()
if err != nil {
logger.Warning("stop server err:", err)
}
server = web.NewServer() server = web.NewServer()
global.SetWebServer(server) global.SetWebServer(server)
err = server.Start() err = server.Start()
@@ -86,18 +69,8 @@ func runWebServer() {
log.Println(err) log.Println(err)
return return
} }
subServer = sub.NewServer()
global.SetSubServer(subServer)
err = subServer.Start()
if err != nil {
log.Println(err)
return
}
default: default:
server.Stop() server.Stop()
subServer.Stop()
return return
} }
} }
@@ -124,7 +97,7 @@ func showSetting(show bool) {
settingService := service.SettingService{} settingService := service.SettingService{}
port, err := settingService.GetPort() port, err := settingService.GetPort()
if err != nil { if err != nil {
fmt.Println("get current port failed,error info:", err) fmt.Println("get current port fialed,error info:", err)
} }
userService := service.UserService{} userService := service.UserService{}
userModel, err := userService.GetFirstUser() userModel, err := userService.GetFirstUser()
@@ -136,7 +109,7 @@ func showSetting(show bool) {
if (username == "") || (userpasswd == "") { if (username == "") || (userpasswd == "") {
fmt.Println("current username or password is empty") fmt.Println("current username or password is empty")
} }
fmt.Println("current panel settings as follows:") fmt.Println("current pannel settings as follows:")
fmt.Println("username:", username) fmt.Println("username:", username)
fmt.Println("userpasswd:", userpasswd) fmt.Println("userpasswd:", userpasswd)
fmt.Println("port:", port) fmt.Println("port:", port)
@@ -160,9 +133,10 @@ func updateTgbotEnableSts(status bool) {
logger.Infof("SetTgbotenabled[%v] success", status) logger.Infof("SetTgbotenabled[%v] success", status)
} }
} }
return
} }
func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) { func updateTgbotSetting(tgBotToken string, tgBotChatid int, tgBotRuntime string) {
err := database.InitDB(config.GetDBPath()) err := database.InitDB(config.GetDBPath())
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
@@ -191,7 +165,7 @@ func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime stri
} }
} }
if tgBotChatid != "" { if tgBotChatid != 0 {
err := settingService.SetTgBotChatId(tgBotChatid) err := settingService.SetTgBotChatId(tgBotChatid)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
@@ -230,36 +204,6 @@ func updateSetting(port int, username string, password string) {
} }
} }
func migrateDb() {
inboundService := service.InboundService{}
err := database.InitDB(config.GetDBPath())
if err != nil {
log.Fatal(err)
}
fmt.Println("Start migrating database...")
inboundService.MigrateDB()
fmt.Println("Migration done!")
}
func removeSecret() {
err := database.InitDB(config.GetDBPath())
if err != nil {
fmt.Println(err)
return
}
userService := service.UserService{}
err = userService.RemoveUserSecret()
if err != nil {
fmt.Println(err)
}
settingService := service.SettingService{}
err = settingService.SetSecretStatus(false)
if err != nil {
fmt.Println(err)
}
}
func main() { func main() {
if len(os.Args) < 2 { if len(os.Args) < 2 {
runWebServer() runWebServer()
@@ -271,25 +215,28 @@ func main() {
runCmd := flag.NewFlagSet("run", flag.ExitOnError) runCmd := flag.NewFlagSet("run", flag.ExitOnError)
v2uiCmd := flag.NewFlagSet("v2-ui", flag.ExitOnError)
var dbPath string
v2uiCmd.StringVar(&dbPath, "db", "/etc/v2-ui/v2-ui.db", "set v2-ui db file path")
settingCmd := flag.NewFlagSet("setting", flag.ExitOnError) settingCmd := flag.NewFlagSet("setting", flag.ExitOnError)
var port int var port int
var username string var username string
var password string var password string
var tgbottoken string var tgbottoken string
var tgbotchatid string var tgbotchatid int
var enabletgbot bool var enabletgbot bool
var tgbotRuntime string var tgbotRuntime string
var reset bool var reset bool
var show bool var show bool
var remove_secret bool
settingCmd.BoolVar(&reset, "reset", false, "reset all settings") settingCmd.BoolVar(&reset, "reset", false, "reset all settings")
settingCmd.BoolVar(&show, "show", false, "show current settings") settingCmd.BoolVar(&show, "show", false, "show current settings")
settingCmd.IntVar(&port, "port", 0, "set panel port") settingCmd.IntVar(&port, "port", 0, "set panel port")
settingCmd.StringVar(&username, "username", "", "set login username") settingCmd.StringVar(&username, "username", "", "set login username")
settingCmd.StringVar(&password, "password", "", "set login password") settingCmd.StringVar(&password, "password", "", "set login password")
settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "set telegram bot token") settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "set telegrame bot token")
settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "set telegram bot cron time") settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "set telegrame bot cron time")
settingCmd.StringVar(&tgbotchatid, "tgbotchatid", "", "set telegram bot chat id") settingCmd.IntVar(&tgbotchatid, "tgbotchatid", 0, "set telegrame bot chat id")
settingCmd.BoolVar(&enabletgbot, "enabletgbot", false, "enable telegram bot notify") settingCmd.BoolVar(&enabletgbot, "enabletgbot", false, "enable telegram bot notify")
oldUsage := flag.Usage oldUsage := flag.Usage
@@ -298,7 +245,7 @@ func main() {
fmt.Println() fmt.Println()
fmt.Println("Commands:") fmt.Println("Commands:")
fmt.Println(" run run web panel") fmt.Println(" run run web panel")
fmt.Println(" migrate migrate form other/old x-ui") fmt.Println(" v2-ui migrate form v2-ui")
fmt.Println(" setting set settings") fmt.Println(" setting set settings")
} }
@@ -316,8 +263,16 @@ func main() {
return return
} }
runWebServer() runWebServer()
case "migrate": case "v2-ui":
migrateDb() err := v2uiCmd.Parse(os.Args[2:])
if err != nil {
fmt.Println(err)
return
}
err = v2ui.MigrateFromV2UI(dbPath)
if err != nil {
fmt.Println("migrate from v2-ui failed:", err)
}
case "setting": case "setting":
err := settingCmd.Parse(os.Args[2:]) err := settingCmd.Parse(os.Args[2:])
if err != nil { if err != nil {
@@ -332,20 +287,16 @@ func main() {
if show { if show {
showSetting(show) showSetting(show)
} }
if (tgbottoken != "") || (tgbotchatid != "") || (tgbotRuntime != "") { if (tgbottoken != "") || (tgbotchatid != 0) || (tgbotRuntime != "") {
updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime) updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime)
} }
if remove_secret {
removeSecret()
}
if enabletgbot {
updateTgbotEnableSts(enabletgbot)
}
default: default:
fmt.Println("except 'run' or 'setting' subcommands") fmt.Println("except 'run' or 'v2-ui' or 'setting' subcommands")
fmt.Println() fmt.Println()
runCmd.Usage() runCmd.Usage()
fmt.Println() fmt.Println()
v2uiCmd.Usage()
fmt.Println()
settingCmd.Usage() settingCmd.Usage()
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

View File

@@ -1,98 +0,0 @@
{
"log": {
"loglevel": "warning",
"access": "./access.log",
"error": "./error.log"
},
"api": {
"tag": "api",
"services": [
"HandlerService",
"LoggerService",
"StatsService"
]
},
"inbounds": [
{
"tag": "api",
"listen": "127.0.0.1",
"port": 62789,
"protocol": "dokodemo-door",
"settings": {
"address": "127.0.0.1"
}
}
],
"outbounds": [
{
"protocol": "freedom",
"settings": {}
},
{
"tag": "blocked",
"protocol": "blackhole",
"settings": {}
},
{
"tag": "IPv4",
"protocol": "freedom",
"settings": {
"domainStrategy": "UseIPv4"
}
}
],
"policy": {
"levels": {
"0": {
"statsUserDownlink": true,
"statsUserUplink": true
}
},
"system": {
"statsInboundDownlink": true,
"statsInboundUplink": true
}
},
"routing": {
"domainStrategy": "IPIfNonMatch",
"rules": [
{
"type": "field",
"inboundTag": [
"api"
],
"outboundTag": "api"
},
{
"type": "field",
"outboundTag": "blocked",
"ip": [
"geoip:private"
]
},
{
"type": "field",
"outboundTag": "blocked",
"protocol": [
"bittorrent"
]
},
{
"type": "field",
"outboundTag": "blocked",
"domain": [
"geosite:category-ads-all",
"ext:iran.dat:ads"
]
},
{
"type": "field",
"outboundTag": "IPv4",
"domain": [
"geosite:google"
]
}
]
},
"stats": {}
}

View File

@@ -1,106 +0,0 @@
{
"log": {
"loglevel": "warning",
"access": "./access.log",
"error": "./error.log"
},
"api": {
"tag": "api",
"services": [
"HandlerService",
"LoggerService",
"StatsService"
]
},
"inbounds": [
{
"tag": "api",
"listen": "127.0.0.1",
"port": 62789,
"protocol": "dokodemo-door",
"settings": {
"address": "127.0.0.1"
}
}
],
"outbounds": [
{
"protocol": "freedom",
"settings": {}
},
{
"tag": "blocked",
"protocol": "blackhole",
"settings": {}
},
{
"tag": "WARP",
"protocol": "socks",
"settings": {
"servers": [
{
"address": "127.0.0.1",
"port": 40000
}
]
}
}
],
"policy": {
"levels": {
"0": {
"statsUserDownlink": true,
"statsUserUplink": true
}
},
"system": {
"statsInboundDownlink": true,
"statsInboundUplink": true
}
},
"routing": {
"domainStrategy": "IPIfNonMatch",
"rules": [
{
"type": "field",
"inboundTag": [
"api"
],
"outboundTag": "api"
},
{
"type": "field",
"outboundTag": "blocked",
"ip": [
"geoip:private"
]
},
{
"type": "field",
"outboundTag": "blocked",
"protocol": [
"bittorrent"
]
},
{
"type": "field",
"outboundTag": "blocked",
"domain": [
"geosite:category-ads-all",
"ext:iran.dat:ads"
]
},
{
"type": "field",
"outboundTag": "WARP",
"domain": [
"geosite:spotify",
"geosite:netflix",
"geosite:openai",
"geosite:google"
]
}
]
},
"stats": {}
}

View File

@@ -1,66 +0,0 @@
{
"log": {
"loglevel": "warning",
"access": "./access.log",
"error": "./error.log"
},
"api": {
"tag": "api",
"services": ["HandlerService", "LoggerService", "StatsService"]
},
"inbounds": [
{
"tag": "api",
"listen": "127.0.0.1",
"port": 62789,
"protocol": "dokodemo-door",
"settings": {
"address": "127.0.0.1"
}
}
],
"outbounds": [
{
"protocol": "freedom",
"settings": {}
},
{
"tag": "blocked",
"protocol": "blackhole",
"settings": {}
}
],
"policy": {
"levels": {
"0": {
"statsUserDownlink": true,
"statsUserUplink": true
}
},
"system": {
"statsInboundDownlink": true,
"statsInboundUplink": true
}
},
"routing": {
"domainStrategy": "IPIfNonMatch",
"rules": [
{
"type": "field",
"inboundTag": ["api"],
"outboundTag": "api"
},
{
"type": "field",
"outboundTag": "blocked",
"ip": ["geoip:private"]
},
{
"type": "field",
"outboundTag": "blocked",
"protocol": ["bittorrent"]
}
]
},
"stats": {}
}

View File

@@ -1,26 +1,26 @@
{ {
"log": { "log": {
"loglevel": "warning", "loglevel": "warning",
"access": "./access.log", "access": "./access.log"
"error": "./error.log"
}, },
"api": { "api": {
"tag": "api",
"services": [ "services": [
"HandlerService", "HandlerService",
"LoggerService", "LoggerService",
"StatsService" "StatsService"
] ],
"tag": "api"
}, },
"inbounds": [ "inbounds": [
{ {
"tag": "api",
"listen": "127.0.0.1", "listen": "127.0.0.1",
"port": 62789, "port": 62789,
"protocol": "dokodemo-door", "protocol": "dokodemo-door",
"settings": { "settings": {
"address": "127.0.0.1" "address": "127.0.0.1"
} },
"tag": "api"
} }
], ],
"outbounds": [ "outbounds": [
@@ -29,16 +29,16 @@
"settings": {} "settings": {}
}, },
{ {
"tag": "blocked",
"protocol": "blackhole", "protocol": "blackhole",
"settings": {} "settings": {},
"tag": "blocked"
} }
], ],
"policy": { "policy": {
"levels": { "levels": {
"0": { "0": {
"statsUserDownlink": true, "statsUserUplink": true,
"statsUserUplink": true "statsUserDownlink": true
} }
}, },
"system": { "system": {
@@ -50,26 +50,26 @@
"domainStrategy": "IPIfNonMatch", "domainStrategy": "IPIfNonMatch",
"rules": [ "rules": [
{ {
"type": "field",
"inboundTag": [ "inboundTag": [
"api" "api"
], ],
"outboundTag": "api" "outboundTag": "api",
"type": "field"
}, },
{ {
"type": "field",
"outboundTag": "blocked",
"ip": [ "ip": [
"geoip:private", "geoip:private",
"geoip:ir" "geoip:ir"
] ],
"outboundTag": "blocked",
"type": "field"
}, },
{ {
"type": "field",
"outboundTag": "blocked", "outboundTag": "blocked",
"protocol": [ "protocol": [
"bittorrent" "bittorrent"
] ],
"type": "field"
} }
] ]
}, },

View File

@@ -1,26 +1,26 @@
{ {
"log": { "log": {
"loglevel": "warning", "loglevel": "warning",
"access": "./access.log", "access": "./access.log"
"error": "./error.log"
}, },
"api": { "api": {
"tag": "api",
"services": [ "services": [
"HandlerService", "HandlerService",
"LoggerService", "LoggerService",
"StatsService" "StatsService"
] ],
"tag": "api"
}, },
"inbounds": [ "inbounds": [
{ {
"tag": "api",
"listen": "127.0.0.1", "listen": "127.0.0.1",
"port": 62789, "port": 62789,
"protocol": "dokodemo-door", "protocol": "dokodemo-door",
"settings": { "settings": {
"address": "127.0.0.1" "address": "127.0.0.1"
} },
"tag": "api"
} }
], ],
"outbounds": [ "outbounds": [
@@ -29,16 +29,16 @@
"settings": {} "settings": {}
}, },
{ {
"tag": "blocked",
"protocol": "blackhole", "protocol": "blackhole",
"settings": {} "settings": {},
"tag": "blocked"
} }
], ],
"policy": { "policy": {
"levels": { "levels": {
"0": { "0": {
"statsUserDownlink": true, "statsUserUplink": true,
"statsUserUplink": true "statsUserDownlink": true
} }
}, },
"system": { "system": {
@@ -47,38 +47,27 @@
} }
}, },
"routing": { "routing": {
"domainStrategy": "IPIfNonMatch",
"rules": [ "rules": [
{ {
"type": "field",
"inboundTag": [ "inboundTag": [
"api" "api"
], ],
"outboundTag": "api" "outboundTag": "api",
"type": "field"
}, },
{ {
"type": "field",
"outboundTag": "blocked",
"ip": [ "ip": [
"geoip:private" "geoip:private"
] ],
"outboundTag": "blocked",
"type": "field"
}, },
{ {
"type": "field",
"outboundTag": "blocked", "outboundTag": "blocked",
"protocol": [ "protocol": [
"bittorrent" "bittorrent"
] ],
}, "type": "field"
{
"type": "field",
"outboundTag": "blocked",
"domain": [
"regexp:.*\\.ir$",
"ext:iran.dat:ir",
"ext:iran.dat:other",
"geosite:category-ir"
]
} }
] ]
}, },

View File

@@ -1,162 +0,0 @@
package sub
import (
"context"
"crypto/tls"
"io"
"net"
"net/http"
"strconv"
"x-ui/config"
"x-ui/logger"
"x-ui/util/common"
"x-ui/web/middleware"
"x-ui/web/network"
"x-ui/web/service"
"github.com/gin-gonic/gin"
)
type Server struct {
httpServer *http.Server
listener net.Listener
sub *SUBController
settingService service.SettingService
ctx context.Context
cancel context.CancelFunc
}
func NewServer() *Server {
ctx, cancel := context.WithCancel(context.Background())
return &Server{
ctx: ctx,
cancel: cancel,
}
}
func (s *Server) initRouter() (*gin.Engine, error) {
if config.IsDebug() {
gin.SetMode(gin.DebugMode)
} else {
gin.DefaultWriter = io.Discard
gin.DefaultErrorWriter = io.Discard
gin.SetMode(gin.ReleaseMode)
}
engine := gin.Default()
subPath, err := s.settingService.GetSubPath()
if err != nil {
return nil, err
}
subDomain, err := s.settingService.GetSubDomain()
if err != nil {
return nil, err
}
if subDomain != "" {
engine.Use(middleware.DomainValidatorMiddleware(subDomain))
}
g := engine.Group(subPath)
s.sub = NewSUBController(g)
return engine, nil
}
func (s *Server) Start() (err error) {
//This is an anonymous function, no function name
defer func() {
if err != nil {
s.Stop()
}
}()
subEnable, err := s.settingService.GetSubEnable()
if err != nil {
return err
}
if !subEnable {
return nil
}
engine, err := s.initRouter()
if err != nil {
return err
}
certFile, err := s.settingService.GetSubCertFile()
if err != nil {
return err
}
keyFile, err := s.settingService.GetSubKeyFile()
if err != nil {
return err
}
listen, err := s.settingService.GetSubListen()
if err != nil {
return err
}
port, err := s.settingService.GetSubPort()
if err != nil {
return err
}
listenAddr := net.JoinHostPort(listen, strconv.Itoa(port))
listener, err := net.Listen("tcp", listenAddr)
if err != nil {
return err
}
if certFile != "" || keyFile != "" {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
listener.Close()
return err
}
c := &tls.Config{
Certificates: []tls.Certificate{cert},
}
listener = network.NewAutoHttpsListener(listener)
listener = tls.NewListener(listener, c)
}
if certFile != "" || keyFile != "" {
logger.Info("Sub server run https on", listener.Addr())
} else {
logger.Info("Sub server run http on", listener.Addr())
}
s.listener = listener
s.httpServer = &http.Server{
Handler: engine,
}
go func() {
s.httpServer.Serve(listener)
}()
return nil
}
func (s *Server) Stop() error {
s.cancel()
var err1 error
var err2 error
if s.httpServer != nil {
err1 = s.httpServer.Shutdown(s.ctx)
}
if s.listener != nil {
err2 = s.listener.Close()
}
return common.Combine(err1, err2)
}
func (s *Server) GetCtx() context.Context {
return s.ctx
}

View File

@@ -1,45 +0,0 @@
package sub
import (
"encoding/base64"
"strings"
"github.com/gin-gonic/gin"
)
type SUBController struct {
subService SubService
}
func NewSUBController(g *gin.RouterGroup) *SUBController {
a := &SUBController{}
a.initRouter(g)
return a
}
func (a *SUBController) initRouter(g *gin.RouterGroup) {
g = g.Group("/")
g.GET("/:subid", a.subs)
}
func (a *SUBController) subs(c *gin.Context) {
subId := c.Param("subid")
host := strings.Split(c.Request.Host, ":")[0]
subs, headers, err := a.subService.GetSubs(subId, host)
if err != nil || len(subs) == 0 {
c.String(400, "Error!")
} else {
result := ""
for _, sub := range subs {
result += sub + "\n"
}
// Add headers
c.Writer.Header().Set("Subscription-Userinfo", headers[0])
c.Writer.Header().Set("Profile-Update-Interval", headers[1])
c.Writer.Header().Set("Profile-Title", headers[2])
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
}
}

View File

@@ -1,744 +0,0 @@
package sub
import (
"encoding/base64"
"fmt"
"net/url"
"strings"
"x-ui/database"
"x-ui/database/model"
"x-ui/logger"
"x-ui/web/service"
"x-ui/xray"
"github.com/goccy/go-json"
)
type SubService struct {
address string
inboundService service.InboundService
settingServics service.SettingService
}
func (s *SubService) GetSubs(subId string, host string) ([]string, []string, error) {
s.address = host
var result []string
var headers []string
var traffic xray.ClientTraffic
var clientTraffics []xray.ClientTraffic
inbounds, err := s.getInboundsBySubId(subId)
if err != nil {
return nil, nil, err
}
for _, inbound := range inbounds {
clients, err := s.inboundService.GetClients(inbound)
if err != nil {
logger.Error("SubService - GetSub: Unable to get clients from inbound")
}
if clients == nil {
continue
}
if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' {
fallbackMaster, err := s.getFallbackMaster(inbound.Listen)
if err == nil {
inbound.Listen = fallbackMaster.Listen
inbound.Port = fallbackMaster.Port
var stream map[string]interface{}
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
var masterStream map[string]interface{}
json.Unmarshal([]byte(fallbackMaster.StreamSettings), &masterStream)
stream["security"] = masterStream["security"]
stream["tlsSettings"] = masterStream["tlsSettings"]
modifiedStream, _ := json.MarshalIndent(stream, "", " ")
inbound.StreamSettings = string(modifiedStream)
}
}
for _, client := range clients {
if client.Enable && client.SubID == subId {
link := s.getLink(inbound, client.Email)
result = append(result, link)
clientTraffics = append(clientTraffics, s.getClientTraffics(inbound.ClientStats, client.Email))
}
}
}
for index, clientTraffic := range clientTraffics {
if index == 0 {
traffic.Up = clientTraffic.Up
traffic.Down = clientTraffic.Down
traffic.Total = clientTraffic.Total
if clientTraffic.ExpiryTime > 0 {
traffic.ExpiryTime = clientTraffic.ExpiryTime
}
} else {
traffic.Up += clientTraffic.Up
traffic.Down += clientTraffic.Down
if traffic.Total == 0 || clientTraffic.Total == 0 {
traffic.Total = 0
} else {
traffic.Total += clientTraffic.Total
}
if clientTraffic.ExpiryTime != traffic.ExpiryTime {
traffic.ExpiryTime = 0
}
}
}
headers = append(headers, fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000))
updateInterval, _ := s.settingServics.GetSubUpdates()
headers = append(headers, fmt.Sprintf("%d", updateInterval))
headers = append(headers, subId)
return result, headers, nil
}
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
db := database.GetDB()
var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Preload("ClientStats").Where("settings like ? and enable = ?", fmt.Sprintf(`%%"subId": "%s"%%`, subId), true).Find(&inbounds).Error
if err != nil {
return nil, err
}
return inbounds, nil
}
func (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email string) xray.ClientTraffic {
for _, traffic := range traffics {
if traffic.Email == email {
return traffic
}
}
return xray.ClientTraffic{}
}
func (s *SubService) getFallbackMaster(dest string) (*model.Inbound, error) {
db := database.GetDB()
var inbound *model.Inbound
err := db.Model(model.Inbound{}).
Where("JSON_TYPE(settings, '$.fallbacks') = 'array'").
Where("EXISTS (SELECT * FROM json_each(settings, '$.fallbacks') WHERE json_extract(value, '$.dest') = ?)", dest).
Find(&inbound).Error
if err != nil {
return nil, err
}
return inbound, nil
}
func (s *SubService) getLink(inbound *model.Inbound, email string) string {
switch inbound.Protocol {
case "vmess":
return s.genVmessLink(inbound, email)
case "vless":
return s.genVlessLink(inbound, email)
case "trojan":
return s.genTrojanLink(inbound, email)
case "shadowsocks":
return s.genShadowsocksLink(inbound, email)
}
return ""
}
func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
if inbound.Protocol != model.VMess {
return ""
}
remark := fmt.Sprintf("%s-%s", inbound.Remark, email)
obj := map[string]interface{}{
"v": "2",
"ps": remark,
"add": s.address,
"port": inbound.Port,
"type": "none",
}
var stream map[string]interface{}
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
network, _ := stream["network"].(string)
obj["net"] = network
switch network {
case "tcp":
tcp, _ := stream["tcpSettings"].(map[string]interface{})
header, _ := tcp["header"].(map[string]interface{})
typeStr, _ := header["type"].(string)
obj["type"] = typeStr
if typeStr == "http" {
request := header["request"].(map[string]interface{})
requestPath, _ := request["path"].([]interface{})
obj["path"] = requestPath[0].(string)
headers, _ := request["headers"].(map[string]interface{})
obj["host"] = searchHost(headers)
}
case "kcp":
kcp, _ := stream["kcpSettings"].(map[string]interface{})
header, _ := kcp["header"].(map[string]interface{})
obj["type"], _ = header["type"].(string)
obj["path"], _ = kcp["seed"].(string)
case "ws":
ws, _ := stream["wsSettings"].(map[string]interface{})
obj["path"] = ws["path"].(string)
headers, _ := ws["headers"].(map[string]interface{})
obj["host"] = searchHost(headers)
case "http":
obj["net"] = "h2"
http, _ := stream["httpSettings"].(map[string]interface{})
obj["path"], _ = http["path"].(string)
obj["host"] = searchHost(http)
case "quic":
quic, _ := stream["quicSettings"].(map[string]interface{})
header := quic["header"].(map[string]interface{})
obj["type"], _ = header["type"].(string)
obj["host"], _ = quic["security"].(string)
obj["path"], _ = quic["key"].(string)
case "grpc":
grpc, _ := stream["grpcSettings"].(map[string]interface{})
obj["path"] = grpc["serviceName"].(string)
if grpc["multiMode"].(bool) {
obj["type"] = "multi"
}
}
security, _ := stream["security"].(string)
var domains []interface{}
obj["tls"] = security
if security == "tls" {
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
alpns, _ := tlsSetting["alpn"].([]interface{})
if len(alpns) > 0 {
var alpn []string
for _, a := range alpns {
alpn = append(alpn, a.(string))
}
obj["alpn"] = strings.Join(alpn, ",")
}
tlsSettings, _ := searchKey(tlsSetting, "settings")
if tlsSetting != nil {
if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
obj["sni"], _ = sniValue.(string)
}
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
obj["fp"], _ = fpValue.(string)
}
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
obj["allowInsecure"], _ = insecure.(bool)
}
if domainSettings, ok := searchKey(tlsSettings, "domains"); ok {
domains, _ = domainSettings.([]interface{})
}
}
serverName, _ := tlsSetting["serverName"].(string)
if serverName != "" {
obj["add"] = serverName
}
}
clients, _ := s.inboundService.GetClients(inbound)
clientIndex := -1
for i, client := range clients {
if client.Email == email {
clientIndex = i
break
}
}
obj["id"] = clients[clientIndex].ID
if len(domains) > 0 {
links := ""
for index, d := range domains {
domain := d.(map[string]interface{})
obj["ps"] = remark + "-" + domain["remark"].(string)
obj["add"] = domain["domain"].(string)
if index > 0 {
links += "\n"
}
jsonStr, _ := json.MarshalIndent(obj, "", " ")
links += "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
}
return links
}
jsonStr, _ := json.MarshalIndent(obj, "", " ")
return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
}
func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
address := s.address
if inbound.Protocol != model.VLESS {
return ""
}
var stream map[string]interface{}
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
clients, _ := s.inboundService.GetClients(inbound)
clientIndex := -1
for i, client := range clients {
if client.Email == email {
clientIndex = i
break
}
}
uuid := clients[clientIndex].ID
port := inbound.Port
streamNetwork := stream["network"].(string)
params := make(map[string]string)
params["type"] = streamNetwork
switch streamNetwork {
case "tcp":
tcp, _ := stream["tcpSettings"].(map[string]interface{})
header, _ := tcp["header"].(map[string]interface{})
typeStr, _ := header["type"].(string)
if typeStr == "http" {
request := header["request"].(map[string]interface{})
requestPath, _ := request["path"].([]interface{})
params["path"] = requestPath[0].(string)
headers, _ := request["headers"].(map[string]interface{})
params["host"] = searchHost(headers)
params["headerType"] = "http"
}
case "kcp":
kcp, _ := stream["kcpSettings"].(map[string]interface{})
header, _ := kcp["header"].(map[string]interface{})
params["headerType"] = header["type"].(string)
params["seed"] = kcp["seed"].(string)
case "ws":
ws, _ := stream["wsSettings"].(map[string]interface{})
params["path"] = ws["path"].(string)
headers, _ := ws["headers"].(map[string]interface{})
params["host"] = searchHost(headers)
case "http":
http, _ := stream["httpSettings"].(map[string]interface{})
params["path"] = http["path"].(string)
params["host"] = searchHost(http)
case "quic":
quic, _ := stream["quicSettings"].(map[string]interface{})
params["quicSecurity"] = quic["security"].(string)
params["key"] = quic["key"].(string)
header := quic["header"].(map[string]interface{})
params["headerType"] = header["type"].(string)
case "grpc":
grpc, _ := stream["grpcSettings"].(map[string]interface{})
params["serviceName"] = grpc["serviceName"].(string)
if grpc["multiMode"].(bool) {
params["mode"] = "multi"
}
}
security, _ := stream["security"].(string)
var domains []interface{}
if security == "tls" {
params["security"] = "tls"
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
alpns, _ := tlsSetting["alpn"].([]interface{})
var alpn []string
for _, a := range alpns {
alpn = append(alpn, a.(string))
}
if len(alpn) > 0 {
params["alpn"] = strings.Join(alpn, ",")
}
tlsSettings, _ := searchKey(tlsSetting, "settings")
if tlsSetting != nil {
if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
params["sni"], _ = sniValue.(string)
}
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
params["fp"], _ = fpValue.(string)
}
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
if insecure.(bool) {
params["allowInsecure"] = "1"
}
}
if domainSettings, ok := searchKey(tlsSettings, "domains"); ok {
domains, _ = domainSettings.([]interface{})
}
}
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
params["flow"] = clients[clientIndex].Flow
}
serverName, _ := tlsSetting["serverName"].(string)
if serverName != "" {
address = serverName
}
}
if security == "reality" {
params["security"] = "reality"
realitySetting, _ := stream["realitySettings"].(map[string]interface{})
realitySettings, _ := searchKey(realitySetting, "settings")
if realitySetting != nil {
if sniValue, ok := searchKey(realitySetting, "serverNames"); ok {
sNames, _ := sniValue.([]interface{})
params["sni"], _ = sNames[0].(string)
}
if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok {
params["pbk"], _ = pbkValue.(string)
}
if sidValue, ok := searchKey(realitySetting, "shortIds"); ok {
shortIds, _ := sidValue.([]interface{})
params["sid"], _ = shortIds[0].(string)
}
if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok {
if fp, ok := fpValue.(string); ok && len(fp) > 0 {
params["fp"] = fp
}
}
if spxValue, ok := searchKey(realitySettings, "spiderX"); ok {
if spx, ok := spxValue.(string); ok && len(spx) > 0 {
params["spx"] = spx
}
}
if serverName, ok := searchKey(realitySettings, "serverName"); ok {
if sname, ok := serverName.(string); ok && len(sname) > 0 {
address = sname
}
}
}
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
params["flow"] = clients[clientIndex].Flow
}
}
if security == "xtls" {
params["security"] = "xtls"
xtlsSetting, _ := stream["xtlsSettings"].(map[string]interface{})
alpns, _ := xtlsSetting["alpn"].([]interface{})
var alpn []string
for _, a := range alpns {
alpn = append(alpn, a.(string))
}
if len(alpn) > 0 {
params["alpn"] = strings.Join(alpn, ",")
}
xtlsSettings, _ := searchKey(xtlsSetting, "settings")
if xtlsSetting != nil {
if fpValue, ok := searchKey(xtlsSettings, "fingerprint"); ok {
params["fp"], _ = fpValue.(string)
}
if insecure, ok := searchKey(xtlsSettings, "allowInsecure"); ok {
if insecure.(bool) {
params["allowInsecure"] = "1"
}
}
if sniValue, ok := searchKey(xtlsSettings, "serverName"); ok {
params["sni"], _ = sniValue.(string)
}
}
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
params["flow"] = clients[clientIndex].Flow
}
serverName, _ := xtlsSetting["serverName"].(string)
if serverName != "" {
address = serverName
}
}
if security != "tls" && security != "reality" && security != "xtls" {
params["security"] = "none"
}
link := fmt.Sprintf("vless://%s@%s:%d", uuid, address, port)
url, _ := url.Parse(link)
q := url.Query()
for k, v := range params {
q.Add(k, v)
}
// Set the new query values on the URL
url.RawQuery = q.Encode()
remark := fmt.Sprintf("%s-%s", inbound.Remark, email)
if len(domains) > 0 {
links := ""
for index, d := range domains {
domain := d.(map[string]interface{})
url.Fragment = remark + "-" + domain["remark"].(string)
url.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port)
if index > 0 {
links += "\n"
}
links += url.String()
}
return links
}
url.Fragment = remark
return url.String()
}
func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string {
address := s.address
if inbound.Protocol != model.Trojan {
return ""
}
var stream map[string]interface{}
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
clients, _ := s.inboundService.GetClients(inbound)
clientIndex := -1
for i, client := range clients {
if client.Email == email {
clientIndex = i
break
}
}
password := clients[clientIndex].Password
port := inbound.Port
streamNetwork := stream["network"].(string)
params := make(map[string]string)
params["type"] = streamNetwork
switch streamNetwork {
case "tcp":
tcp, _ := stream["tcpSettings"].(map[string]interface{})
header, _ := tcp["header"].(map[string]interface{})
typeStr, _ := header["type"].(string)
if typeStr == "http" {
request := header["request"].(map[string]interface{})
requestPath, _ := request["path"].([]interface{})
params["path"] = requestPath[0].(string)
headers, _ := request["headers"].(map[string]interface{})
params["host"] = searchHost(headers)
params["headerType"] = "http"
}
case "kcp":
kcp, _ := stream["kcpSettings"].(map[string]interface{})
header, _ := kcp["header"].(map[string]interface{})
params["headerType"] = header["type"].(string)
params["seed"] = kcp["seed"].(string)
case "ws":
ws, _ := stream["wsSettings"].(map[string]interface{})
params["path"] = ws["path"].(string)
headers, _ := ws["headers"].(map[string]interface{})
params["host"] = searchHost(headers)
case "http":
http, _ := stream["httpSettings"].(map[string]interface{})
params["path"] = http["path"].(string)
params["host"] = searchHost(http)
case "quic":
quic, _ := stream["quicSettings"].(map[string]interface{})
params["quicSecurity"] = quic["security"].(string)
params["key"] = quic["key"].(string)
header := quic["header"].(map[string]interface{})
params["headerType"] = header["type"].(string)
case "grpc":
grpc, _ := stream["grpcSettings"].(map[string]interface{})
params["serviceName"] = grpc["serviceName"].(string)
if grpc["multiMode"].(bool) {
params["mode"] = "multi"
}
}
security, _ := stream["security"].(string)
var domains []interface{}
if security == "tls" {
params["security"] = "tls"
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
alpns, _ := tlsSetting["alpn"].([]interface{})
var alpn []string
for _, a := range alpns {
alpn = append(alpn, a.(string))
}
if len(alpn) > 0 {
params["alpn"] = strings.Join(alpn, ",")
}
tlsSettings, _ := searchKey(tlsSetting, "settings")
if tlsSetting != nil {
if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
params["sni"], _ = sniValue.(string)
}
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
params["fp"], _ = fpValue.(string)
}
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
if insecure.(bool) {
params["allowInsecure"] = "1"
}
}
if domainSettings, ok := searchKey(tlsSettings, "domains"); ok {
domains, _ = domainSettings.([]interface{})
}
}
serverName, _ := tlsSetting["serverName"].(string)
if serverName != "" {
address = serverName
}
}
if security == "reality" {
params["security"] = "reality"
realitySetting, _ := stream["realitySettings"].(map[string]interface{})
realitySettings, _ := searchKey(realitySetting, "settings")
if realitySetting != nil {
if sniValue, ok := searchKey(realitySetting, "serverNames"); ok {
sNames, _ := sniValue.([]interface{})
params["sni"], _ = sNames[0].(string)
}
if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok {
params["pbk"], _ = pbkValue.(string)
}
if sidValue, ok := searchKey(realitySetting, "shortIds"); ok {
shortIds, _ := sidValue.([]interface{})
params["sid"], _ = shortIds[0].(string)
}
if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok {
if fp, ok := fpValue.(string); ok && len(fp) > 0 {
params["fp"] = fp
}
}
if spxValue, ok := searchKey(realitySettings, "spiderX"); ok {
if spx, ok := spxValue.(string); ok && len(spx) > 0 {
params["spx"] = spx
}
}
if serverName, ok := searchKey(realitySettings, "serverName"); ok {
if sname, ok := serverName.(string); ok && len(sname) > 0 {
address = sname
}
}
}
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
params["flow"] = clients[clientIndex].Flow
}
}
if security == "xtls" {
params["security"] = "xtls"
xtlsSetting, _ := stream["xtlsSettings"].(map[string]interface{})
alpns, _ := xtlsSetting["alpn"].([]interface{})
var alpn []string
for _, a := range alpns {
alpn = append(alpn, a.(string))
}
if len(alpn) > 0 {
params["alpn"] = strings.Join(alpn, ",")
}
xtlsSettings, _ := searchKey(xtlsSetting, "settings")
if xtlsSetting != nil {
if fpValue, ok := searchKey(xtlsSettings, "fingerprint"); ok {
params["fp"], _ = fpValue.(string)
}
if insecure, ok := searchKey(xtlsSettings, "allowInsecure"); ok {
if insecure.(bool) {
params["allowInsecure"] = "1"
}
}
if sniValue, ok := searchKey(xtlsSettings, "serverName"); ok {
params["sni"], _ = sniValue.(string)
}
}
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
params["flow"] = clients[clientIndex].Flow
}
serverName, _ := xtlsSetting["serverName"].(string)
if serverName != "" {
address = serverName
}
}
if security != "tls" && security != "reality" && security != "xtls" {
params["security"] = "none"
}
link := fmt.Sprintf("trojan://%s@%s:%d", password, address, port)
url, _ := url.Parse(link)
q := url.Query()
for k, v := range params {
q.Add(k, v)
}
// Set the new query values on the URL
url.RawQuery = q.Encode()
remark := fmt.Sprintf("%s-%s", inbound.Remark, email)
if len(domains) > 0 {
links := ""
for index, d := range domains {
domain := d.(map[string]interface{})
url.Fragment = remark + "-" + domain["remark"].(string)
url.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port)
if index > 0 {
links += "\n"
}
links += url.String()
}
return links
}
url.Fragment = remark
return url.String()
}
func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) string {
address := s.address
if inbound.Protocol != model.Shadowsocks {
return ""
}
clients, _ := s.inboundService.GetClients(inbound)
var settings map[string]interface{}
json.Unmarshal([]byte(inbound.Settings), &settings)
inboundPassword := settings["password"].(string)
method := settings["method"].(string)
clientIndex := -1
for i, client := range clients {
if client.Email == email {
clientIndex = i
break
}
}
encPart := fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password)
remark := fmt.Sprintf("%s-%s", inbound.Remark, clients[clientIndex].Email)
return fmt.Sprintf("ss://%s@%s:%d#%s", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port, remark)
}
func searchKey(data interface{}, key string) (interface{}, bool) {
switch val := data.(type) {
case map[string]interface{}:
for k, v := range val {
if k == key {
return v, true
}
if result, ok := searchKey(v, key); ok {
return result, true
}
}
case []interface{}:
for _, v := range val {
if result, ok := searchKey(v, key); ok {
return result, true
}
}
}
return nil, false
}
func searchHost(headers interface{}) string {
data, _ := headers.(map[string]interface{})
for k, v := range data {
if strings.EqualFold(k, "host") {
switch v.(type) {
case []interface{}:
hosts, _ := v.([]interface{})
if len(hosts) > 0 {
return hosts[0].(string)
} else {
return ""
}
case interface{}:
return v.(string)
}
}
}
return ""
}

View File

@@ -6,6 +6,8 @@ import (
"x-ui/logger" "x-ui/logger"
) )
var CtxDone = errors.New("context done")
func NewErrorf(format string, a ...interface{}) error { func NewErrorf(format string, a ...interface{}) error {
msg := fmt.Sprintf(format, a...) msg := fmt.Sprintf(format, a...)
return errors.New(msg) return errors.New(msg)

View File

@@ -0,0 +1,9 @@
package common
import "sort"
func IsSubString(target string, str_array []string) bool {
sort.Strings(str_array)
index := sort.SearchStrings(str_array, target)
return index < len(str_array) && str_array[index] == target
}

12
util/context.go Normal file
View File

@@ -0,0 +1,12 @@
package util
import "context"
func IsDone(ctx context.Context) bool {
select {
case <-ctx.Done():
return true
default:
return false
}
}

View File

@@ -6,7 +6,7 @@ import (
type RawMessage []byte type RawMessage []byte
// MarshalJSON: Customize json.RawMessage default behavior // MarshalJSON 自定义 json.RawMessage 默认行为
func (m RawMessage) MarshalJSON() ([]byte, error) { func (m RawMessage) MarshalJSON() ([]byte, error) {
if len(m) == 0 { if len(m) == 0 {
return []byte("null"), nil return []byte("null"), nil
@@ -14,7 +14,7 @@ func (m RawMessage) MarshalJSON() ([]byte, error) {
return m, nil return m, nil
} }
// UnmarshalJSON: sets *m to a copy of data. // UnmarshalJSON sets *m to a copy of data.
func (m *RawMessage) UnmarshalJSON(data []byte) error { func (m *RawMessage) UnmarshalJSON(data []byte) error {
if m == nil { if m == nil {
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")

0
util/sys/a.s Normal file
View File

View File

@@ -1,4 +1,3 @@
//go:build darwin
// +build darwin // +build darwin
package sys package sys

View File

@@ -1,4 +1,3 @@
//go:build linux
// +build linux // +build linux
package sys package sys
@@ -24,8 +23,8 @@ func getLinesNum(filename string) (int, error) {
var buffPosition int var buffPosition int
for { for {
i := bytes.IndexByte(buf[buffPosition:n], '\n') i := bytes.IndexByte(buf[buffPosition:], '\n')
if i < 0 { if i < 0 || n == buffPosition {
break break
} }
buffPosition += i + 1 buffPosition += i + 1
@@ -33,12 +32,11 @@ func getLinesNum(filename string) (int, error) {
} }
if err == io.EOF { if err == io.EOF {
break return sum, nil
} else if err != nil { } else if err != nil {
return 0, err return sum, err
} }
} }
return sum, nil
} }
func GetTCPCount() (int, error) { func GetTCPCount() (int, error) {
@@ -46,11 +44,11 @@ func GetTCPCount() (int, error) {
tcp4, err := getLinesNum(fmt.Sprintf("%v/net/tcp", root)) tcp4, err := getLinesNum(fmt.Sprintf("%v/net/tcp", root))
if err != nil { if err != nil {
return 0, err return tcp4, err
} }
tcp6, err := getLinesNum(fmt.Sprintf("%v/net/tcp6", root)) tcp6, err := getLinesNum(fmt.Sprintf("%v/net/tcp6", root))
if err != nil { if err != nil {
return 0, err return tcp4 + tcp6, nil
} }
return tcp4 + tcp6, nil return tcp4 + tcp6, nil
@@ -61,11 +59,11 @@ func GetUDPCount() (int, error) {
udp4, err := getLinesNum(fmt.Sprintf("%v/net/udp", root)) udp4, err := getLinesNum(fmt.Sprintf("%v/net/udp", root))
if err != nil { if err != nil {
return 0, err return udp4, err
} }
udp6, err := getLinesNum(fmt.Sprintf("%v/net/udp6", root)) udp6, err := getLinesNum(fmt.Sprintf("%v/net/udp6", root))
if err != nil { if err != nil {
return 0, err return udp4 + udp6, nil
} }
return udp4 + udp6, nil return udp4 + udp6, nil

View File

@@ -4,27 +4,21 @@
package sys package sys
import ( import (
"errors"
"github.com/shirou/gopsutil/v3/net" "github.com/shirou/gopsutil/v3/net"
) )
func GetConnectionCount(proto string) (int, error) { func GetTCPCount() (int, error) {
if proto != "tcp" && proto != "udp" { stats, err := net.Connections("tcp")
return 0, errors.New("invalid protocol")
}
stats, err := net.Connections(proto)
if err != nil { if err != nil {
return 0, err return 0, err
} }
return len(stats), nil return len(stats), nil
} }
func GetTCPCount() (int, error) {
return GetConnectionCount("tcp")
}
func GetUDPCount() (int, error) { func GetUDPCount() (int, error) {
return GetConnectionCount("udp") stats, err := net.Connections("udp")
if err != nil {
return 0, err
}
return len(stats), nil
} }

28
v2ui/db.go Normal file
View File

@@ -0,0 +1,28 @@
package v2ui
import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var v2db *gorm.DB
func initDB(dbPath string) error {
c := &gorm.Config{
Logger: logger.Discard,
}
var err error
v2db, err = gorm.Open(sqlite.Open(dbPath), c)
if err != nil {
return err
}
return nil
}
func getV2Inbounds() ([]*V2Inbound, error) {
inbounds := make([]*V2Inbound, 0)
err := v2db.Model(V2Inbound{}).Find(&inbounds).Error
return inbounds, err
}

41
v2ui/models.go Normal file
View File

@@ -0,0 +1,41 @@
package v2ui
import "x-ui/database/model"
type V2Inbound struct {
Id int `gorm:"primaryKey;autoIncrement"`
Port int `gorm:"unique"`
Listen string
Protocol string
Settings string
StreamSettings string
Tag string `gorm:"unique"`
Sniffing string
Remark string
Up int64
Down int64
Enable bool
}
func (i *V2Inbound) TableName() string {
return "inbound"
}
func (i *V2Inbound) ToInbound(userId int) *model.Inbound {
return &model.Inbound{
UserId: userId,
Up: i.Up,
Down: i.Down,
Total: 0,
Remark: i.Remark,
Enable: i.Enable,
ExpiryTime: 0,
Listen: i.Listen,
Port: i.Port,
Protocol: model.Protocol(i.Protocol),
Settings: i.Settings,
StreamSettings: i.StreamSettings,
Tag: i.Tag,
Sniffing: i.Sniffing,
}
}

51
v2ui/v2ui.go Normal file
View File

@@ -0,0 +1,51 @@
package v2ui
import (
"fmt"
"x-ui/config"
"x-ui/database"
"x-ui/database/model"
"x-ui/util/common"
"x-ui/web/service"
)
func MigrateFromV2UI(dbPath string) error {
err := initDB(dbPath)
if err != nil {
return common.NewError("init v2-ui database failed:", err)
}
err = database.InitDB(config.GetDBPath())
if err != nil {
return common.NewError("init x-ui database failed:", err)
}
v2Inbounds, err := getV2Inbounds()
if err != nil {
return common.NewError("get v2-ui inbounds failed:", err)
}
if len(v2Inbounds) == 0 {
fmt.Println("migrate v2-ui inbounds success: 0")
return nil
}
userService := service.UserService{}
user, err := userService.GetFirstUser()
if err != nil {
return common.NewError("get x-ui user failed:", err)
}
inbounds := make([]*model.Inbound, 0)
for _, v2inbound := range v2Inbounds {
inbounds = append(inbounds, v2inbound.ToInbound(user.Id))
}
inboundService := service.InboundService{}
err = inboundService.AddInbounds(inbounds)
if err != nil {
return common.NewError("add x-ui inbounds failed:", err)
}
fmt.Println("migrate v2-ui inbounds success:", len(inbounds))
return nil
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,22 +1,5 @@
html,
body {
height: 100vh;
width: 100vw;
margin: 0;
padding: 0;
overflow: hidden;
}
#app { #app {
height: 100%; height: 100%;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: 0;
padding: 0;
overflow: auto;
} }
.ant-space { .ant-space {
@@ -27,14 +10,8 @@ body {
display: none; display: none;
} }
@media (max-width: 768px) {
.ant-layout-sider {
display: none;
}
}
.ant-card { .ant-card {
border-radius: 1.5rem; border-radius: 30px;
} }
.ant-card-hoverable { .ant-card-hoverable {
@@ -179,12 +156,6 @@ body {
padding:16px; padding:16px;
} }
.ant-table-expand-icon-th,
.ant-table-row-expand-icon-cell {
width: 30px;
min-width: 30px;
}
.ant-menu-dark, .ant-menu-dark,
.ant-menu-dark .ant-menu-sub, .ant-menu-dark .ant-menu-sub,
.ant-layout-header, .ant-layout-header,
@@ -192,7 +163,7 @@ body {
.ant-layout-sider-zero-width-trigger, .ant-layout-sider-zero-width-trigger,
.ant-dropdown-menu-dark,.ant-dropdown-menu-dark .ant-dropdown-menu, .ant-dropdown-menu-dark,.ant-dropdown-menu-dark .ant-dropdown-menu,
.ant-menu-dark.ant-menu-horizontal>.ant-menu-item,.ant-menu-dark.ant-menu-horizontal>.ant-menu-submenu { .ant-menu-dark.ant-menu-horizontal>.ant-menu-item,.ant-menu-dark.ant-menu-horizontal>.ant-menu-submenu {
background:#1A202B background:#161b22
} }
.ant-card-dark { .ant-card-dark {
@@ -202,52 +173,8 @@ body {
} }
.ant-card-dark:hover { .ant-card-dark:hover {
/*border-color: #e8e8e8; border-color: #e8e8e8;
animation:light-shadow ease-in 3s infinite;*/
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 80%);
} }
/*
@keyframes light-shadow {
0% {
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 60%);
}
20% {
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 60%);
}
60% {
box-shadow: 0 1px 11px 2px rgb(154 175 238 / 70%);
}
100% {
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 60%);
}
}*/
.ant-setting-textarea {
margin-top: 1.5rem;
min-height: 300px !important;
/*max-height: 800px !important;*/
}
.ant-card-dark-box-nohover{
margin-top: .5rem;
padding: 0 20px 20px !important;
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 0%) !important;
}
.ant-card-dark-box-nohover:hover{
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 0%) !important;
/*background-color: rgb(36 44 58 / 50%);*/
}
.ant-card-dark-securitybox-nohover{
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 0%) !important;
}
.ant-card-dark-securitybox-nohover:hover{
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 0%) !important;
}
/* .ant-card-bordered:hover {
box-shadow: 0 3px 12px -0.8px #0000005c;
} */
.ant-card-dark .ant-table-thead th { .ant-card-dark .ant-table-thead th {
color: hsla(0,0%,100%,.65); color: hsla(0,0%,100%,.65);
@@ -265,7 +192,6 @@ body {
.ant-card-dark .ant-input-group-addon { .ant-card-dark .ant-input-group-addon {
color: hsla(0,0%,100%,.65); color: hsla(0,0%,100%,.65);
background-color: #262f3d; background-color: #262f3d;
border: 1px solid rgb(149 149 149 / 30%);
} }
.ant-card-dark .ant-list-item-meta-title, .ant-card-dark .ant-list-item-meta-title,
@@ -279,7 +205,6 @@ body {
.ant-card-dark .ant-modal-close, .ant-card-dark .ant-modal-close,
.ant-card-dark i, .ant-card-dark i,
.ant-card-dark .ant-select-dropdown-menu-item, .ant-card-dark .ant-select-dropdown-menu-item,
.ant-card-dark .ant-calendar-day-select,
.ant-card-dark .ant-calendar-month-select, .ant-card-dark .ant-calendar-month-select,
.ant-card-dark .ant-calendar-year-select, .ant-card-dark .ant-calendar-year-select,
.ant-card-dark .ant-calendar-date, .ant-card-dark .ant-calendar-date,
@@ -289,80 +214,39 @@ body {
color: hsla(0,0%,100%,.65); color: hsla(0,0%,100%,.65);
} }
.ant-card-dark .ant-table-tbody>tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td { .ant-card-dark .ant-table-tbody>tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td,
background-color: #11314d;
}
.ant-card-dark .ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled), .ant-card-dark .ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled),
.ant-card-dark .ant-select-dropdown-menu-item-active, .ant-card-dark .ant-calendar-date:hover {
.ant-card-dark .ant-calendar-date:hover, background-color: #004488;
.ant-card-dark li.ant-calendar-time-picker-select-option-selected {
background-color: rgb(4, 119, 90);
} }
.ant-card-dark tbody .ant-table-expanded-row, .ant-card-dark tbody .ant-table-expanded-row {
.ant-card-dark .ant-calendar-time-picker-inner {
color: hsla(0,0%,100%,.65); color: hsla(0,0%,100%,.65);
background-color: #1a212a; background-color: #1a212a;
} }
.ant-input-number {
min-width: 100px;
}
.ant-card-dark .ant-input, .ant-card-dark .ant-input,
.ant-card-dark .ant-input-number, .ant-card-dark .ant-input-number,
.ant-card-dark .ant-input-number-handler-wrap,
.ant-card-dark .ant-calendar-input, .ant-card-dark .ant-calendar-input,
.ant-card-dark .ant-select-dropdown-menu-item-selected, .ant-card-dark .ant-select-dropdown-menu-item-selected,
.ant-card-dark .ant-select-selection, .ant-card-dark .ant-select-selection {
.ant-card-dark .ant-calendar-picker-clear {
color: hsla(0,0%,100%,.65); color: hsla(0,0%,100%,.65);
background-color: rgb(46 59 82 / 50%); background-color: #2e3b52;
border: 1px solid rgb(0 150 112 / 0%);
}
.ant-layout:not(.login) .ant-input:focus,
.ant-layout:not(.login) .ant-input:hover,
.ant-layout:not(.login) .ant-input-number:focus,
.ant-layout:not(.login) .ant-input-number:hover,
.ant-layout:not(.login) .ant-calendar-input:focus,
.ant-layout:not(.login) .ant-calendar-input:hover {
background-color: rgba(0, 149, 111, 0.1);
}
.ant-card-dark .ant-select-disabled .ant-select-selection {
border: 1px solid rgba(255, 255, 255, 0.2);
background-color: #242c3a;
} }
.ant-card-dark .ant-collapse-item { .ant-card-dark .ant-collapse-item {
color: hsla(0,0%,100%,.65); color: hsla(0,0%,100%,.65);
background-color: #161b22; background-color: #161b22;
border-radius: 0.5rem 0.5rem 0 0;
}
.ant-dropdown-menu-dark,
.ant-card-dark .ant-modal-content {
border:1px solid rgb(100 100 100 / 20%);
box-shadow: 0 2px 8px rgba(255,255,255,.15);
} }
.ant-card-dark .ant-modal-content, .ant-card-dark .ant-modal-content,
.ant-card-dark .ant-modal-body, .ant-card-dark .ant-modal-body,
.ant-card-dark .ant-modal-header { .ant-card-dark .ant-modal-header,
.ant-card-dark .ant-calendar-selected-day .ant-calendar-date {
color: hsla(0,0%,100%,.65); color: hsla(0,0%,100%,.65);
background-color: #222a37; background-color: #222a37;
} }
.ant-card-dark .ant-calendar-selected-day .ant-calendar-date {
background-color: rgb(0 150 112);
}
.ant-card-dark .ant-calendar-time-picker-select li:hover {
background: #1668dc;
}
.client-table-header { .client-table-header {
background-color: #f0f2f5; background-color: #f0f2f5;
} }
@@ -396,12 +280,6 @@ body {
border: 1px solid hsla(0,0%,100%,.30); border: 1px solid hsla(0,0%,100%,.30);
} }
.ant-card-dark .ant-tag {
color: hsla(0,0%,100%,.65);
background: rgba(255,255,255,.04);
border-color: #434343;
}
.ant-card-dark .ant-tag-blue { .ant-card-dark .ant-tag-blue {
color: #3c9ae8; color: #3c9ae8;
background: #111d2c; background: #111d2c;
@@ -409,9 +287,9 @@ body {
} }
.ant-card-dark .ant-tag-green { .ant-card-dark .ant-tag-green {
color: #37b998; color: #6abe39;
background: #101e1a; background: #162312;
border-color: #144237; border-color: #274916;
} }
.ant-card-dark .ant-tag-cyan { .ant-card-dark .ant-tag-cyan {
@@ -438,7 +316,7 @@ body {
} }
.ant-card-dark .ant-switch-checked { .ant-card-dark .ant-switch-checked {
background-color: #009670; background-color: #0c61b0;
} }
.ant-card-dark .ant-btn, .ant-card-dark .ant-btn,
@@ -449,53 +327,13 @@ body {
} }
.ant-card-dark .ant-radio-button-wrapper:hover { .ant-card-dark .ant-radio-button-wrapper:hover {
color: #009670; color: #177ddc;
} }
.ant-card-dark .ant-btn-primary { .ant-card-dark .ant-btn-primary {
color: hsla(0,0%,100%,.65); color: hsla(0,0%,100%,.65);
background-color: rgb(7 98 75 / 50%); background-color: #073763;
border-color: #009670; border-color: #1890ff;
text-shadow: 0 -1px 0 rgba(255,255,255,.12); text-shadow: 0 -1px 0 rgba(0,0,0,.12);
box-shadow: 0 2px 0 rgba(255,255,255,.045); box-shadow: 0 2px 0 rgba(0,0,0,.045);
}
.ant-card-dark .ant-btn-primary:hover {
background-color: #009670;
border-color: #40a9ff00;
}
.ant-dark .ant-popover-content {
border: 1px solid #e8e8e8;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(255,255,255,.15);
}
.ant-dark .ant-popover-inner {
background: #222a37;
}
.ant-dark .ant-popover-title,
.ant-dark .ant-popover-inner-content {
color: hsla(0,0%,100%,.65);
}
.ant-dark .ant-popover-placement-top>.ant-popover-content>.ant-popover-arrow {
border-color: transparent #2e3b52 #2e3b52 transparent;
}
::-webkit-scrollbar {
width: 0.7em;
}
::-webkit-scrollbar-track {
background: rgb(50 62 82 / 25%);
}
::-webkit-scrollbar-thumb {
background: rgb(133 133 133 / 80%);
border-radius: 100vw;
}
::-webkit-scrollbar-thumb:hover {
background: #919191;
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -2,15 +2,11 @@ axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
axios.interceptors.request.use( axios.interceptors.request.use(
(config) => { config => {
if (config.data instanceof FormData) { config.data = Qs.stringify(config.data, {
config.headers['Content-Type'] = 'multipart/form-data'; arrayFormat: 'repeat'
} else { });
config.data = Qs.stringify(config.data, {
arrayFormat: 'repeat',
});
}
return config; return config;
}, },
(error) => Promise.reject(error), error => Promise.reject(error)
); );

View File

@@ -1,41 +1,36 @@
const supportLangs = [ supportLangs = [
{ {
name: 'English', name : "English",
value: 'en-US', value : "en-US",
icon: '🇺🇸', icon : "🇺🇸"
},
{
name : "Farsi",
value : "fa_IR",
icon : "🇮🇷"
}, },
{ {
name: 'فارسی', name : "汉语",
value: 'fa-IR', value : "zh-Hans",
icon: '🇮🇷', icon : "🇨🇳"
}, },
{ ]
name: '汉语',
value: 'zh-Hans',
icon: '🇨🇳',
},
{
name: 'Русский',
value: 'ru-RU',
icon: '🇷🇺',
},
];
function getLang() { function getLang(){
let lang = getCookie('lang'); let lang = getCookie('lang')
if (!lang) { if (! lang){
if (window.navigator) { if (window.navigator){
lang = window.navigator.language || window.navigator.userLanguage; lang = window.navigator.language || window.navigator.userLanguage;
if (isSupportLang(lang)) { if (isSupportLang(lang)){
setCookie('lang', lang, 150); setCookie('lang' , lang , 150)
} else { }else{
setCookie('lang', 'en-US', 150); setCookie('lang' , 'en-US' , 150)
window.location.reload(); window.location.reload();
} }
} else { }else{
setCookie('lang', 'en-US', 150); setCookie('lang' , 'en-US' , 150)
window.location.reload(); window.location.reload();
} }
} }
@@ -43,21 +38,47 @@ function getLang() {
return lang; return lang;
} }
function setLang(lang) { function setLang(lang){
if (!isSupportLang(lang)) {
if (!isSupportLang(lang)){
lang = 'en-US'; lang = 'en-US';
} }
setCookie('lang', lang, 150); setCookie('lang' , lang , 150)
window.location.reload(); window.location.reload();
} }
function isSupportLang(lang) { function isSupportLang(lang){
for (l of supportLangs) { for (l of supportLangs){
if (l.value === lang) { if (l.value === lang){
return true; return true;
} }
} }
return false; return false;
} }
function getCookie(cname) {
let name = cname + "=";
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(';');
for(let i = 0; i <ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
function setCookie(cname, cvalue, exdays) {
const d = new Date();
d.setTime(d.getTime() + (exdays*24*60*60*1000));
let expires = "expires="+ d.toUTCString();
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
}

View File

@@ -3,7 +3,6 @@ class User {
constructor() { constructor() {
this.username = ""; this.username = "";
this.password = ""; this.password = "";
this.LoginSecret = "";
} }
} }
@@ -37,8 +36,7 @@ class DBInbound {
this.remark = ""; this.remark = "";
this.enable = true; this.enable = true;
this.expiryTime = 0; this.expiryTime = 0;
this.limitIp = 0; this.iplimit = 0;
this.listen = ""; this.listen = "";
this.port = 0; this.port = 0;
this.protocol = ""; this.protocol = "";
@@ -111,6 +109,10 @@ class DBInbound {
get isExpiry() { get isExpiry() {
return this.expiryTime < new Date().getTime(); return this.expiryTime < new Date().getTime();
} }
get isDBInboundEmpty() {
const inbound = this.toInbound();
return inbound.isInboundEmpty();
}
toInbound() { toInbound() {
let settings = {}; let settings = {};
@@ -153,11 +155,10 @@ class DBInbound {
} }
} }
genLink(address=this.address, remark=this.remark, clientIndex=0) { genLink(clientIndex) {
const inbound = this.toInbound(); const inbound = this.toInbound();
return inbound.genLink(address, remark, clientIndex); return inbound.genLink(this.address, this.remark, clientIndex);
} }
get genInboundLinks() { get genInboundLinks() {
const inbound = this.toInbound(); const inbound = this.toInbound();
return inbound.genInboundLinks(this.address, this.remark); return inbound.genInboundLinks(this.address, this.remark);
@@ -168,32 +169,15 @@ class AllSetting {
constructor(data) { constructor(data) {
this.webListen = ""; this.webListen = "";
this.webDomain = "";
this.webPort = 2053; this.webPort = 2053;
this.webCertFile = ""; this.webCertFile = "";
this.webKeyFile = ""; this.webKeyFile = "";
this.webBasePath = "/"; this.webBasePath = "/";
this.sessionMaxAge = "";
this.expireDiff = "";
this.trafficDiff = "";
this.tgBotEnable = false; this.tgBotEnable = false;
this.tgBotToken = ""; this.tgBotToken = "";
this.tgBotChatId = ""; this.tgBotChatId = 0;
this.tgRunTime = "@daily"; this.tgRunTime = "";
this.tgBotBackup = false;
this.tgBotLoginNotify = false;
this.tgCpu = "";
this.tgLang = "en-US";
this.xrayTemplateConfig = ""; this.xrayTemplateConfig = "";
this.secretEnable = false;
this.subEnable = false;
this.subListen = "";
this.subPort = "2096";
this.subPath = "/sub/";
this.subDomain = "";
this.subCertFile = "";
this.subKeyFile = "";
this.subUpdates = 0;
this.timeLocation = "Asia/Tehran"; this.timeLocation = "Asia/Tehran";

File diff suppressed because it is too large Load Diff

View File

@@ -5,9 +5,7 @@ const ONE_TB = ONE_GB * 1024;
const ONE_PB = ONE_TB * 1024; const ONE_PB = ONE_TB * 1024;
function sizeFormat(size) { function sizeFormat(size) {
if (size < 0) { if (size < ONE_KB) {
return "0 B";
} else if (size < ONE_KB) {
return size.toFixed(0) + " B"; return size.toFixed(0) + " B";
} else if (size < ONE_MB) { } else if (size < ONE_MB) {
return (size / ONE_KB).toFixed(2) + " KB"; return (size / ONE_KB).toFixed(2) + " KB";
@@ -22,23 +20,6 @@ function sizeFormat(size) {
} }
} }
function cpuSpeedFormat(speed) {
if (speed > 1000) {
const GHz = speed / 1000;
return GHz.toFixed(2) + " GHz";
} else {
return speed.toFixed(2) + " MHz";
}
}
function cpuCoreFormat(cores) {
if (cores === 1) {
return "1 Core";
} else {
return cores + " Cores";
}
}
function base64(str) { function base64(str) {
return Base64.encode(str); return Base64.encode(str);
} }
@@ -75,81 +56,14 @@ function toFixed(num, n) {
return Math.round(num * n) / n; return Math.round(num * n) / n;
} }
function debounce(fn, delay) { function debounce (fn, delay) {
var timeoutID = null; var timeoutID = null
return function () { return function () {
clearTimeout(timeoutID); clearTimeout(timeoutID)
var args = arguments; var args = arguments
var that = this; var that = this
timeoutID = setTimeout(function () { timeoutID = setTimeout(function () {
fn.apply(that, args); fn.apply(that, args)
}, delay); }, delay)
};
}
function getCookie(cname) {
let name = cname + '=';
let ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
// decode cookie value only
return decodeURIComponent(c.substring(name.length, c.length));
}
} }
return ''; }
}
function setCookie(cname, cvalue, exdays) {
const d = new Date();
d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000);
let expires = 'expires=' + d.toUTCString();
// encode cookie value
document.cookie = cname + '=' + encodeURIComponent(cvalue) + ';' + expires + ';path=/';
}
function usageColor(data, threshold, total) {
switch (true) {
case data === null:
return 'blue';
case total <= 0:
return 'blue';
case data < total - threshold:
return 'cyan';
case data < total:
return 'orange';
default:
return 'red';
}
}
function doAllItemsExist(array1, array2) {
for (let i = 0; i < array1.length; i++) {
if (!array2.includes(array1[i])) {
return false;
}
}
return true;
}
function buildURL({ host, port, isTLS, base, path }) {
if (!host || host.length === 0) host = window.location.hostname;
if (!port || port.length === 0) port = window.location.port;
if (isTLS === undefined) isTLS = window.location.protocol === "https:";
const protocol = isTLS ? "https:" : "http:";
port = String(port);
if (port === "" || (isTLS && port === "443") || (!isTLS && port === "80")) {
port = "";
} else {
port = `:${port}`;
}
return `${protocol}//${host}${port}${base}${path}`;
}

View File

@@ -1,67 +1,67 @@
const oneMinute = 1000 * 60; // MilliseConds in a Minute const oneMinute = 1000 * 60; // 一分钟的毫秒数
const oneHour = oneMinute * 60; // The milliseconds of one hour const oneHour = oneMinute * 60; // 一小时的毫秒数
const oneDay = oneHour * 24; // The Number of MilliseConds A Day const oneDay = oneHour * 24; // 一天的毫秒数
const oneWeek = oneDay * 7; // The milliseconds per week const oneWeek = oneDay * 7; // 一星期的毫秒数
const oneMonth = oneDay * 30; // The milliseconds of a month const oneMonth = oneDay * 30; // 一个月的毫秒数
/** /**
* Decrease according to the number of days * 按天数减少
* *
* @param days to reduce the number of days to be reduced * @param days 要减少的天数
*/ */
Date.prototype.minusDays = function (days) { Date.prototype.minusDays = function (days) {
return this.minusMillis(oneDay * days); return this.minusMillis(oneDay * days);
}; };
/** /**
* Increase according to the number of days * 按天数增加
* *
* @param days The number of days to be increased * @param days 要增加的天数
*/ */
Date.prototype.plusDays = function (days) { Date.prototype.plusDays = function (days) {
return this.plusMillis(oneDay * days); return this.plusMillis(oneDay * days);
}; };
/** /**
* A few * 按小时减少
* *
* @param hours to be reduced * @param hours 要减少的小时数
*/ */
Date.prototype.minusHours = function (hours) { Date.prototype.minusHours = function (hours) {
return this.minusMillis(oneHour * hours); return this.minusMillis(oneHour * hours);
}; };
/** /**
* Increase hourly * 按小时增加
* *
* @param hours to increase the number of hours * @param hours 要增加的小时数
*/ */
Date.prototype.plusHours = function (hours) { Date.prototype.plusHours = function (hours) {
return this.plusMillis(oneHour * hours); return this.plusMillis(oneHour * hours);
}; };
/** /**
* Make reduction in minutes * 按分钟减少
* *
* @param minutes to reduce the number of minutes * @param minutes 要减少的分钟数
*/ */
Date.prototype.minusMinutes = function (minutes) { Date.prototype.minusMinutes = function (minutes) {
return this.minusMillis(oneMinute * minutes); return this.minusMillis(oneMinute * minutes);
}; };
/** /**
* Add in minutes * 按分钟增加
* *
* @param minutes to increase the number of minutes * @param minutes 要增加的分钟数
*/ */
Date.prototype.plusMinutes = function (minutes) { Date.prototype.plusMinutes = function (minutes) {
return this.plusMillis(oneMinute * minutes); return this.plusMillis(oneMinute * minutes);
}; };
/** /**
* Decrease in milliseconds * 按毫秒减少
* *
* @param millis to reduce the milliseconds * @param millis 要减少的毫秒数
*/ */
Date.prototype.minusMillis = function(millis) { Date.prototype.minusMillis = function(millis) {
let time = this.getTime() - millis; let time = this.getTime() - millis;
@@ -71,9 +71,9 @@ Date.prototype.minusMillis = function(millis) {
}; };
/** /**
* Add in milliseconds to increase * 按毫秒增加
* *
* @param millis to increase the milliseconds to increase * @param millis 要增加的毫秒数
*/ */
Date.prototype.plusMillis = function(millis) { Date.prototype.plusMillis = function(millis) {
let time = this.getTime() + millis; let time = this.getTime() + millis;
@@ -83,7 +83,7 @@ Date.prototype.plusMillis = function(millis) {
}; };
/** /**
* Setting time is 00: 00: 00.000 on the day * 设置时间为当天的 00:00:00.000
*/ */
Date.prototype.setMinTime = function () { Date.prototype.setMinTime = function () {
this.setHours(0); this.setHours(0);
@@ -94,7 +94,7 @@ Date.prototype.setMinTime = function () {
}; };
/** /**
* Setting time is 23: 59: 59.999 on the same day * 设置时间为当天的 23:59:59.999
*/ */
Date.prototype.setMaxTime = function () { Date.prototype.setMaxTime = function () {
this.setHours(23); this.setHours(23);
@@ -105,36 +105,37 @@ Date.prototype.setMaxTime = function () {
}; };
/** /**
* Formatting date * 格式化日期
*/ */
Date.prototype.formatDate = function () { Date.prototype.formatDate = function () {
return this.getFullYear() + "-" + addZero(this.getMonth() + 1) + "-" + addZero(this.getDate()); return this.getFullYear() + "-" + addZero(this.getMonth() + 1) + "-" + addZero(this.getDate());
}; };
/** /**
* Format time * 格式化时间
*/ */
Date.prototype.formatTime = function () { Date.prototype.formatTime = function () {
return addZero(this.getHours()) + ":" + addZero(this.getMinutes()) + ":" + addZero(this.getSeconds()); return addZero(this.getHours()) + ":" + addZero(this.getMinutes()) + ":" + addZero(this.getSeconds());
}; };
/** /**
* Formatting date plus time * 格式化日期加时间
* *
* @param split Date and time separation symbols, default is a space * @param split 日期和时间之间的分隔符,默认是一个空格
*/ */
Date.prototype.formatDateTime = function (split = ' ') { Date.prototype.formatDateTime = function (split = ' ') {
return this.formatDate() + split + this.formatTime(); return this.formatDate() + split + this.formatTime();
}; };
class DateUtil { class DateUtil {
// String string to date object
// 字符串转 Date 对象
static parseDate(str) { static parseDate(str) {
return new Date(str.replace(/-/g, '/')); return new Date(str.replace(/-/g, '/'));
} }
static formatMillis(millis) { static formatMillis(millis) {
return moment(millis).format('YYYY-M-D H:m:s'); return moment(millis).format('YYYY-M-D H:m:s')
} }
static firstDayOfMonth() { static firstDayOfMonth() {

View File

@@ -68,18 +68,31 @@ class HttpUtil {
} }
class PromiseUtil { class PromiseUtil {
static async sleep(timeout) { static async sleep(timeout) {
await new Promise(resolve => { await new Promise(resolve => {
setTimeout(resolve, timeout) setTimeout(resolve, timeout)
}); });
} }
} }
const seq = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); const seq = [
'a', 'b', 'c', 'd', 'e', 'f', 'g',
'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'A', 'B', 'C', 'D', 'E', 'F', 'G',
'H', 'I', 'J', 'K', 'L', 'M', 'N',
'O', 'P', 'Q', 'R', 'S', 'T',
'U', 'V', 'W', 'X', 'Y', 'Z'
];
class RandomUtil { class RandomUtil {
static randomIntRange(min, max) { static randomIntRange(min, max) {
return Math.floor(Math.random() * (max - min) + min); return parseInt(Math.random() * (max - min) + min, 10);
} }
static randomInt(n) { static randomInt(n) {
@@ -94,41 +107,49 @@ class RandomUtil {
return str; return str;
} }
static randomShortId() { static randomLowerAndNum(count) {
let str = ''; let str = '';
for (let i = 0; i < 8; ++i) { for (let i = 0; i < count; ++i) {
str += seq[this.randomInt(16)];
}
return str;
}
static randomLowerAndNum(len) {
let str = '';
for (let i = 0; i < len; ++i) {
str += seq[this.randomInt(36)]; str += seq[this.randomInt(36)];
} }
return str; return str;
} }
static randomMTSecret() {
let str = '';
for (let i = 0; i < 32; ++i) {
let index = this.randomInt(16);
if (index <= 9) {
str += index;
} else {
str += seq[index - 10];
}
}
return str;
}
static randomUUID() { static randomUUID() {
const template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'; let d = new Date().getTime();
return template.replace(/[xy]/g, function (c) { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const randomValues = new Uint8Array(1); let r = (d + Math.random() * 16) % 16 | 0;
crypto.getRandomValues(randomValues); d = Math.floor(d / 16);
let randomValue = randomValues[0] % 16; return (c === 'x' ? r : (r & 0x7 | 0x8)).toString(16);
let calculatedValue = (c === 'x') ? randomValue : (randomValue & 0x3 | 0x8);
return calculatedValue.toString(16);
}); });
} }
static randomShadowsocksPassword() { static randomText() {
let array = new Uint8Array(32); var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
window.crypto.getRandomValues(array); var string = '';
return btoa(String.fromCharCode.apply(null, array)); var len = 6 + Math.floor(Math.random() * 5)
for(var ii=0; ii<len; ii++){
string += chars[Math.floor(Math.random() * chars.length)];
}
return string;
} }
} }
class ObjectUtil { class ObjectUtil {
static getPropIgnoreCase(obj, prop) { static getPropIgnoreCase(obj, prop) {
for (const name in obj) { for (const name in obj) {
if (!obj.hasOwnProperty(name)) { if (!obj.hasOwnProperty(name)) {
@@ -276,4 +297,5 @@ class ObjectUtil {
} }
return true; return true;
} }
} }

View File

@@ -1,15 +1,13 @@
package controller package controller
import ( import (
"x-ui/web/service"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
type APIController struct { type APIController struct {
BaseController BaseController
inboundController *InboundController inboundController *InboundController
Tgbot service.Tgbot settingController *SettingController
} }
func NewAPIController(g *gin.RouterGroup) *APIController { func NewAPIController(g *gin.RouterGroup) *APIController {
@@ -19,88 +17,32 @@ func NewAPIController(g *gin.RouterGroup) *APIController {
} }
func (a *APIController) initRouter(g *gin.RouterGroup) { func (a *APIController) initRouter(g *gin.RouterGroup) {
g = g.Group("/panel/api/inbounds") g = g.Group("/xui/API/inbounds")
g.Use(a.checkLogin) g.Use(a.checkLogin)
g.GET("/list", a.getAllInbounds) g.GET("/", a.inbounds)
g.GET("/get/:id", a.getSingleInbound) g.GET("/get/:id", a.inbound)
g.GET("/getClientTraffics/:email", a.getClientTraffics)
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("/addClient", a.addInboundClient)
g.POST("/:id/delClient/:clientId", a.delInboundClient)
g.POST("/updateClient/:clientId", a.updateInboundClient)
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
g.POST("/resetAllTraffics", a.resetAllTraffics)
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
g.POST("/delDepletedClients/:id", a.delDepletedClients)
g.GET("/createbackup", a.createBackup)
a.inboundController = NewInboundController(g) a.inboundController = NewInboundController(g)
} }
func (a *APIController) getAllInbounds(c *gin.Context) {
func (a *APIController) inbounds(c *gin.Context) {
a.inboundController.getInbounds(c) a.inboundController.getInbounds(c)
} }
func (a *APIController) inbound(c *gin.Context) {
func (a *APIController) getSingleInbound(c *gin.Context) {
a.inboundController.getInbound(c) a.inboundController.getInbound(c)
} }
func (a *APIController) getClientTraffics(c *gin.Context) {
a.inboundController.getClientTraffics(c)
}
func (a *APIController) addInbound(c *gin.Context) { func (a *APIController) addInbound(c *gin.Context) {
a.inboundController.addInbound(c) a.inboundController.addInbound(c)
} }
func (a *APIController) delInbound(c *gin.Context) { func (a *APIController) delInbound(c *gin.Context) {
a.inboundController.delInbound(c) a.inboundController.delInbound(c)
} }
func (a *APIController) updateInbound(c *gin.Context) { func (a *APIController) updateInbound(c *gin.Context) {
a.inboundController.updateInbound(c) a.inboundController.updateInbound(c)
} }
func (a *APIController) getClientIps(c *gin.Context) {
a.inboundController.getClientIps(c)
}
func (a *APIController) clearClientIps(c *gin.Context) {
a.inboundController.clearClientIps(c)
}
func (a *APIController) addInboundClient(c *gin.Context) {
a.inboundController.addInboundClient(c)
}
func (a *APIController) delInboundClient(c *gin.Context) {
a.inboundController.delInboundClient(c)
}
func (a *APIController) updateInboundClient(c *gin.Context) {
a.inboundController.updateInboundClient(c)
}
func (a *APIController) resetClientTraffic(c *gin.Context) {
a.inboundController.resetClientTraffic(c)
}
func (a *APIController) resetAllTraffics(c *gin.Context) {
a.inboundController.resetAllTraffics(c)
}
func (a *APIController) resetAllClientTraffics(c *gin.Context) {
a.inboundController.resetAllClientTraffics(c)
}
func (a *APIController) delDepletedClients(c *gin.Context) {
a.inboundController.delDepletedClients(c)
}
func (a *APIController) createBackup(c *gin.Context) {
a.Tgbot.SendBackupToAdmins()
}

View File

@@ -1,12 +1,9 @@
package controller package controller
import ( import (
"net/http"
"x-ui/logger"
"x-ui/web/locale"
"x-ui/web/session"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"net/http"
"x-ui/web/session"
) )
type BaseController struct { type BaseController struct {
@@ -15,7 +12,7 @@ type BaseController struct {
func (a *BaseController) checkLogin(c *gin.Context) { func (a *BaseController) checkLogin(c *gin.Context) {
if !session.IsLogin(c) { if !session.IsLogin(c) {
if isAjax(c) { if isAjax(c) {
pureJsonMsg(c, false, I18nWeb(c, "pages.login.loginAgain")) pureJsonMsg(c, false, I18n(c, "pages.login.loginAgain"))
} else { } else {
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path")) c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
} }
@@ -25,13 +22,11 @@ func (a *BaseController) checkLogin(c *gin.Context) {
} }
} }
func I18nWeb(c *gin.Context, name string, params ...string) string { func I18n(c *gin.Context, name string) string {
anyfunc, funcExists := c.Get("I18n") anyfunc, _ := c.Get("I18n")
if !funcExists { i18n, _ := anyfunc.(func(key string, params ...string) (string, error))
logger.Warning("I18n function not exists in gin context!")
return "" message, _ := i18n(name)
}
i18nFunc, _ := anyfunc.(func(i18nType locale.I18nType, key string, keyParams ...string) string) return message
msg := i18nFunc(locale.Web, name, params...)
return msg
} }

View File

@@ -33,13 +33,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.POST("/update/:id", a.updateInbound) g.POST("/update/:id", a.updateInbound)
g.POST("/clientIps/:email", a.getClientIps) g.POST("/clientIps/:email", a.getClientIps)
g.POST("/clearClientIps/:email", a.clearClientIps) g.POST("/clearClientIps/:email", a.clearClientIps)
g.POST("/addClient", a.addInboundClient) g.POST("/resetClientTraffic/:email", a.resetClientTraffic)
g.POST("/:id/delClient/:clientId", a.delInboundClient)
g.POST("/updateClient/:clientId", a.updateInboundClient)
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
g.POST("/resetAllTraffics", a.resetAllTraffics)
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
g.POST("/delDepletedClients/:id", a.delDepletedClients)
} }
@@ -60,40 +54,30 @@ 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, I18nWeb(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)
} }
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, I18nWeb(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, I18nWeb(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)
} }
func (a *InboundController) getClientTraffics(c *gin.Context) {
email := c.Param("email")
clientTraffics, err := a.inboundService.GetClientTrafficByEmail(email)
if err != nil {
jsonMsg(c, "Error getting traffics", err)
return
}
jsonObj(c, clientTraffics, nil)
}
func (a *InboundController) addInbound(c *gin.Context) { 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, I18nWeb(c, "pages.inbounds.create"), err) jsonMsg(c, I18n(c, "pages.inbounds.addTo"), err)
return return
} }
user := session.GetLoginUser(c) user := session.GetLoginUser(c)
@@ -101,7 +85,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, I18nWeb(c, "pages.inbounds.create"), inbound, err) jsonMsgObj(c, I18n(c, "pages.inbounds.addTo"), inbound, err)
if err == nil { if err == nil {
a.xrayService.SetToNeedRestart() a.xrayService.SetToNeedRestart()
} }
@@ -110,11 +94,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, I18nWeb(c, "delete"), err) jsonMsg(c, I18n(c, "delete"), err)
return return
} }
err = a.inboundService.DelInbound(id) err = a.inboundService.DelInbound(id)
jsonMsgObj(c, I18nWeb(c, "delete"), id, err) jsonMsgObj(c, I18n(c, "delete"), id, err)
if err == nil { if err == nil {
a.xrayService.SetToNeedRestart() a.xrayService.SetToNeedRestart()
} }
@@ -123,7 +107,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, I18nWeb(c, "pages.inbounds.update"), err) jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
return return
} }
inbound := &model.Inbound{ inbound := &model.Inbound{
@@ -131,163 +115,42 @@ func (a *InboundController) updateInbound(c *gin.Context) {
} }
err = c.ShouldBind(inbound) err = c.ShouldBind(inbound)
if err != nil { if err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), 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, I18nWeb(c, "pages.inbounds.update"), 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) { func (a *InboundController) getClientIps(c *gin.Context) {
email := c.Param("email") email := c.Param("email")
ips, err := a.inboundService.GetInboundClientIps(email) ips , err := a.inboundService.GetInboundClientIps(email)
if err != nil || ips == "" { if err != nil {
jsonObj(c, "No IP Record", nil) jsonObj(c, "No IP Record", nil)
return return
} }
jsonObj(c, ips, nil) jsonObj(c, ips, nil)
} }
func (a *InboundController) clearClientIps(c *gin.Context) { func (a *InboundController) clearClientIps(c *gin.Context) {
email := c.Param("email") email := c.Param("email")
err := a.inboundService.ClearClientIps(email) err := a.inboundService.ClearClientIps(email)
if err != nil { if err != nil {
jsonMsg(c, "Update", err) jsonMsg(c, "修改", err)
return return
} }
jsonMsg(c, "Log Cleared", nil) jsonMsg(c, "Log Cleared", nil)
} }
func (a *InboundController) addInboundClient(c *gin.Context) {
data := &model.Inbound{}
err := c.ShouldBind(data)
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return
}
needRestart := false
needRestart, err = a.inboundService.AddInboundClient(data)
if err != nil {
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "Client(s) added", nil)
if err == nil && needRestart {
a.xrayService.SetToNeedRestart()
}
}
func (a *InboundController) delInboundClient(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return
}
clientId := c.Param("clientId")
needRestart := false
needRestart, err = a.inboundService.DelInboundClient(id, clientId)
if err != nil {
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "Client deleted", nil)
if err == nil && needRestart {
a.xrayService.SetToNeedRestart()
}
}
func (a *InboundController) updateInboundClient(c *gin.Context) {
clientId := c.Param("clientId")
inbound := &model.Inbound{}
err := c.ShouldBind(inbound)
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return
}
needRestart := false
needRestart, err = a.inboundService.UpdateInboundClient(inbound, clientId)
if err != nil {
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "Client updated", nil)
if err == nil && needRestart {
a.xrayService.SetToNeedRestart()
}
}
func (a *InboundController) resetClientTraffic(c *gin.Context) { func (a *InboundController) resetClientTraffic(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return
}
email := c.Param("email") email := c.Param("email")
needRestart := false err := a.inboundService.ResetClientTraffic(email)
needRestart, err = a.inboundService.ResetClientTraffic(id, email)
if err != nil { if err != nil {
jsonMsg(c, "Something went wrong!", err) jsonMsg(c, "something worng!", err)
return return
} }
jsonMsg(c, "traffic reseted", nil) jsonMsg(c, "traffic reseted", nil)
if err == nil && needRestart {
a.xrayService.SetToNeedRestart()
}
}
func (a *InboundController) resetAllTraffics(c *gin.Context) {
err := a.inboundService.ResetAllTraffics()
if err != nil {
jsonMsg(c, "Something went wrong!", err)
return
} else {
a.xrayService.SetToNeedRestart()
}
jsonMsg(c, "All traffics reseted", nil)
}
func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return
}
err = a.inboundService.ResetAllClientTraffics(id)
if err != nil {
jsonMsg(c, "Something went wrong!", err)
return
} else {
a.xrayService.SetToNeedRestart()
}
jsonMsg(c, "All traffics of client reseted", nil)
}
func (a *InboundController) delDepletedClients(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return
}
err = a.inboundService.DelDepletedClients(id)
if err != nil {
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "All delpeted clients are deleted", nil)
} }

View File

@@ -4,6 +4,7 @@ import (
"net/http" "net/http"
"time" "time"
"x-ui/logger" "x-ui/logger"
"x-ui/web/job"
"x-ui/web/service" "x-ui/web/service"
"x-ui/web/session" "x-ui/web/session"
@@ -11,17 +12,14 @@ import (
) )
type LoginForm struct { type LoginForm struct {
Username string `json:"username" form:"username"` Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"` Password string `json:"password" form:"password"`
LoginSecret string `json:"loginSecret" form:"loginSecret"`
} }
type IndexController struct { type IndexController struct {
BaseController BaseController
settingService service.SettingService userService service.UserService
userService service.UserService
tgbot service.Tgbot
} }
func NewIndexController(g *gin.RouterGroup) *IndexController { func NewIndexController(g *gin.RouterGroup) *IndexController {
@@ -34,12 +32,11 @@ func (a *IndexController) initRouter(g *gin.RouterGroup) {
g.GET("/", a.index) g.GET("/", a.index)
g.POST("/login", a.login) g.POST("/login", a.login)
g.GET("/logout", a.logout) g.GET("/logout", a.logout)
g.POST("/getSecretStatus", a.getSecretStatus)
} }
func (a *IndexController) index(c *gin.Context) { func (a *IndexController) index(c *gin.Context) {
if session.IsLogin(c) { if session.IsLogin(c) {
c.Redirect(http.StatusTemporaryRedirect, "panel/") c.Redirect(http.StatusTemporaryRedirect, "xui/")
return return
} }
html(c, "login.html", "pages.login.title", nil) html(c, "login.html", "pages.login.title", nil)
@@ -49,45 +46,32 @@ func (a *IndexController) login(c *gin.Context) {
var form LoginForm var form LoginForm
err := c.ShouldBind(&form) err := c.ShouldBind(&form)
if err != nil { if err != nil {
pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.invalidFormData")) pureJsonMsg(c, false, I18n(c, "pages.login.toasts.invalidFormData"))
return return
} }
if form.Username == "" { if form.Username == "" {
pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.emptyUsername")) pureJsonMsg(c, false, I18n(c, "pages.login.toasts.emptyUsername"))
return return
} }
if form.Password == "" { if form.Password == "" {
pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.emptyPassword")) pureJsonMsg(c, false, I18n(c, "pages.login.toasts.emptyPassword"))
return return
} }
user := a.userService.CheckUser(form.Username, form.Password)
user := a.userService.CheckUser(form.Username, form.Password, form.LoginSecret)
timeStr := time.Now().Format("2006-01-02 15:04:05") timeStr := time.Now().Format("2006-01-02 15:04:05")
if user == nil { if user == nil {
logger.Warningf("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password) job.NewStatsNotifyJob().UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0) logger.Infof("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password)
pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword")) pureJsonMsg(c, false, I18n(c, "pages.login.toasts.wrongUsernameOrPassword"))
return return
} else { } else {
logger.Infof("%s login success, Ip Address: %s\n", form.Username, getRemoteIp(c)) logger.Infof("%s login success,Ip Address:%s\n", form.Username, getRemoteIp(c))
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 1) job.NewStatsNotifyJob().UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 1)
}
sessionMaxAge, err := a.settingService.GetSessionMaxAge()
if err != nil {
logger.Warningf("Unable to get session's max age from DB")
}
if sessionMaxAge > 0 {
err = session.SetMaxAge(c, sessionMaxAge*60)
if err != nil {
logger.Warningf("Unable to set session's max age")
}
} }
err = session.SetLoginUser(c, user) err = session.SetLoginUser(c, user)
logger.Info("user", user.Id, "login success") logger.Info("user", user.Id, "login success")
jsonMsg(c, I18nWeb(c, "pages.login.toasts.successLogin"), err) jsonMsg(c, I18n(c, "pages.login.toasts.successLogin"), err)
} }
func (a *IndexController) logout(c *gin.Context) { func (a *IndexController) logout(c *gin.Context) {
@@ -98,10 +82,3 @@ func (a *IndexController) logout(c *gin.Context) {
session.ClearSession(c) session.ClearSession(c)
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path")) c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
} }
func (a *IndexController) getSecretStatus(c *gin.Context) {
status, err := a.settingService.GetSecretStatus()
if err == nil {
jsonObj(c, status, nil)
}
}

View File

@@ -1,18 +1,12 @@
package controller package controller
import ( import (
"fmt" "github.com/gin-gonic/gin"
"net/http"
"regexp"
"time" "time"
"x-ui/web/global" "x-ui/web/global"
"x-ui/web/service" "x-ui/web/service"
"github.com/gin-gonic/gin"
) )
var filenameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\-.]+$`)
type ServerController struct { type ServerController struct {
BaseController BaseController
@@ -43,11 +37,6 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
g.POST("/stopXrayService", a.stopXrayService) g.POST("/stopXrayService", a.stopXrayService)
g.POST("/restartXrayService", a.restartXrayService) g.POST("/restartXrayService", a.restartXrayService)
g.POST("/installXray/:version", a.installXray) g.POST("/installXray/:version", a.installXray)
g.POST("/logs/:count", a.getLogs)
g.POST("/getConfigJson", a.getConfigJson)
g.GET("/getDb", a.getDb)
g.POST("/importDB", a.importDB)
g.POST("/getNewX25519Cert", a.getNewX25519Cert)
} }
func (a *ServerController) refreshStatus() { func (a *ServerController) refreshStatus() {
@@ -81,7 +70,7 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
versions, err := a.serverService.GetXrayVersions() versions, err := a.serverService.GetXrayVersions()
if err != nil { if err != nil {
jsonMsg(c, I18nWeb(c, "getVersion"), err) jsonMsg(c, I18n(c, "getVersion"), err)
return return
} }
@@ -94,102 +83,25 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
func (a *ServerController) installXray(c *gin.Context) { func (a *ServerController) installXray(c *gin.Context) {
version := c.Param("version") version := c.Param("version")
err := a.serverService.UpdateXray(version) err := a.serverService.UpdateXray(version)
jsonMsg(c, I18nWeb(c, "install")+" xray", err) jsonMsg(c, I18n(c, "install")+" xray", err)
} }
func (a *ServerController) stopXrayService(c *gin.Context) { func (a *ServerController) stopXrayService(c *gin.Context) {
a.lastGetStatusTime = time.Now() a.lastGetStatusTime = time.Now()
err := a.serverService.StopXrayService() err := a.serverService.StopXrayService()
if err != nil { if err != nil {
jsonMsg(c, "", err) jsonMsg(c, "", err)
return return
} }
jsonMsg(c, "Xray stoped", err) jsonMsg(c, "Xray stoped",err)
}
}
func (a *ServerController) restartXrayService(c *gin.Context) { func (a *ServerController) restartXrayService(c *gin.Context) {
err := a.serverService.RestartXrayService() err := a.serverService.RestartXrayService()
if err != nil { if err != nil {
jsonMsg(c, "", err) jsonMsg(c, "", err)
return return
} }
jsonMsg(c, "Xray restarted", err) jsonMsg(c, "Xray restarted",err)
}
func (a *ServerController) getLogs(c *gin.Context) {
count := c.Param("count")
logLevel := c.PostForm("logLevel")
logs, err := a.serverService.GetLogs(count, logLevel)
if err != nil {
jsonMsg(c, "getLogs", err)
return
}
jsonObj(c, logs, nil)
}
func (a *ServerController) getConfigJson(c *gin.Context) {
configJson, err := a.serverService.GetConfigJson()
if err != nil {
jsonMsg(c, "get config.json", err)
return
}
jsonObj(c, configJson, nil)
}
func (a *ServerController) getDb(c *gin.Context) {
db, err := a.serverService.GetDb()
if err != nil {
jsonMsg(c, "get Database", err)
return
}
filename := "x-ui.db"
if !isValidFilename(filename) {
c.AbortWithError(http.StatusBadRequest, fmt.Errorf("invalid filename"))
return
}
// Set the headers for the response
c.Header("Content-Type", "application/octet-stream")
c.Header("Content-Disposition", "attachment; filename="+filename)
// Write the file contents to the response
c.Writer.Write(db)
}
func isValidFilename(filename string) bool {
// Validate that the filename only contains allowed characters
return filenameRegex.MatchString(filename)
}
func (a *ServerController) importDB(c *gin.Context) {
// Get the file from the request body
file, _, err := c.Request.FormFile("db")
if err != nil {
jsonMsg(c, "Error reading db file", err)
return
}
defer file.Close()
// Always restart Xray before return
defer a.serverService.RestartXrayService()
defer func() {
a.lastGetStatusTime = time.Now()
}()
// Import it
err = a.serverService.ImportDB(file)
if err != nil {
jsonMsg(c, "", err)
return
}
jsonObj(c, "Import DB", nil)
}
func (a *ServerController) getNewX25519Cert(c *gin.Context) {
cert, err := a.serverService.GetNewX25519Cert()
if err != nil {
jsonMsg(c, "get x25519 certificate", err)
return
}
jsonObj(c, cert, nil)
} }

View File

@@ -2,12 +2,11 @@ package controller
import ( import (
"errors" "errors"
"github.com/gin-gonic/gin"
"time" "time"
"x-ui/web/entity" "x-ui/web/entity"
"x-ui/web/service" "x-ui/web/service"
"x-ui/web/session" "x-ui/web/session"
"github.com/gin-gonic/gin"
) )
type updateUserForm struct { type updateUserForm struct {
@@ -17,10 +16,6 @@ type updateUserForm struct {
NewPassword string `json:"newPassword" form:"newPassword"` NewPassword string `json:"newPassword" form:"newPassword"`
} }
type updateSecretForm struct {
LoginSecret string `json:"loginSecret" form:"loginSecret"`
}
type SettingController struct { type SettingController struct {
settingService service.SettingService settingService service.SettingService
userService service.UserService userService service.UserService
@@ -37,98 +32,45 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
g = g.Group("/setting") g = g.Group("/setting")
g.POST("/all", a.getAllSetting) g.POST("/all", a.getAllSetting)
g.POST("/defaultSettings", a.getDefaultSettings)
g.POST("/update", a.updateSetting) g.POST("/update", a.updateSetting)
g.POST("/updateUser", a.updateUser) g.POST("/updateUser", a.updateUser)
g.POST("/restartPanel", a.restartPanel) g.POST("/restartPanel", a.restartPanel)
g.GET("/getDefaultJsonConfig", a.getDefaultJsonConfig)
g.POST("/updateUserSecret", a.updateSecret)
g.POST("/getUserSecret", a.getUserSecret)
} }
func (a *SettingController) getAllSetting(c *gin.Context) { func (a *SettingController) getAllSetting(c *gin.Context) {
allSetting, err := a.settingService.GetAllSetting() allSetting, err := a.settingService.GetAllSetting()
if err != nil { if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err)
return return
} }
jsonObj(c, allSetting, nil) jsonObj(c, allSetting, nil)
} }
func (a *SettingController) getDefaultJsonConfig(c *gin.Context) {
defaultJsonConfig, err := a.settingService.GetDefaultJsonConfig()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
jsonObj(c, defaultJsonConfig, nil)
}
func (a *SettingController) getDefaultSettings(c *gin.Context) {
type settingFunc func() (interface{}, error)
settings := map[string]settingFunc{
"expireDiff": func() (interface{}, error) { return a.settingService.GetExpireDiff() },
"trafficDiff": func() (interface{}, error) { return a.settingService.GetTrafficDiff() },
"defaultCert": func() (interface{}, error) { return a.settingService.GetCertFile() },
"defaultKey": func() (interface{}, error) { return a.settingService.GetKeyFile() },
"tgBotEnable": func() (interface{}, error) { return a.settingService.GetTgbotenabled() },
"subEnable": func() (interface{}, error) { return a.settingService.GetSubEnable() },
"subPort": func() (interface{}, error) { return a.settingService.GetSubPort() },
"subPath": func() (interface{}, error) { return a.settingService.GetSubPath() },
"subDomain": func() (interface{}, error) { return a.settingService.GetSubDomain() },
"subKeyFile": func() (interface{}, error) { return a.settingService.GetSubKeyFile() },
"subCertFile": func() (interface{}, error) { return a.settingService.GetSubCertFile() },
}
result := make(map[string]interface{})
for key, fn := range settings {
value, err := fn()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
result[key] = value
}
subTLS := false
if result["subKeyFile"] != "" || result["subCertFile"] != "" {
subTLS = true
}
result["subTLS"] = subTLS
delete(result, "subKeyFile")
delete(result, "subCertFile")
jsonObj(c, result, nil)
}
func (a *SettingController) updateSetting(c *gin.Context) { func (a *SettingController) updateSetting(c *gin.Context) {
allSetting := &entity.AllSetting{} allSetting := &entity.AllSetting{}
err := c.ShouldBind(allSetting) err := c.ShouldBind(allSetting)
if err != nil { if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err) jsonMsg(c, I18n(c, "pages.setting.toasts.modifySetting"), err)
return return
} }
err = a.settingService.UpdateAllSetting(allSetting) err = a.settingService.UpdateAllSetting(allSetting)
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err) jsonMsg(c, I18n(c, "pages.setting.toasts.modifySetting"), err)
} }
func (a *SettingController) updateUser(c *gin.Context) { func (a *SettingController) updateUser(c *gin.Context) {
form := &updateUserForm{} form := &updateUserForm{}
err := c.ShouldBind(form) err := c.ShouldBind(form)
if err != nil { if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err) jsonMsg(c, I18n(c, "pages.setting.toasts.modifySetting"), err)
return return
} }
user := session.GetLoginUser(c) user := session.GetLoginUser(c)
if user.Username != form.OldUsername || user.Password != form.OldPassword { if user.Username != form.OldUsername || user.Password != form.OldPassword {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), errors.New(I18nWeb(c, "pages.settings.toasts.originalUserPassIncorrect"))) jsonMsg(c, I18n(c, "pages.setting.toasts.modifyUser"), errors.New(I18n(c, "pages.setting.toasts.originalUserPassIncorrect")))
return return
} }
if form.NewUsername == "" || form.NewPassword == "" { if form.NewUsername == "" || form.NewPassword == "" {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), errors.New(I18nWeb(c, "pages.settings.toasts.userPassMustBeNotEmpty"))) jsonMsg(c, I18n(c, "pages.setting.toasts.modifyUser"), errors.New(I18n(c, "pages.setting.toasts.userPassMustBeNotEmpty")))
return return
} }
err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword) err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword)
@@ -137,33 +79,10 @@ func (a *SettingController) updateUser(c *gin.Context) {
user.Password = form.NewPassword user.Password = form.NewPassword
session.SetLoginUser(c, user) session.SetLoginUser(c, user)
} }
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err) jsonMsg(c, I18n(c, "pages.setting.toasts.modifyUser"), err)
} }
func (a *SettingController) restartPanel(c *gin.Context) { func (a *SettingController) restartPanel(c *gin.Context) {
err := a.panelService.RestartPanel(time.Second * 3) err := a.panelService.RestartPanel(time.Second * 3)
jsonMsg(c, I18nWeb(c, "pages.settings.restartPanel"), err) jsonMsg(c, I18n(c, "pages.setting.restartPanel"), err)
}
func (a *SettingController) updateSecret(c *gin.Context) {
form := &updateSecretForm{}
err := c.ShouldBind(form)
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
}
user := session.GetLoginUser(c)
err = a.userService.UpdateUserSecret(user.Id, form.LoginSecret)
if err == nil {
user.LoginSecret = form.LoginSecret
session.SetLoginUser(c, user)
}
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err)
}
func (a *SettingController) getUserSecret(c *gin.Context) {
loginUser := session.GetLoginUser(c)
user := a.userService.GetUserSecret(loginUser.Id)
if user != nil {
jsonObj(c, user, nil)
}
} }

View File

@@ -1,16 +1,24 @@
package controller package controller
import ( import (
"github.com/gin-gonic/gin"
"net" "net"
"net/http" "net/http"
"strings" "strings"
"x-ui/config" "x-ui/config"
"x-ui/logger" "x-ui/logger"
"x-ui/web/entity" "x-ui/web/entity"
"github.com/gin-gonic/gin"
) )
func getUriId(c *gin.Context) int64 {
s := struct {
Id int64 `uri:"id"`
}{}
_ = c.BindUri(&s)
return s.Id
}
func getRemoteIp(c *gin.Context) string { func getRemoteIp(c *gin.Context) string {
value := c.GetHeader("X-Forwarded-For") value := c.GetHeader("X-Forwarded-For")
if value != "" { if value != "" {
@@ -38,12 +46,12 @@ func jsonMsgObj(c *gin.Context, msg string, obj interface{}, err error) {
if err == nil { if err == nil {
m.Success = true m.Success = true
if msg != "" { if msg != "" {
m.Msg = msg + I18nWeb(c, "success") m.Msg = msg + I18n(c, "success")
} }
} else { } else {
m.Success = false m.Success = false
m.Msg = msg + I18nWeb(c, "fail") + ": " + err.Error() m.Msg = msg + I18n(c, "fail") + ": " + err.Error()
logger.Warning(msg+I18nWeb(c, "fail")+": ", err) logger.Warning(msg+I18n(c, "fail")+": ", err)
} }
c.JSON(http.StatusOK, m) c.JSON(http.StatusOK, m)
} }
@@ -67,7 +75,6 @@ func html(c *gin.Context, name string, title string, data gin.H) {
data = gin.H{} data = gin.H{}
} }
data["title"] = title data["title"] = title
data["host"] = strings.Split(c.Request.Host, ":")[0]
data["request_uri"] = c.Request.RequestURI data["request_uri"] = c.Request.RequestURI
data["base_path"] = c.GetString("base_path") data["base_path"] = c.GetString("base_path")
c.HTML(http.StatusOK, name, getContext(data)) c.HTML(http.StatusOK, name, getContext(data))
@@ -77,8 +84,10 @@ func getContext(h gin.H) gin.H {
a := gin.H{ a := gin.H{
"cur_ver": config.GetVersion(), "cur_ver": config.GetVersion(),
} }
for key, value := range h { if h != nil {
a[key] = value for key, value := range h {
a[key] = value
}
} }
return a return a
} }

View File

@@ -18,12 +18,12 @@ func NewXUIController(g *gin.RouterGroup) *XUIController {
} }
func (a *XUIController) initRouter(g *gin.RouterGroup) { func (a *XUIController) initRouter(g *gin.RouterGroup) {
g = g.Group("/panel") g = g.Group("/xui")
g.Use(a.checkLogin) g.Use(a.checkLogin)
g.GET("/", a.index) g.GET("/", a.index)
g.GET("/inbounds", a.inbounds) g.GET("/inbounds", a.inbounds)
g.GET("/settings", a.settings) g.GET("/setting", a.setting)
a.inboundController = NewInboundController(g) a.inboundController = NewInboundController(g)
a.settingController = NewSettingController(g) a.settingController = NewSettingController(g)
@@ -37,6 +37,6 @@ func (a *XUIController) inbounds(c *gin.Context) {
html(c, "inbounds.html", "pages.inbounds.title", nil) html(c, "inbounds.html", "pages.inbounds.title", nil)
} }
func (a *XUIController) settings(c *gin.Context) { func (a *XUIController) setting(c *gin.Context) {
html(c, "settings.html", "pages.settings.title", nil) html(c, "setting.html", "pages.setting.title", nil)
} }

View File

@@ -28,33 +28,17 @@ type Pager struct {
type AllSetting struct { type AllSetting struct {
WebListen string `json:"webListen" form:"webListen"` WebListen string `json:"webListen" form:"webListen"`
WebDomain string `json:"webDomain" form:"webDomain"`
WebPort int `json:"webPort" form:"webPort"` WebPort int `json:"webPort" form:"webPort"`
WebCertFile string `json:"webCertFile" form:"webCertFile"` WebCertFile string `json:"webCertFile" form:"webCertFile"`
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"` WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
WebBasePath string `json:"webBasePath" form:"webBasePath"` WebBasePath string `json:"webBasePath" form:"webBasePath"`
SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge"`
ExpireDiff int `json:"expireDiff" form:"expireDiff"`
TrafficDiff int `json:"trafficDiff" form:"trafficDiff"`
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"` TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
TgBotToken string `json:"tgBotToken" form:"tgBotToken"` TgBotToken string `json:"tgBotToken" form:"tgBotToken"`
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"` TgBotChatId int `json:"tgBotChatId" form:"tgBotChatId"`
TgRunTime string `json:"tgRunTime" form:"tgRunTime"` TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"`
TgBotLoginNotify bool `json:"tgBotLoginNotify" form:"tgBotLoginNotify"`
TgCpu int `json:"tgCpu" form:"tgCpu"`
TgLang string `json:"tgLang" form:"tgLang"`
XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"` XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`
TimeLocation string `json:"timeLocation" form:"timeLocation"`
SecretEnable bool `json:"secretEnable" form:"secretEnable"` TimeLocation string `json:"timeLocation" form:"timeLocation"`
SubEnable bool `json:"subEnable" form:"subEnable"`
SubListen string `json:"subListen" form:"subListen"`
SubPort int `json:"subPort" form:"subPort"`
SubPath string `json:"subPath" form:"subPath"`
SubDomain string `json:"subDomain" form:"subDomain"`
SubCertFile string `json:"subCertFile" form:"subCertFile"`
SubKeyFile string `json:"subKeyFile" form:"subKeyFile"`
SubUpdates int `json:"subUpdates" form:"subUpdates"`
} }
func (s *AllSetting) CheckValid() error { func (s *AllSetting) CheckValid() error {
@@ -65,25 +49,10 @@ func (s *AllSetting) CheckValid() error {
} }
} }
if s.SubListen != "" {
ip := net.ParseIP(s.SubListen)
if ip == nil {
return common.NewError("Sub listen is not valid ip:", s.SubListen)
}
}
if s.WebPort <= 0 || s.WebPort > 65535 { if s.WebPort <= 0 || s.WebPort > 65535 {
return common.NewError("web port is not a valid port:", s.WebPort) return common.NewError("web port is not a valid port:", s.WebPort)
} }
if s.SubPort <= 0 || s.SubPort > 65535 {
return common.NewError("Sub port is not a valid port:", s.SubPort)
}
if s.SubPort == s.WebPort {
return common.NewError("Sub and Web could not use same port:", s.SubPort)
}
if s.WebCertFile != "" || s.WebKeyFile != "" { if s.WebCertFile != "" || s.WebKeyFile != "" {
_, err := tls.LoadX509KeyPair(s.WebCertFile, s.WebKeyFile) _, err := tls.LoadX509KeyPair(s.WebCertFile, s.WebKeyFile)
if err != nil { if err != nil {
@@ -91,13 +60,6 @@ func (s *AllSetting) CheckValid() error {
} }
} }
if s.SubCertFile != "" || s.SubKeyFile != "" {
_, err := tls.LoadX509KeyPair(s.SubCertFile, s.SubKeyFile)
if err != nil {
return common.NewErrorf("cert file <%v> or key file <%v> invalid: %v", s.SubCertFile, s.SubKeyFile, err)
}
}
if !strings.HasPrefix(s.WebBasePath, "/") { if !strings.HasPrefix(s.WebBasePath, "/") {
s.WebBasePath = "/" + s.WebBasePath s.WebBasePath = "/" + s.WebBasePath
} }

View File

@@ -2,23 +2,17 @@ package global
import ( import (
"context" "context"
_ "unsafe"
"github.com/robfig/cron/v3" "github.com/robfig/cron/v3"
_ "unsafe"
) )
var webServer WebServer var webServer WebServer
var subServer SubServer
type WebServer interface { type WebServer interface {
GetCron() *cron.Cron GetCron() *cron.Cron
GetCtx() context.Context GetCtx() context.Context
} }
type SubServer interface {
GetCtx() context.Context
}
func SetWebServer(s WebServer) { func SetWebServer(s WebServer) {
webServer = s webServer = s
} }
@@ -26,11 +20,3 @@ func SetWebServer(s WebServer) {
func GetWebServer() WebServer { func GetWebServer() WebServer {
return webServer return webServer
} }
func SetSubServer(s SubServer) {
subServer = s
}
func GetSubServer() SubServer {
return subServer
}

View File

@@ -1,80 +0,0 @@
package global
import (
"crypto/md5"
"encoding/hex"
"regexp"
"sync"
"time"
)
type HashEntry struct {
Hash string
Value string
Timestamp time.Time
}
type HashStorage struct {
sync.RWMutex
Data map[string]HashEntry
Expiration time.Duration
}
func NewHashStorage(expiration time.Duration) *HashStorage {
return &HashStorage{
Data: make(map[string]HashEntry),
Expiration: expiration,
}
}
func (h *HashStorage) SaveHash(query string) string {
h.Lock()
defer h.Unlock()
md5Hash := md5.Sum([]byte(query))
md5HashString := hex.EncodeToString(md5Hash[:])
entry := HashEntry{
Hash: md5HashString,
Value: query,
Timestamp: time.Now(),
}
h.Data[md5HashString] = entry
return md5HashString
}
func (h *HashStorage) GetValue(hash string) (string, bool) {
h.RLock()
defer h.RUnlock()
entry, exists := h.Data[hash]
return entry.Value, exists
}
func (h *HashStorage) IsMD5(hash string) bool {
match, _ := regexp.MatchString("^[a-f0-9]{32}$", hash)
return match
}
func (h *HashStorage) RemoveExpiredHashes() {
h.Lock()
defer h.Unlock()
now := time.Now()
for hash, entry := range h.Data {
if now.Sub(entry.Timestamp) > h.Expiration {
delete(h.Data, hash)
}
}
}
func (h *HashStorage) Reset() {
h.Lock()
defer h.Unlock()
h.Data = make(map[string]HashEntry)
}

View File

@@ -7,13 +7,11 @@
<link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue@1.7.2/antd.min.css"> <link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue@1.7.2/antd.min.css">
<link rel="stylesheet" href="{{ .base_path }}assets/element-ui@2.15.0/theme-chalk/display.css"> <link rel="stylesheet" href="{{ .base_path }}assets/element-ui@2.15.0/theme-chalk/display.css">
<link rel="stylesheet" href="{{ .base_path }}assets/css/custom.css?{{ .cur_ver }}"> <link rel="stylesheet" href="{{ .base_path }}assets/css/custom.css?{{ .cur_ver }}">
<link rel=”icon” type=”image/x-icon” href="{{ .base_path }}assets/favicon.ico">
<link rel="shortcut icon" type="image/x-icon" href="{{ .base_path }}assets/favicon.ico">
<style> <style>
[v-cloak] { [v-cloak] {
display: none; display: none;
} }
</style> </style>
<title>{{ .host }}-{{ i18n .title}}</title> <title>{{ i18n .title}}</title>
</head> </head>
{{end}} {{end}}

View File

@@ -1,7 +1,7 @@
{{define "promptModal"}} {{define "promptModal"}}
<a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title" <a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title"
:closable="true" @ok="promptModal.ok" :mask-closable="false" :closable="true" @ok="promptModal.ok" :mask-closable="false"
:class="themeSwitcher.darkCardClass" :class="siderDrawer.isDarkTheme ? darkClass : ''"
:ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}'> :ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}'>
<a-input id="prompt-modal-input" :type="promptModal.type" <a-input id="prompt-modal-input" :type="promptModal.type"
v-model="promptModal.value" v-model="promptModal.value"
@@ -36,12 +36,12 @@
}, },
confirm() {}, confirm() {},
open({ open({
title = '', title='',
type = 'text', type='text',
value = '', value='',
okText = '{{ i18n "sure"}}', okText='{{ i18n "sure"}}',
confirm = () => {}, confirm=() => {},
}) { }) {
this.title = title; this.title = title;
this.type = type; this.type = type;
this.value = value; this.value = value;

View File

@@ -1,60 +1,52 @@
{{define "qrcodeModal"}} {{define "qrcodeModal"}}
<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" :closable="true" width="300px" :ok-text="qrModal.okText"
:class="themeSwitcher.darkCardClass" :class="siderDrawer.isDarkTheme ? darkClass : ''"
:footer="null" cancel-text='{{ i18n "close" }}' :ok-button-props="{attrs:{id:'qr-modal-ok-btn'}}">
width="300px"> <canvas id="qrCode" style="width: 100%; height: 100%;"></canvas>
<a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;">
{{ i18n "pages.inbounds.clickOnQRcode" }}
</a-tag>
<template v-if="app.subSettings.enable && qrModal.subId">
<a-divider>Subscription</a-divider>
<canvas @click="copyToClipboard('qrCode-sub',genSubLink(qrModal.client.subId))" id="qrCode-sub" style="width: 100%; height: 100%;"></canvas>
</template>
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
<template v-for="(row, index) in qrModal.qrcodes">
<a-tag color="orange" style="margin-top: 10px;display: block;text-align: center;">[[ row.remark ]]</a-tag>
<canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" style="width: 100%; height: 100%;"></canvas>
</template>
</a-modal> </a-modal>
<script> <script>
const qrModal = { const qrModal = {
title: '', title: '',
clientIndex: 0, content: '',
inbound: new Inbound(), inbound: new Inbound(),
dbInbound: new DBInbound(), dbInbound: new DBInbound(),
client: null, okText: '',
qrcodes: [], copyText: '',
qrcode: null,
clipboard: null, clipboard: null,
visible: false, visible: false,
subId: '', show: function (title='', content='', dbInbound=new DBInbound(),okText='{{ i18n "copy" }}', copyText='') {
show: function (title = '', dbInbound = new DBInbound(), clientIndex = 0) {
this.title = title; this.title = title;
this.clientIndex = clientIndex; this.content = content;
this.dbInbound = dbInbound; this.dbInbound = dbInbound;
this.inbound = dbInbound.toInbound(); this.inbound = dbInbound.toInbound();
settings = JSON.parse(this.inbound.settings); this.okText = okText;
this.client = settings.clients[clientIndex]; if (ObjectUtil.isEmpty(copyText)) {
remark = this.dbInbound.remark + "-" + this.client.email; this.copyText = content;
address = this.dbInbound.address;
this.subId = '';
this.qrcodes = [];
if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) {
this.inbound.stream.tls.settings.domains.forEach((domain) => {
this.qrcodes.push({
remark: remark + "-" + domain.remark,
link: this.inbound.genLink(domain.domain, remark + "-" + domain.remark, clientIndex)
});
});
} else { } else {
this.qrcodes.push({ this.copyText = copyText;
remark: remark,
link: this.inbound.genLink(address, remark, clientIndex)
});
} }
this.visible = true; this.visible = true;
qrModalApp.$nextTick(() => {
if (this.clipboard === null) {
this.clipboard = new ClipboardJS('#qr-modal-ok-btn', {
text: () => this.copyText,
});
this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
}
if (this.qrcode === null) {
this.qrcode = new QRious({
element: document.querySelector('#qrCode'),
size: 260,
value: content,
});
} else {
this.qrcode.value = content;
}
});
}, },
close: function () { close: function () {
this.visible = false; this.visible = false;
@@ -62,42 +54,10 @@
}; };
const qrModalApp = new Vue({ const qrModalApp = new Vue({
delimiters: ['[[', ']]'],
el: '#qrcode-modal', el: '#qrcode-modal',
data: { data: {
qrModal: qrModal, qrModal: qrModal,
}, },
methods: {
copyToClipboard(elmentId, content) {
this.qrModal.clipboard = new ClipboardJS('#' + elmentId, {
text: () => content,
});
this.qrModal.clipboard.on('success', () => {
app.$message.success('{{ i18n "copied" }}')
this.qrModal.clipboard.destroy();
});
},
setQrCode(elmentId, content) {
new QRious({
element: document.querySelector('#' + elmentId),
size: 260,
value: content,
});
},
genSubLink(subID) {
const { domain: host, port, tls: isTLS, path: base } = app.subSettings;
return buildURL({ host, port, isTLS, base, path: subID });
}
},
updated() {
if (qrModal.client && qrModal.client.subId) {
qrModal.subId = qrModal.client.subId;
this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId));
}
qrModal.qrcodes.forEach((element, index) => {
this.setQrCode("qrCode-" + index, element.link);
});
}
}); });
</script> </script>

View File

@@ -1,11 +1,10 @@
{{define "textModal"}} {{define "textModal"}}
<a-modal id="text-modal" v-model="txtModal.visible" :title="txtModal.title" <a-modal id="text-modal" v-model="txtModal.visible" :title="txtModal.title"
:closable="true" ok-text='{{ i18n "copy" }}' cancel-text='{{ i18n "close" }}' :closable="true" ok-text='{{ i18n "copy" }}' cancel-text='{{ i18n "close" }}'
:class="themeSwitcher.darkCardClass" :class="siderDrawer.isDarkTheme ? darkClass : ''"
:ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}"> :ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}">
<a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;" <a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;"
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)" :href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)" :download="txtModal.fileName">
:download="txtModal.fileName">
{{ i18n "download" }} [[ txtModal.fileName ]] {{ i18n "download" }} [[ txtModal.fileName ]]
</a-button> </a-button>
<a-input type="textarea" v-model="txtModal.content" <a-input type="textarea" v-model="txtModal.content"
@@ -21,7 +20,7 @@
qrcode: null, qrcode: null,
clipboard: null, clipboard: null,
visible: false, visible: false,
show: function (title = '', content = '', fileName = '') { show: function (title='', content='', fileName='') {
this.title = title; this.title = title;
this.content = content; this.content = content;
this.fileName = fileName; this.fileName = fileName;
@@ -33,6 +32,7 @@
}); });
this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}')); this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
} }
}); });
}, },
close: function () { close: function () {
@@ -41,7 +41,7 @@
}; };
const textModalApp = new Vue({ const textModalApp = new Vue({
delimiters: ['[[', ']]'], delimiters: ['[[', ']]'],
el: '#text-modal', el: '#text-modal',
data: { data: {
txtModal: txtModal, txtModal: txtModal,

View File

@@ -18,12 +18,6 @@
border-radius: 30px; border-radius: 30px;
} }
.ant-input-group-addon {
border-radius: 0 30px 30px 0;
width: 50px;
font-size: 18px;
}
.ant-input-affix-wrapper .ant-input-prefix { .ant-input-affix-wrapper .ant-input-prefix {
left: 23px; left: 23px;
} }
@@ -32,26 +26,20 @@
padding-left: 50px; padding-left: 50px;
} }
.centered { .selectLang{
display: flex; display: flex;
text-align: center; text-align: center;
align-items: center;
justify-content: center; justify-content: center;
} }
.title {
font-size: 32px;
font-weight: bold;
}
</style> </style>
<body> <body>
<a-layout id="app" v-cloak class="login" :class="themeSwitcher.darkCardClass"> <a-layout id="app" v-cloak>
<transition name="list" appear> <transition name="list" appear>
<a-layout-content> <a-layout-content>
<a-row type="flex" justify="center"> <a-row type="flex" justify="center">
<a-col :xs="22" :sm="20" :md="16" :lg="12" :xl="8"> <a-col :xs="22" :sm="20" :md="16" :lg="12" :xl="8">
<h1 class="title">{{ i18n "pages.login.title" }}</h1> <h1>3x-ui {{ i18n "pages.login.title" }}</h1>
</a-col> </a-col>
</a-row> </a-row>
<a-row type="flex" justify="center"> <a-row type="flex" justify="center">
@@ -60,44 +48,40 @@
<a-form-item> <a-form-item>
<a-input v-model.trim="user.username" placeholder='{{ i18n "username" }}' <a-input v-model.trim="user.username" placeholder='{{ i18n "username" }}'
@keydown.enter.native="login" autofocus> @keydown.enter.native="login" autofocus>
<a-icon slot="prefix" type="user" :style="'font-size: 16px;' + themeSwitcher.textStyle" /> <a-icon slot="prefix" type="user" style="color: rgba(0,0,0,.25)"/>
</a-input> </a-input>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<password-input icon="lock" v-model.trim="user.password" <a-input type="password" v-model.trim="user.password"
placeholder='{{ i18n "password" }}' @keydown.enter.native="login"> placeholder='{{ i18n "password" }}' @keydown.enter.native="login">
</password-input> <a-icon slot="prefix" type="lock" style="color: rgba(0,0,0,.25)"/>
</a-form-item> </a-input>
<a-form-item v-if="secretEnable">
<password-input icon="key" v-model.trim="user.loginSecret"
placeholder='{{ i18n "secretToken" }}' @keydown.enter.native="login">
</password-input>
</a-input>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<a-row justify="center" class="centered"> <a-button block @click="login" :loading="loading">{{ i18n "login" }}</a-button>
<a-button type="primary" :loading="loading" @click="login" :icon="loading ? 'poweroff' : undefined"
:style="loading ? { width: '50px' } : { display: 'block', width: '100%' }">
[[ loading ? '' : '{{ i18n "login" }}' ]]
</a-button>
</a-row>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<a-row justify="center" class="centered">
<a-col :span="12"> <a-row justify="center" class="selectLang">
<a-select ref="selectLang" v-model="lang" @change="setLang(lang)" :dropdown-class-name="themeSwitcher.darkCardClass"> <a-col :span="4"><span>Language : </span></a-col>
<a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
<span role="img" :aria-label="l.name" v-text="l.icon"></span> <a-col :span="6">
<a-select
ref="selectLang"
v-model="lang"
@change="setLang(lang)"
>
<a-select-option :value="l.value" label="China" v-for="l in supportLangs" >
<span role="img" aria-label="l.name" v-text="l.icon"></span>
&nbsp;&nbsp;<span v-text="l.name"></span> &nbsp;&nbsp;<span v-text="l.name"></span>
</a-select-option> </a-select-option>
</a-select> </a-select>
</a-col> </a-col>
</a-row> </a-row>
</a-form-item>
<a-form-item>
<a-row justify="center" class="centered">
<theme-switch />
</a-row>
</a-form-item> </a-form-item>
</a-form> </a-form>
</a-col> </a-col>
@@ -106,24 +90,22 @@
</transition> </transition>
</a-layout> </a-layout>
{{template "js" .}} {{template "js" .}}
{{template "component/themeSwitcher" .}}
{{template "component/password" .}}
<script> <script>
const leftColor = RandomUtil.randomIntRange(0x222222, 0xFFFFFF / 2).toString(16);
const rightColor = RandomUtil.randomIntRange(0xFFFFFF / 2, 0xDDDDDD).toString(16);
const deg = RandomUtil.randomIntRange(0, 360);
const background = `linear-gradient(${deg}deg, #${leftColor} 10%, #${rightColor} 100%)`;
document.querySelector('#app').style.background = background;
const app = new Vue({ const app = new Vue({
delimiters: ['[[', ']]'], delimiters: ['[[', ']]'],
el: '#app', el: '#app',
data: { data: {
themeSwitcher,
loading: false, loading: false,
user: new User(), user: new User(),
secretEnable: false, lang : ""
lang: ""
}, },
async created() { created(){
this.updateBackground(); this.lang = getLang();
this.lang = getLang();
this.secretEnable = await this.getSecretStatus();
}, },
methods: { methods: {
async login() { async login() {
@@ -131,33 +113,11 @@
const msg = await HttpUtil.post('/login', this.user); const msg = await HttpUtil.post('/login', this.user);
this.loading = false; this.loading = false;
if (msg.success) { if (msg.success) {
location.href = basePath + 'panel/'; location.href = basePath + 'xui/';
} }
}, }
async getSecretStatus() { }
this.loading = true;
const msg = await HttpUtil.post('/getSecretStatus');
this.loading = false;
if (msg.success) {
this.secretEnable = msg.obj;
return msg.obj;
}
},
updateBackground() {
const leftColor = RandomUtil.randomIntRange(0x222222, 0xFFFFFF / 2).toString(16);
const rightColor = RandomUtil.randomIntRange(0xFFFFFF / 2, 0xDDDDDD).toString(16);
const deg = RandomUtil.randomIntRange(0, 360);
const background = `linear-gradient(${deg}deg, #${leftColor} 10%, #${rightColor} 100%)`;
document.querySelector('#app').style.background = this.themeSwitcher.isDarkTheme ? colors.dark.bg : background;
},
},
watch: {
'themeSwitcher.isDarkTheme'(newVal, oldVal) {
this.updateBackground();
},
},
}); });
</script> </script>
</body> </body>
</html> </html>

View File

@@ -1,248 +0,0 @@
{{define "clientsBulkModal"}}
<a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible" :title="clientsBulkModal.title" @ok="clientsBulkModal.ok"
:confirm-loading="clientsBulkModal.confirmLoading" :closable="true" :mask-closable="false"
:class="themeSwitcher.darkCardClass"
:ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}'>
<a-form layout="inline">
<a-form-item label='{{ i18n "pages.client.method" }}'>
<a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 350px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option :value="0">Random</a-select-option>
<a-select-option :value="1">Random+Prefix</a-select-option>
<a-select-option :value="2">Random+Prefix+Num</a-select-option>
<a-select-option :value="3">Random+Prefix+Num+Postfix</a-select-option>
<a-select-option :value="4">Prefix+Num+Postfix [ BE CAREFUL! ]</a-select-option>
</a-select>
</a-form-item><br />
<a-form-item v-if="clientsBulkModal.emailMethod>1">
<span slot="label">{{ i18n "pages.client.first" }}</span>
<a-input-number v-model="clientsBulkModal.firstNum" :min="1"></a-input-number>
</a-form-item>
<a-form-item v-if="clientsBulkModal.emailMethod>1">
<span slot="label">{{ i18n "pages.client.last" }}</span>
<a-input-number v-model="clientsBulkModal.lastNum" :min="clientsBulkModal.firstNum"></a-input-number>
</a-form-item>
<a-form-item v-if="clientsBulkModal.emailMethod>0">
<span slot="label">{{ i18n "pages.client.prefix" }}</span>
<a-input v-model="clientsBulkModal.emailPrefix" style="width: 120px"></a-input>
</a-form-item>
<a-form-item v-if="clientsBulkModal.emailMethod>2">
<span slot="label">{{ i18n "pages.client.postfix" }}</span>
<a-input v-model="clientsBulkModal.emailPostfix" style="width: 120px"></a-input>
</a-form-item>
<a-form-item v-if="clientsBulkModal.emailMethod < 2">
<span slot="label">{{ i18n "pages.client.clientCount" }}</span>
<a-input-number v-model="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number>
</a-form-item>
<a-form-item v-if="app.subSettings.enable">
<span slot="label">
Subscription
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="clientsBulkModal.subId"></a-input>
</a-form-item>
<a-form-item v-if="app.tgBotEnable">
<span slot="label">
Telegram ID
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="clientsBulkModal.tgId"></a-input>
</a-form-item>
<br>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.IPLimit" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="clientsBulkModal.limitIp" min="0"></a-input-number>
</a-form-item>
<br>
<a-form-item v-if="clientsBulkModal.inbound.xtls" label="Flow">
<a-select v-model="clientsBulkModal.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="">{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-if="clientsBulkModal.inbound.canEnableTlsFlow()" label="Flow" layout="inline">
<a-select v-model="clientsBulkModal.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in TLS_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="clientsBulkModal.totalGB" :min="0"></a-input-number>
</a-form-item>
<br>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="clientsBulkModal.delayedStart" @click="clientsBulkModal.expiryTime=0"></a-switch>
</a-form-item>
<br>
<a-form-item label='{{ i18n "pages.client.expireDays" }}' v-if="clientsBulkModal.delayedStart">
<a-input-number v-model="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
<a-form-item v-else>
<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:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.darkCardClass"
v-model="clientsBulkModal.expiryTime" style="width: 300px;"></a-date-picker>
</a-form-item>
</a-form>
</a-modal>
<script>
const clientsBulkModal = {
visible: false,
confirmLoading: false,
title: '',
okText: '',
confirm: null,
dbInbound: new DBInbound(),
inbound: new Inbound(),
quantity: 1,
totalGB: 0,
limitIp: 0,
expiryTime: '',
emailMethod: 0,
firstNum: 1,
lastNum: 1,
emailPrefix: "",
emailPostfix: "",
subId: "",
tgId: "",
flow: "",
delayedStart: false,
ok() {
clients = [];
method = clientsBulkModal.emailMethod;
if (method > 1) {
start = clientsBulkModal.firstNum;
end = clientsBulkModal.lastNum + 1;
} else {
start = 0;
end = clientsBulkModal.quantity;
}
prefix = (method > 0 && clientsBulkModal.emailPrefix.length > 0) ? clientsBulkModal.emailPrefix : "";
useNum = (method > 1);
postfix = (method > 2 && clientsBulkModal.emailPostfix.length > 0) ? clientsBulkModal.emailPostfix : "";
for (let i = start; i < end; i++) {
newClient = clientsBulkModal.newClient(clientsBulkModal.dbInbound.protocol);
if (method == 4) newClient.email = "";
newClient.email += useNum ? prefix + i.toString() + postfix : prefix + postfix;
if (clientsBulkModal.subId.length > 0) newClient.subId = clientsBulkModal.subId;
if (clientsBulkModal.tgId.length > 0) newClient.tgId = clientsBulkModal.tgId;
newClient.limitIp = clientsBulkModal.limitIp;
newClient._totalGB = clientsBulkModal.totalGB;
newClient._expiryTime = clientsBulkModal.expiryTime;
if (clientsBulkModal.inbound.canEnableTlsFlow()) {
newClient.flow = clientsBulkModal.flow;
}
if (clientsBulkModal.inbound.xtls) {
newClient.flow = clientsBulkModal.flow;
}
clients.push(newClient);
}
ObjectUtil.execute(clientsBulkModal.confirm, clients, clientsBulkModal.dbInbound.id);
},
show({
title = '',
okText = '{{ i18n "sure" }}',
dbInbound = null,
confirm = (inbound, dbInbound) => { }
}) {
this.visible = true;
this.title = title;
this.okText = okText;
this.confirm = confirm;
this.quantity = 1;
this.totalGB = 0;
this.expiryTime = 0;
this.emailMethod = 0;
this.limitIp = 0;
this.firstNum = 1;
this.lastNum = 1;
this.emailPrefix = "";
this.emailPostfix = "";
this.subId = "";
this.tgId = "";
this.flow = "";
this.dbInbound = new DBInbound(dbInbound);
this.inbound = dbInbound.toInbound();
this.delayedStart = false;
},
getClients(protocol, clientSettings) {
switch (protocol) {
case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans;
case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
default: return null;
}
},
newClient(protocol) {
switch (protocol) {
case Protocols.VMESS: return new Inbound.VmessSettings.Vmess();
case Protocols.VLESS: return new Inbound.VLESSSettings.VLESS();
case Protocols.TROJAN: return new Inbound.TrojanSettings.Trojan();
case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings.Shadowsocks();
default: return null;
}
},
close() {
clientsBulkModal.visible = false;
clientsBulkModal.loading(false);
},
loading(loading) {
clientsBulkModal.confirmLoading = loading;
},
};
const clientsBulkModalApp = new Vue({
delimiters: ['[[', ']]'],
el: '#client-bulk-modal',
data: {
clientsBulkModal,
get inbound() {
return this.clientsBulkModal.inbound;
},
get delayedExpireDays() {
return this.clientsBulkModal.expiryTime < 0 ? this.clientsBulkModal.expiryTime / -86400000 : 0;
},
set delayedExpireDays(days) {
this.clientsBulkModal.expiryTime = -86400000 * days;
},
},
});
</script>
{{end}}

View File

@@ -1,172 +0,0 @@
{{define "clientsModal"}}
<a-modal id="client-modal" v-model="clientModal.visible" :title="clientModal.title" @ok="clientModal.ok"
:confirm-loading="clientModal.confirmLoading" :closable="true" :mask-closable="false"
:class="themeSwitcher.darkCardClass"
:ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'>
{{template "form/client"}}
</a-modal>
<script>
const clientModal = {
visible: false,
confirmLoading: false,
title: '',
okText: '',
isEdit: false,
dbInbound: new DBInbound(),
inbound: new Inbound(),
clients: [],
clientStats: [],
oldClientId: "",
index: null,
clientIps: null,
delayedStart: false,
ok() {
if (clientModal.isEdit) {
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.oldClientId);
} else {
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id);
}
},
show({ title = '', okText = '{{ i18n "sure" }}', index = null, dbInbound = null, confirm = () => { }, isEdit = false }) {
this.visible = true;
this.title = title;
this.okText = okText;
this.isEdit = isEdit;
this.dbInbound = new DBInbound(dbInbound);
this.inbound = dbInbound.toInbound();
this.clients = this.getClients(this.inbound.protocol, this.inbound.settings);
this.index = index === null ? this.clients.length : index;
this.delayedStart = false;
if (isEdit) {
if (this.clients[index].expiryTime < 0) {
this.delayedStart = true;
}
this.oldClientId = this.getClientId(dbInbound.protocol, clients[index]);
} else {
this.addClient(this.inbound.protocol, this.clients);
}
this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
this.confirm = confirm;
},
getClients(protocol, clientSettings) {
switch (protocol) {
case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans;
case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
default: return null;
}
},
getClientId(protocol, client) {
switch (protocol) {
case Protocols.TROJAN: return client.password;
case Protocols.SHADOWSOCKS: return client.email;
default: return client.id;
}
},
addClient(protocol, clients) {
switch (protocol) {
case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.Vmess());
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
case Protocols.SHADOWSOCKS: return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks());
default: return null;
}
},
close() {
clientModal.visible = false;
clientModal.loading(false);
},
loading(loading) {
clientModal.confirmLoading = loading;
},
};
const clientModalApp = new Vue({
delimiters: ['[[', ']]'],
el: '#client-modal',
data: {
clientModal,
get inbound() {
return this.clientModal.inbound;
},
get client() {
return this.clientModal.clients[this.clientModal.index];
},
get clientStats() {
return this.clientModal.clientStats;
},
get isEdit() {
return this.clientModal.isEdit;
},
get isTrafficExhausted() {
if (!clientStats) return false
if (clientStats.total <= 0) return false
if (clientStats.up + clientStats.down < clientStats.total) return false
return true
},
get isExpiry() {
return this.clientModal.isEdit && this.client.expiryTime >0 ? (this.client.expiryTime < new Date().getTime()) : false;
},
get statsColor() {
return usageColor(clientStats.up + clientStats.down, app.trafficDiff, this.client.totalGB);
},
get delayedExpireDays() {
return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
},
set delayedExpireDays(days) {
this.client.expiryTime = -86400000 * days;
},
},
methods: {
async getDBClientIps(email) {
const msg = await HttpUtil.post(`/panel/inbound/clientIps/${email}`);
if (!msg.success) {
document.getElementById("clientIPs").value = msg.obj;
return;
}
let ips = msg.obj;
if (typeof ips === 'string' && ips.startsWith('[') && ips.endsWith(']')) {
try {
ips = JSON.parse(ips);
ips = Array.isArray(ips) ? ips.join("\n") : ips;
} catch (e) {
console.error('Error parsing JSON:', e);
}
}
document.getElementById("clientIPs").value = ips;
},
async clearDBClientIps(email) {
try {
const msg = await HttpUtil.post(`/panel/inbound/clearClientIps/${email}`);
if (!msg.success) {
return;
}
document.getElementById("clientIPs").value = "";
} catch (error) {
}
},
resetClientTraffic(email, dbInboundId, iconElement) {
this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
class: themeSwitcher.darkCardClass,
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: async () => {
iconElement.disabled = true;
const msg = await HttpUtil.postWithModal('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + email);
if (msg.success) {
this.clientModal.clientStats.up = 0;
this.clientModal.clientStats.down = 0;
}
iconElement.disabled = false;
},
})
},
},
});
</script>
{{end}}

View File

@@ -1,20 +1,34 @@
{{define "menuItems"}} {{define "menuItems"}}
<a-menu-item key="{{ .base_path }}panel/"> <a-menu-item key="{{ .base_path }}xui/">
<a-icon type="dashboard"></a-icon> <a-icon type="dashboard"></a-icon>
<span>{{ i18n "menu.dashboard"}}</span> <span>{{ i18n "menu.dashboard"}}</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="{{ .base_path }}panel/inbounds"> <a-menu-item key="{{ .base_path }}xui/inbounds">
<a-icon type="user"></a-icon> <a-icon type="user"></a-icon>
<span>{{ i18n "menu.inbounds"}}</span> <span>{{ i18n "menu.inbounds"}}</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="{{ .base_path }}panel/settings"> <a-menu-item key="{{ .base_path }}xui/setting">
<a-icon type="setting"></a-icon> <a-icon type="setting"></a-icon>
<span>{{ i18n "menu.settings"}}</span> <span>{{ i18n "menu.setting"}}</span>
</a-menu-item> </a-menu-item>
<!--<a-menu-item key="{{ .base_path }}panel/clients">--> <!--<a-menu-item key="{{ .base_path }}xui/clients">-->
<!-- <a-icon type="laptop"></a-icon>--> <!-- <a-icon type="laptop"></a-icon>-->
<!-- <span>Client</span>--> <!-- <span>client</span>-->
<!--</a-menu-item>--> <!--</a-menu-item>-->
<a-sub-menu>
<template slot="title">
<a-icon type="link"></a-icon>
<span>{{ i18n "menu.link"}}</span>
</template>
<a-menu-item key="https://github.com/mhsanaei/3x-ui/">
<a-icon type="github"></a-icon>
<span>Github</span>
</a-menu-item>
<a-menu-item key="https://t.me/panel3xui">
<a-icon type="usergroup-add"></a-icon>
<span>Telegram</span>
</a-menu-item>
</a-sub-menu>
<a-menu-item key="{{ .base_path }}logout"> <a-menu-item key="{{ .base_path }}logout">
<a-icon type="logout"></a-icon> <a-icon type="logout"></a-icon>
<span>{{ i18n "menu.logout"}}</span> <span>{{ i18n "menu.logout"}}</span>
@@ -23,14 +37,17 @@
{{define "commonSider"}} {{define "commonSider"}}
<a-layout-sider :theme="themeSwitcher.currentTheme" id="sider" collapsible breakpoint="md" collapsed-width="0"> <a-layout-sider :theme="siderDrawer.theme" id="sider" collapsible breakpoint="md" collapsed-width="0">
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" selected-keys=""> <a-menu :theme="siderDrawer.theme" mode="inline" selected-keys="">
<a-menu-item mode="inline"> <a-menu-item mode="inline">
<a-icon type="bg-colors"></a-icon> <a-icon type="bg-colors"></a-icon>
<theme-switch /> <a-switch size="small" :default-checked="siderDrawer.isDarkTheme"
checked-children="☀"
un-checked-children="🌙"
@change="siderDrawer.changeTheme()"></a-switch>
</a-menu-item> </a-menu-item>
</a-menu> </a-menu>
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="['{{ .request_uri }}']" <a-menu :theme="siderDrawer.theme" mode="inline" :selected-keys="['{{ .request_uri }}']"
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key"> @click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
{{template "menuItems" .}} {{template "menuItems" .}}
</a-menu> </a-menu>
@@ -38,25 +55,33 @@
<a-drawer id="sider-drawer" placement="left" :closable="false" <a-drawer id="sider-drawer" placement="left" :closable="false"
@close="siderDrawer.close()" @close="siderDrawer.close()"
:visible="siderDrawer.visible" :visible="siderDrawer.visible"
:wrap-class-name="themeSwitcher.darkDrawerClass"
:wrap-style="{ padding: 0 }"> :wrap-style="{ padding: 0 }">
<div class="drawer-handle" @click="siderDrawer.change()" slot="handle"> <div class="drawer-handle" @click="siderDrawer.change()" slot="handle">
<a-icon :type="siderDrawer.visible ? 'close' : 'menu-fold'"></a-icon> <a-icon :type="siderDrawer.visible ? 'close' : 'menu-fold'"></a-icon>
</div> </div>
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" selected-keys=""> <a-menu mode="inline" selected-keys="">
<a-menu-item mode="inline"> <a-menu-item mode="inline">
<a-icon type="bg-colors"></a-icon> <a-icon type="bg-colors"></a-icon>
<theme-switch /> <a-switch :default-checked="siderDrawer.isDarkTheme"
checked-children="☀"
un-checked-children="🌙"
@change="siderDrawer.changeTheme()"></a-switch>
</a-menu-item> </a-menu-item>
</a-menu> </a-menu>
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="['{{ .request_uri }}']" <a-menu mode="inline" :selected-keys="['{{ .request_uri }}']"
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key"> @click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
{{template "menuItems" .}} {{template "menuItems" .}}
</a-menu> </a-menu>
</a-drawer> </a-drawer>
<script> <script>
const darkClass = "ant-card-dark";
const bgDarkStyle = "background-color: #242c3a";
const siderDrawer = { const siderDrawer = {
visible: false, visible: false,
collapsed: false,
isDarkTheme: localStorage.getItem("dark-mode") === 'true' ? true : false,
show() { show() {
this.visible = true; this.visible = true;
}, },
@@ -65,7 +90,18 @@
}, },
change() { change() {
this.visible = !this.visible; this.visible = !this.visible;
},
toggleCollapsed() {
this.collapsed = !this.collapsed;
}, },
changeTheme() {
this.isDarkTheme = ! this.isDarkTheme;
localStorage.setItem("dark-mode", this.isDarkTheme);
},
get theme() {
return this.isDarkTheme ? 'dark' : 'light';
}
}; };
</script> </script>
{{end}} {{end}}

View File

@@ -1,35 +0,0 @@
{{define "component/passwordInput"}}
<template>
<a-input :value="value" :type="showPassword ? 'text' : 'password'"
:placeholder="placeholder"
@input="$emit('input', $event.target.value)">
<template v-if="icon" #prefix>
<a-icon :type="icon" :style="'font-size: 16px;' + themeSwitcher.textStyle" />
</template>
<template #addonAfter>
<a-icon :type="showPassword ? 'eye-invisible' : 'eye'"
@click="toggleShowPassword"
:style="'font-size: 16px;' + themeSwitcher.textStyle" />
</template>
</a-input>
</template>
{{end}}
{{define "component/password"}}
<script>
Vue.component('password-input', {
props: ["title", "value", "placeholder", "icon"],
template: `{{template "component/passwordInput"}}`,
data() {
return {
showPassword: false,
};
},
methods: {
toggleShowPassword() {
this.showPassword = !this.showPassword;
},
},
});
</script>
{{end}}

View File

@@ -1,13 +1,6 @@
{{define "component/settingListItem"}} {{define "component/settingListItem"}}
<a-list-item style="padding: 20px"> <a-list-item style="padding: 20px">
<a-row v-if="type === 'textarea'"> <a-row>
<a-col>
<a-list-item-meta :title="title" :description="desc"/>
<a-textarea class="ant-setting-textarea" :value="value" @input="$emit('input', $event.target.value)" :auto-size="{ minRows: 10 }"></a-textarea>
<!--a-textarea :value="value" @input="$emit('input', $event.target.value)" :auto-size="{ minRows: 10, maxRows: 30 }"></a-textarea-->
</a-col>
</a-row>
<a-row v-else>
<a-col :lg="24" :xl="12"> <a-col :lg="24" :xl="12">
<a-list-item-meta :title="title" :description="desc"/> <a-list-item-meta :title="title" :description="desc"/>
</a-col> </a-col>
@@ -16,7 +9,10 @@
<a-input :value="value" @input="$emit('input', $event.target.value)"></a-input> <a-input :value="value" @input="$emit('input', $event.target.value)"></a-input>
</template> </template>
<template v-else-if="type === 'number'"> <template v-else-if="type === 'number'">
<a-input-number :value="value" @change="value => $emit('input', value)" :min="min" style="width: 100%;"></a-input-number> <a-input type="number" :value="value" @input="$emit('input', $event.target.value)"></a-input>
</template>
<template v-else-if="type === 'textarea'">
<a-textarea :value="value" @input="$emit('input', $event.target.value)" :auto-size="{ minRows: 10, maxRows: 10 }"></a-textarea>
</template> </template>
<template v-else-if="type === 'switch'"> <template v-else-if="type === 'switch'">
<a-switch :checked="value" @change="value => $emit('input', value)"></a-switch> <a-switch :checked="value" @change="value => $emit('input', value)"></a-switch>
@@ -29,7 +25,7 @@
{{define "component/setting"}} {{define "component/setting"}}
<script> <script>
Vue.component('setting-list-item', { Vue.component('setting-list-item', {
props: ["type", "title", "desc", "value", "min"], props: ["type", "title", "desc", "value"],
template: `{{template "component/settingListItem"}}`, template: `{{template "component/settingListItem"}}`,
}); });
</script> </script>

View File

@@ -1,58 +0,0 @@
{{define "component/themeSwitchTemplate"}}
<template>
<a-switch :default-checked="themeSwitcher.isDarkTheme"
checked-children="☀"
un-checked-children="🌙"
@change="themeSwitcher.toggleTheme()">
</a-switch>
</template>
{{end}}
{{define "component/themeSwitcher"}}
<script>
const colors = {
dark: {
bg: "#242c3a",
text: "hsla(0,0%,100%,.65)"
},
light: {
bg: '#f0f2f5',
text: "rgba(0, 0, 0, 0.7)",
}
}
function createThemeSwitcher() {
const isDarkTheme = localStorage.getItem('dark-mode') === 'true';
const theme = isDarkTheme ? 'dark' : 'light';
return {
isDarkTheme,
bgStyle: `background: ${colors[theme].bg};`,
textStyle: `color: ${colors[theme].text};`,
darkClass: isDarkTheme ? 'ant-dark' : '',
darkCardClass: isDarkTheme ? 'ant-card-dark' : '',
darkDrawerClass: isDarkTheme ? 'ant-drawer-dark' : '',
get currentTheme() {
return this.isDarkTheme ? 'dark' : 'light';
},
toggleTheme() {
this.isDarkTheme = !this.isDarkTheme;
this.theme = this.isDarkTheme ? 'dark' : 'light';
localStorage.setItem('dark-mode', this.isDarkTheme);
this.bgStyle = `background: ${colors[this.theme].bg};`;
this.textStyle = `color: ${colors[this.theme].text};`;
this.darkClass = this.isDarkTheme ? 'ant-dark' : '';
this.darkCardClass = this.isDarkTheme ? 'ant-card-dark' : '';
this.darkDrawerClass = this.isDarkTheme ? 'ant-drawer-dark' : '';
},
};
}
const themeSwitcher = createThemeSwitcher();
Vue.component('theme-switch', {
props: [],
template: `{{template "component/themeSwitchTemplate"}}`,
data: () => ({ themeSwitcher }),
});
</script>
{{end}}

View File

@@ -1,161 +0,0 @@
{{define "form/client"}}
<a-form layout="inline" v-if="client">
<template v-if="isEdit">
<a-tag v-if="isExpiry || isTrafficExhausted" color="red" style="margin-bottom: 10px;display: block;text-align: center;">
Account is (Expired|Traffic Ended) And Disabled
</a-tag>
</template>
<a-form-item label='{{ i18n "pages.inbounds.enable" }}'>
<a-switch v-model="client.enable"></a-switch>
</a-form-item>
<br>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template>
</a-tooltip>
</span>
<a-icon @click="client.email = RandomUtil.randomLowerAndNum(8)" type="sync"> </a-icon>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item>
<a-form-item label="Password" v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS">
<a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS" @click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
<a-icon v-if="inbound.protocol === Protocols.TROJAN" @click="client.password = RandomUtil.randomSeq(10)" type="sync"> </a-icon>
<a-input v-model.trim="client.password" style="width: 300px;"></a-input>
</a-form-item>
<br>
<a-form-item label="ID" v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS">
<a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon>
<a-input v-model.trim="client.id" style="width: 300px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email && app.subSettings.enable">
<span slot="label">
Subscription
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"> </a-icon>
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email && app.tgBotEnable" >
<span slot="label">
Telegram ID
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="client.tgId"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.IPLimit" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="client.limitIp" min="0"></a-input-number>
</a-form-item>
<a-form-item v-if="client.email && client.limitIp > 0 && isEdit">
<span slot="label">
<span>{{ i18n "pages.inbounds.IPLimitlog" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitlogDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span>
</template>
<span style="color: #FF4D4F">
<a-icon type="delete" @click="clearDBClientIps(client.email)"></a-icon>
</span>
</a-tooltip>
</span>
<a-form layout="block">
<a-textarea id="clientIPs" readonly
@click="getDBClientIps(client.email)"
placeholder="Click To Get IPs"
:auto-size="{ minRows: 5, maxRows: 10 }"
>
</a-textarea>
</a-form>
</a-form-item>
<br>
<a-form-item v-if="inbound.xtls" label="Flow">
<a-select v-model="client.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="">{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-else-if="inbound.canEnableTlsFlow()" label="Flow">
<a-select v-model="client.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in TLS_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="client._totalGB" :min="0"></a-input-number>
<template v-if="isEdit && clientStats">
<br>
<span> {{ i18n "usage" }}:</span>
<a-tag :color="statsColor">
[[ sizeFormat(clientStats.up) ]] /
[[ sizeFormat(clientStats.down) ]]
([[ sizeFormat(clientStats.up + clientStats.down) ]])
</a-tag>
<a-tooltip>
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
<a-icon type="retweet" @click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)"
v-if="client.email.length > 0"></a-icon>
</a-tooltip>
</template>
</a-form-item>
<br>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="clientModal.delayedStart" @click="client._expiryTime=0"></a-switch>
</a-form-item>
<br>
<a-form-item label='{{ i18n "pages.client.expireDays" }}' v-if="clientModal.delayedStart">
<a-input-number v-model="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
<a-form-item v-else>
<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:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.darkCardClass"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
<a-tag color="red" v-if="isExpiry">Expired</a-tag>
</a-form-item>
</a-form>
{{end}}

View File

@@ -1,15 +1,14 @@
{{define "form/inbound"}} {{define "form/inbound"}}
<!-- base --> <!-- base -->
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label='{{ i18n "enable" }}'>
<a-switch v-model="dbInbound.enable"></a-switch>
</a-form-item>
<br>
<a-form-item label='{{ i18n "remark" }}'> <a-form-item label='{{ i18n "remark" }}'>
<a-input v-model.trim="dbInbound.remark"></a-input> <a-input v-model.trim="dbInbound.remark"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "enable" }}'>
<a-switch v-model="dbInbound.enable"></a-switch>
</a-form-item>
<a-form-item label='{{ i18n "protocol" }}'> <a-form-item label='{{ i18n "protocol" }}'>
<a-select v-model="inbound.protocol" style="width: 160px;" :disabled="isEdit" :dropdown-class-name="themeSwitcher.darkCardClass"> <a-select v-model="inbound.protocol" style="width: 160px;">
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option> <a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
@@ -25,14 +24,12 @@
</span> </span>
<a-input v-model.trim="inbound.listen"></a-input> <a-input v-model.trim="inbound.listen"></a-input>
</a-form-item> </a-form-item>
<br>
<a-form-item label='{{ i18n "pages.inbounds.port" }}'> <a-form-item label='{{ i18n "pages.inbounds.port" }}'>
<a-input-number v-model="inbound.port"></a-input-number> <a-input type="number" v-model.number="inbound.port"></a-input>
</a-form-item> </a-form-item>
<br>
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB) <span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span> 0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
@@ -44,7 +41,7 @@
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span>{{ i18n "pages.inbounds.expireDate" }}</span> <span >{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span> <span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
@@ -52,9 +49,8 @@
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss" <a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
:dropdown-class-name="themeSwitcher.darkCardClass" v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker>
v-model="dbInbound._expiryTime" style="width: 250px;"></a-date-picker>
</a-form-item> </a-form-item>
</a-form> </a-form>

View File

@@ -4,19 +4,14 @@
<a-input v-model.trim="inbound.settings.address"></a-input> <a-input v-model.trim="inbound.settings.address"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.destinationPort"}}'> <a-form-item label='{{ i18n "pages.inbounds.destinationPort"}}'>
<a-input-number v-model="inbound.settings.port"></a-input-number> <a-input type="number" v-model.number="inbound.settings.port"></a-input>
</a-form-item> </a-form-item>
<br>
<a-form-item label='{{ i18n "pages.inbounds.network"}}'> <a-form-item label='{{ i18n "pages.inbounds.network"}}'>
<a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="themeSwitcher.darkCardClass"> <a-select v-model="inbound.settings.network" style="width: 100px;">
<a-select-option value="tcp,udp">TCP+UDP</a-select-option> <a-select-option value="tcp,udp">tcp+udp</a-select-option>
<a-select-option value="tcp">TCP</a-select-option> <a-select-option value="tcp">tcp</a-select-option>
<a-select-option value="udp">UDP</a-select-option> <a-select-option value="udp">udp</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<br>
<a-form-item label="FollowRedirect">
<a-switch v-model="inbound.settings.followRedirect"></a-switch>
</a-form-item>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -1,127 +1,18 @@
{{define "form/shadowsocks"}} {{define "form/shadowsocks"}}
<a-form layout="inline" style="padding: 10px 0px;">
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.shadowsockses.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template>
</a-tooltip>
</span>
<a-icon @click="client.email = RandomUtil.randomLowerAndNum(8)" type="sync"> </a-icon>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item>
<a-form-item label="Password">
<a-icon @click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
<a-input v-model.trim="client.password" style="width: 250px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email && app.subSettings.enable">
<span slot="label">
Subscription
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"> </a-icon>
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email && app.tgBotEnable">
<span slot="label">
Telegram ID
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="client.tgId"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.IPLimit" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="client.limitIp" min="0"></a-input-number>
</a-form-item>
<br>
<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="client._totalGB" :min="0"></a-input-number>
</a-form-item>
<br>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
</a-form-item>
<br>
<a-form-item v-if="delayedStart" label='{{ i18n "pages.client.expireDays" }}'>
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
<a-form-item v-else>
<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:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.darkCardClass"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
</a-form-item>
</a-collapse-panel>
</a-collapse>
<a-collapse v-else>
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.shadowsockses.length">
<table width="100%">
<tr class="client-table-header">
<th>{{ i18n "pages.inbounds.email" }}</th>
<th>Password</th>
</tr>
<tr v-for="(client, index) in inbound.settings.shadowsockses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
<td>[[ client.email ]]</td>
<td>[[ client.password ]]</td>
</tr>
</table>
</a-collapse-panel>
</a-collapse>
</a-form>
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label='{{ i18n "encryption" }}'> <a-form-item label='{{ i18n "encryption" }}'>
<a-select v-model="inbound.settings.method" style="width: 250px;" :dropdown-class-name="themeSwitcher.darkCardClass"> <a-select v-model="inbound.settings.method" style="width: 165px;">
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option> <a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "password" }}'> <a-form-item label='{{ i18n "password" }}'>
<a-icon @click="inbound.settings.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon> <a-input v-model.trim="inbound.settings.password"></a-input>
<a-input v-model.trim="inbound.settings.password" style="width: 250px;"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.network" }}'> <a-form-item label='{{ i18n "pages.inbounds.network" }}'>
<a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="themeSwitcher.darkCardClass"> <a-select v-model="inbound.settings.network" style="width: 100px;">
<a-select-option value="tcp,udp">TCP+UDP</a-select-option> <a-select-option value="tcp,udp">tcp+udp</a-select-option>
<a-select-option value="tcp">TCP</a-select-option> <a-select-option value="tcp">tcp</a-select-option>
<a-select-option value="udp">UDP</a-select-option> <a-select-option value="udp">udp</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</a-form> </a-form>

View File

@@ -1,10 +1,10 @@
{{define "form/socks"}} {{define "form/socks"}}
<a-form layout="inline"> <a-form layout="inline">
<!-- <a-form-item label="密码认证">-->
<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>
</a-form-item> </a-form-item>
<br>
<template v-if="inbound.settings.auth === 'password'"> <template v-if="inbound.settings.auth === 'password'">
<a-form-item label='{{ i18n "username" }}'> <a-form-item label='{{ i18n "username" }}'>
<a-input v-model.trim="inbound.settings.accounts[0].user"></a-input> <a-input v-model.trim="inbound.settings.accounts[0].user"></a-input>
@@ -13,11 +13,11 @@
<a-input v-model.trim="inbound.settings.accounts[0].pass"></a-input> <a-input v-model.trim="inbound.settings.accounts[0].pass"></a-input>
</a-form-item> </a-form-item>
</template> </template>
<br>
<a-form-item label='{{ i18n "pages.inbounds.enable" }} udp'> <a-form-item label='{{ i18n "pages.inbounds.enable" }} udp'>
<a-switch v-model="inbound.settings.udp"></a-switch> <a-switch v-model="inbound.settings.udp"></a-switch>
</a-form-item> </a-form-item>
<a-form-item v-if="inbound.settings.udp" label="IP"> <a-form-item v-if="inbound.settings.udp"
label="IP">
<a-input v-model.trim="inbound.settings.ip"></a-input> <a-input v-model.trim="inbound.settings.ip"></a-input>
</a-form-item> </a-form-item>
</a-form> </a-form>

View File

@@ -1,150 +1,161 @@
{{define "form/trojan"}} {{define "form/trojan"}}
<a-form layout="inline" style="padding: 10px 0px;"> <a-form layout="inline">
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.trojans.slice(0,1)" v-if="!isEdit"> <label style="color: green;">{{ i18n "clients"}}</label>
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'> <a-collapse activeKey="0" v-for="(trojan, index) in inbound.settings.trojans"
:key="`trojan-${index}`">
<a-collapse-panel :class="getHeaderStyle(trojan.email)" :header="getHeaderText(trojan.email)">
<a-tag v-if="isExpiry(index) || ((getUpStats(trojan.email) + getDownStats(trojan.email)) > trojan.totalGB && trojan.totalGB != 0)" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
<a-form layout="inline">
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span>{{ i18n "pages.inbounds.email" }}</span> Email
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.emailDesc" }}</span> The Email Must Be Completely Unique
</template> </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> </a-tooltip>
</span> </span>
<a-icon @click="client.email = RandomUtil.randomLowerAndNum(8)" type="sync"> </a-icon> <a-input v-model.trim="trojan.email" style="width: 150px;"></a-input>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Password"> <a-form-item label="Password" >
<a-icon @click="client.password = RandomUtil.randomSeq(10)" type="sync"> </a-icon> <a-input v-model.trim="trojan.password" style="width: 150px;"></a-input>
<a-input v-model.trim="client.password" style="width: 150px;"></a-input> </a-form-item>
</a-form-item> <a-form-item>
<a-form-item v-if="client.email && app.subSettings.enable">
<span slot="label"> <span slot="label">
Subscription IP Count Limit
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span> disable inbound if more than entered count (0 for disable limit ip)
</template> </template>
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"> </a-icon> <a-input type="number" v-model.number="trojan.limitIp" min="0" style="width: 70px;"></a-input>
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item> </a-form-item>
<a-form-item v-if="client.email && app.tgBotEnable"> <a-form-item v-if="trojan.email && trojan.limitIp > 0 && isEdit">
<span slot="label"> <span slot="label">
Telegram ID IP log
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.telegramDesc" }}</span> IPs history Log (before enabling inbound after it has been disabled by IP limit, you should clear the log)
</template> </template>
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span>
<a-input v-model.trim="client.tgId"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.IPLimit" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span> clear the log
</template> </template>
<a-icon type="question-circle" theme="filled"></a-icon> <span style="color: #FF4D4F">
<a-icon type="delete" @click="clearDBClientIps(trojan.email,$event)"></a-icon>
</span>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input-number v-model="client.limitIp" min="0"></a-input-number> <a-form layout="block">
<a-textarea readonly @click="getDBClientIps(trojan.email,$event)" placeholder="Click To Get IPs" :auto-size="{ minRows: 2, maxRows: 10 }">
</a-textarea>
</a-form>
</a-form-item> </a-form-item>
<br> </a-form>
<a-form-item v-if="inbound.xtls" label="Flow"> <a-form-item v-if="inbound.XTLS" label="Flow">
<a-select v-model="client.flow" style="width: 150px" :dropdown-class-name="themeSwitcher.darkCardClass"> <a-select v-model="trojan.flow" style="width: 150px">
<a-select-option value="">{{ i18n "none" }}</a-select-option> <a-select-option value="">{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB) <span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span> 0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template> </template>
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </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: 170px;"></a-date-picker>
</a-form-item>
<a-form layout="inline">
<a-tooltip v-if="trojan._totalGB > 0">
<template slot="title">
{{ i18n "pages.inbounds.resetTraffic" }}
</template>
<span style="color: #FF4D4F">
<a-icon type="delete" @click="resetClientTraffic(trojan,$event)"></a-icon>
</span> </span>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number> </a-tooltip>
</a-form-item> <a-tag color="blue">[[ sizeFormat(getUpStats(trojan.email)) ]] / [[ sizeFormat(getDownStats(trojan.email)) ]]</a-tag>
<br> <a-tag v-if="trojan._totalGB > 0" color="red">used : [[ sizeFormat(getUpStats(trojan.email) + getDownStats(trojan.email)) ]]</a-tag>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'> <a-tag v-show="inbound.settings.trojans.length > 1" @click="removeClient(index, inbound.settings.trojans)">
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" width="22" height="22" class="mt-2 cursor-pointer">
</a-form-item> <path fill="none" d="M0 0h24v24H0z" />
<br> <path fill="#EC4899"
<a-form-item v-if="delayedStart" label='{{ i18n "pages.client.expireDays" }}'> 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"
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number> />
</a-form-item> </svg>
<a-form-item v-else> </a-tag>
<span slot="label"> </a-form>
<span>{{ i18n "pages.inbounds.expireDate" }}</span> </a-collapse-panel>
<a-tooltip> </a-collapse>
<template slot="title"> <a-tag @click="addClient(inbound.protocol, inbound.settings.trojans)">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" class="ml-2 cursor-pointer">
</template> <path fill="none" d="M0 0h24v24H0z" />
<a-icon type="question-circle" theme="filled"></a-icon> <path fill="green"
</a-tooltip> 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"
</span> />
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss" </svg>
:dropdown-class-name="themeSwitcher.darkCardClass" </a-tag>
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
</a-form-item> <template v-if="inbound.isTcp && inbound.tls || inbound.XTLS">
</a-collapse-panel>
</a-collapse>
<a-collapse v-else>
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.trojans.length">
<table width="100%">
<tr class="client-table-header">
<th>{{ i18n "pages.inbounds.email" }}</th>
<th>Password</th>
</tr>
<tr v-for="(client, index) in inbound.settings.trojans" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
<td>[[ client.email ]]</td>
<td>[[ client.password ]]</td>
</tr>
</table>
</a-collapse-panel>
</a-collapse>
</a-form>
<template v-if="inbound.isTcp">
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label="Fallbacks"> <a-form-item label="Fallbacks">
<a-row> <a-row>
<a-button type="primary" size="small" @click="inbound.settings.addTrojanFallback()"> <a-button type="primary" size="small"
@click="inbound.settings.addTrojanFallback()">
+ +
</a-button> </a-button>
</a-row> </a-row>
</a-form-item> </a-form-item>
</a-form> </a-form>
<!-- trojan fallbacks --> <!-- trojan fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline"> <a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
<a-divider> <a-divider>
fallback[[ index + 1 ]] fallback[[ index + 1 ]]
<a-icon type="delete" @click="() => inbound.settings.delTrojanFallback(index)" <a-icon type="delete" @click="() => inbound.settings.delTrojanFallback(index)"
style="color: rgb(255, 77, 79);cursor: pointer;"/> style="color: rgb(255, 77, 79);cursor: pointer;"/>
</a-divider> </a-divider>
<a-form-item label="Name"> <a-form-item label="name">
<a-input v-model="fallback.name"></a-input> <a-input v-model="fallback.name"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Alpn"> <a-form-item label="alpn">
<a-input v-model="fallback.alpn"></a-input> <a-input v-model="fallback.alpn"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Path"> <a-form-item label="path">
<a-input v-model="fallback.path"></a-input> <a-input v-model="fallback.path"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Dest"> <a-form-item label="dest">
<a-input v-model="fallback.dest"></a-input> <a-input v-model="fallback.dest"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="xVer"> <a-form-item label="xver">
<a-input-number v-model="fallback.xver"></a-input-number> <a-input type="number" v-model.number="fallback.xver"></a-input>
</a-form-item> </a-form-item>
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/> <a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
</a-form> </a-form>

View File

@@ -1,158 +1,170 @@
{{define "form/vless"}} {{define "form/vless"}}
<a-form layout="inline" style="padding: 10px 0px;"> <a-form layout="inline">
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit"> <label style="color: green;">{{ i18n "clients"}}</label>
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'> <a-collapse activeKey="0" v-for="(vless, index) in inbound.settings.vlesses"
:key="`vless-${index}`">
<a-collapse-panel :class="getHeaderStyle(vless.email)" :header="getHeaderText(vless.email)">
<a-tag v-if="isExpiry(index) || ((getUpStats(vless.email) + getDownStats(vless.email)) > vless.totalGB && vless.totalGB != 0)" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
<a-form layout="inline">
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span>{{ i18n "pages.inbounds.email" }}</span> Email
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.emailDesc" }}</span> The Email Must Be Completely Unique
</template> </template>
<!--Renew Svg Icon-->
<svg
@click="getNewEmail(vless)"
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> </a-tooltip>
</span> </span>
<a-icon @click="client.email = RandomUtil.randomLowerAndNum(8)" type="sync"> </a-icon> <a-input v-model.trim="vless.email" style="width: 150px;"></a-input>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="ID"> <a-form-item label="ID">
<a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon> <a-input v-model.trim="vless.id" style="width: 300px;" ></a-input>
<a-input v-model.trim="client.id" style="width: 300px;"></a-input> </a-form-item>
</a-form-item> <a-form-item>
<a-form-item v-if="client.email && app.subSettings.enable">
<span slot="label"> <span slot="label">
Subscription IP Count Limit
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span> disable inbound if more than entered count (0 for disable limit ip)
</template> </template>
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"> </a-icon> <a-input type="number" v-model.number="vless.limitIp" min="0" style="width: 70px;"></a-input>
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item> </a-form-item>
<a-form-item v-if="client.email && app.tgBotEnable"> <a-form-item v-if="vless.email && vless.limitIp > 0 && isEdit">
<span slot="label"> <span slot="label">
Telegram ID IP log
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.telegramDesc" }}</span> IPs history Log (before enabling inbound after it has been disabled by IP limit, you should clear the log)
</template> </template>
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span>
<a-input v-model.trim="client.tgId"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.IPLimit" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span> clear the log
</template> </template>
<a-icon type="question-circle" theme="filled"></a-icon> <span style="color: #FF4D4F">
<a-icon type="delete" @click="clearDBClientIps(vless.email,$event)"></a-icon>
</span>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input-number v-model="client.limitIp" min="0"></a-input-number> <a-form layout="block">
<a-textarea readonly @click="getDBClientIps(vless.email,$event)" placeholder="Click To Get IPs" :auto-size="{ minRows: 2, maxRows: 10 }">
</a-textarea>
</a-form>
</a-form-item> </a-form-item>
<br> </a-form>
<a-form-item v-if="inbound.xtls" label="Flow"> <a-form-item v-if="inbound.XTLS" label="Flow">
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass"> <a-select v-model="inbound.settings.vlesses[index].flow" style="width: 150px">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option> <a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item v-else-if="inbound.canEnableTlsFlow()" label="Flow"> <a-form-item v-else-if="inbound.canEnableTlsFlow()" label="Flow" layout="inline">
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass"> <a-select v-model="inbound.settings.vlesses[index].flow" style="width: 150px">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option> <a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB) <span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span> 0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template> </template>
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span>
<a-input-number v-model="vless._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="vless._expiryTime" style="width: 300px;"></a-date-picker>
</a-form-item>
<a-form layout="inline">
<a-tooltip v-if="vless._totalGB > 0">
<template slot="title">
{{ i18n "pages.inbounds.resetTraffic" }}
</template>
<span style="color: #FF4D4F">
<a-icon type="delete" @click="resetClientTraffic(vless,$event)"></a-icon>
</span> </span>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number> </a-tooltip>
</a-form-item> <a-tag color="blue">[[ sizeFormat(getUpStats(vless.email)) ]] / [[ sizeFormat(getDownStats(vless.email)) ]]</a-tag>
<br> <a-tag v-if="vless._totalGB > 0" color="red">used : [[ sizeFormat(getUpStats(vless.email) + getDownStats(vless.email)) ]]</a-tag>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch> <a-tag v-show="inbound.settings.vlesses.length > 1" @click="removeClient(index, inbound.settings.vlesses)">
</a-form-item> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" width="22" height="22" class="mt-2 cursor-pointer">
<br> <path fill="none" d="M0 0h24v24H0z" />
<a-form-item v-if="delayedStart" label='{{ i18n "pages.client.expireDays" }}'> <path fill="#EC4899"
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number> 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"
</a-form-item> />
<a-form-item v-else> </svg>
<span slot="label"> </a-tag>
<span>{{ i18n "pages.inbounds.expireDate" }}</span> </a-form>
<a-tooltip> </a-collapse-panel>
<template slot="title"> </a-collapse>
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span> <a-tag @click="addClient(inbound.protocol, inbound.settings.vlesses)">
</template> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" class="ml-2 cursor-pointer">
<a-icon type="question-circle" theme="filled"></a-icon> <path fill="none" d="M0 0h24v24H0z" />
</a-tooltip> <path fill="green"
</span> 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"
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss" />
:dropdown-class-name="themeSwitcher.darkCardClass" </svg>
v-model="client._expiryTime" style="width: 170px;"></a-date-picker> </a-tag>
</a-form-item>
</a-collapse-panel> <template v-if="inbound.isTcp && inbound.tls || inbound.XTLS">
</a-collapse>
<a-collapse v-else>
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vlesses.length">
<table width="100%">
<tr class="client-table-header">
<th>{{ i18n "pages.inbounds.email" }}</th>
<th>Flow</th>
<th>ID</th>
</tr>
<tr v-for="(client, index) in inbound.settings.vlesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
<td>[[ client.email ]]</td>
<td>[[ client.flow ]]</td>
<td>[[ client.id ]]</td>
</tr>
</table>
</a-collapse-panel>
</a-collapse>
</a-form>
<template v-if="inbound.isTcp">
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label="Fallbacks"> <a-form-item label="Fallbacks">
<a-row> <a-row>
<a-button type="primary" size="small" @click="inbound.settings.addFallback()"> <a-button type="primary" size="small"
@click="inbound.settings.addFallback()">
+ +
</a-button> </a-button>
</a-row> </a-row>
</a-form-item> </a-form-item>
</a-form> </a-form>
<!-- vless fallbacks --> <!-- vless fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline"> <a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
<a-divider> <a-divider>
fallback[[ index + 1 ]] fallback[[ index + 1 ]]
<a-icon type="delete" @click="() => inbound.settings.delFallback(index)" <a-icon type="delete" @click="() => inbound.settings.delFallback(index)"
style="color: rgb(255, 77, 79);cursor: pointer;"/> style="color: rgb(255, 77, 79);cursor: pointer;"/>
</a-divider> </a-divider>
<a-form-item label="Name"> <a-form-item label="name">
<a-input v-model="fallback.name"></a-input> <a-input v-model="fallback.name"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Alpn"> <a-form-item label="alpn">
<a-input v-model="fallback.alpn"></a-input> <a-input v-model="fallback.alpn"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Path"> <a-form-item label="path">
<a-input v-model="fallback.path"></a-input> <a-input v-model="fallback.path"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Dest"> <a-form-item label="dest">
<a-input v-model="fallback.dest"></a-input> <a-input v-model="fallback.dest"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="xVer"> <a-form-item label="xver">
<a-input-number v-model="fallback.xver"></a-input-number> <a-input type="number" v-model.number="fallback.xver"></a-input>
</a-form-item> </a-form-item>
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/> <a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
</a-form> </a-form>

View File

@@ -1,116 +1,131 @@
{{define "form/vmess"}} {{define "form/vmess"}}
<a-form layout="inline" style="padding: 10px 0px;"> <a-form layout="inline">
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vmesses.slice(0,1)" v-if="!isEdit"> <label style="color: green;">{{ i18n "clients"}}</label>
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'> <a-collapse activeKey="0" v-for="(vmess, index) in inbound.settings.vmesses"
:key="`vmess-${index}`">
<a-collapse-panel :class="getHeaderStyle(vmess.email)" :header="getHeaderText(vmess.email)">
<a-tag v-if="isExpiry(index) || ((getUpStats(vmess.email) + getDownStats(vmess.email)) > vmess.totalGB && vmess.totalGB != 0)" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
<a-form layout="inline">
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span>{{ i18n "pages.inbounds.email" }}</span> Email
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.emailDesc" }}</span> The Email Must Be Completely Unique
</template> </template>
<!--Renew Svg Icon-->
<svg
@click="getNewEmail(vmess)"
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> </a-tooltip>
</span> </span>
<a-icon @click="client.email = RandomUtil.randomLowerAndNum(8)" type="sync"> </a-icon> <a-input v-model.trim="vmess.email" style="width: 150px;"></a-input>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item> </a-form-item>
<br> <a-form-item label="ID">
<a-form-item label="ID"> <a-input v-model.trim="vmess.id" style="width: 300px;" ></a-input>
<a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon> </a-form-item>
<a-input v-model.trim="client.id" style="width: 300px;"></a-input> <a-form-item label='{{ i18n "additional" }} ID'>
</a-form-item> <a-input type="number" v-model.number="vmess.alterId"></a-input>
<a-form-item v-if="client.email && app.subSettings.enable"> </a-form-item>
<a-form-item>
<span slot="label"> <span slot="label">
Subscription IP Count Limit
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span> disable inbound if more than entered count (0 for disable limit ip)
</template> </template>
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"> </a-icon> <a-input type="number" v-model.number="vmess.limitIp" min="0" style="width: 70px;" ></a-input>
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item> </a-form-item>
<a-form-item v-if="client.email && app.tgBotEnable"> <a-form-item v-if="vmess.email && vmess.limitIp > 0 && isEdit">
<span slot="label"> <span slot="label">
Telegram ID IP Log
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.telegramDesc" }}</span> IPs history Log (before enabling inbound after it has been disabled by IP limit, you should clear the log)
</template> </template>
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span>
<a-input v-model.trim="client.tgId"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.IPLimit" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span> clear the log
</template> </template>
<a-icon type="question-circle" theme="filled"></a-icon> <span style="color: #FF4D4F">
<a-icon type="delete" @click="clearDBClientIps(vmess.email,$event)"></a-icon>
</span>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input-number v-model="client.limitIp" min="0"></a-input-number> <a-textarea readonly @click="getDBClientIps(vmess.email,$event)" placeholder="Click To Get IPs" :auto-size="{ minRows: 2, maxRows: 10 }">
</a-textarea>
</a-form-item> </a-form-item>
<br> </a-form>
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB) <span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span> 0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template> </template>
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span>
<a-input-number v-model="vmess._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="vmess._expiryTime" style="width: 300px;"></a-date-picker>
</a-form-item>
<a-form layout="inline">
<a-tooltip v-if="vmess._totalGB > 0">
<template slot="title">
{{ i18n "pages.inbounds.resetTraffic" }}
</template>
<span style="color: #FF4D4F">
<a-icon type="delete" @click="resetClientTraffic(vmess,$event)"></a-icon>
</span> </span>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number> </a-tooltip>
</a-form-item> <a-tag color="blue">[[ sizeFormat(getUpStats(vmess.email)) ]] / [[ sizeFormat(getDownStats(vmess.email)) ]]</a-tag>
<br> <a-tag v-if="vmess._totalGB > 0" color="red">used : [[ sizeFormat(getUpStats(vmess.email) + getDownStats(vmess.email)) ]]</a-tag>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'> <a-tag v-show="inbound.settings.vmesses.length > 1" @click="removeClient(index, inbound.settings.vmesses)">
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" width="22" height="22" class="mt-2 cursor-pointer">
</a-form-item> <path fill="none" d="M0 0h24v24H0z" />
<br> <path fill="#EC4899"
<a-form-item v-if="delayedStart" label='{{ i18n "pages.client.expireDays" }}'> 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"
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number> />
</a-form-item> </svg>
<a-form-item v-else> </a-tag>
<span slot="label"> </a-form>
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title"> </a-collapse-panel>
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template> </a-collapse>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> <a-tag @click="addClient(inbound.protocol, inbound.settings.vmesses)">
</span> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" class="ml-2 cursor-pointer">
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss" <path fill="none" d="M0 0h24v24H0z" />
:dropdown-class-name="themeSwitcher.darkCardClass" <path fill="green"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker> 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"
</a-form-item> />
</a-collapse-panel> </svg>
</a-collapse> </a-tag>
<a-collapse v-else>
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount" }}: ' + inbound.settings.vmesses.length">
<table width="100%">
<tr class="client-table-header">
<th>{{ i18n "pages.inbounds.email" }}</th>
<th>ID</th>
</tr>
<tr v-for="(client, index) in inbound.settings.vmesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
<td>[[ client.email ]]</td>
<td>[[ client.id ]]</td>
</tr>
</table>
</a-collapse-panel>
</a-collapse>
</a-form>
<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>
</a-form-item> </a-form-item>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -1,21 +1,16 @@
{{define "form/sniffing"}} {{define "form/sniffing"}}
<a-form layout="inline"> <a-form layout="inline">
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
Sniffing Sniffing
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span >{{ i18n "pages.inbounds.noRecommendKeepDefault" }}</span> <span >{{ i18n "pages.inbounds.noRecommendKeepDefault" }}</span>
</template> </template>
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-switch v-model="inbound.sniffing.enabled"></a-switch> <a-switch v-model="inbound.sniffing.enabled"></a-switch>
</a-form-item> </a-form-item>
<a-form-item>
<a-checkbox-group v-model="inbound.sniffing.destOverride" v-if="inbound.sniffing.enabled">
<a-checkbox v-for="key,value in SNIFFING_OPTION" :value="key">[[ value ]]</a-checkbox>
</a-checkbox-group>
</a-form-item>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -1,14 +1,7 @@
{{define "form/streamGRPC"}} {{define "form/streamGRPC"}}
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label="AcceptProxyProtocol">
<a-switch v-model="inbound.stream.grpc.sockopt.acceptProxyProtocol"></a-switch>
</a-form-item>
<br>
<a-form-item label="ServiceName"> <a-form-item label="ServiceName">
<a-input v-model.trim="inbound.stream.grpc.serviceName"></a-input> <a-input v-model.trim="inbound.stream.grpc.serviceName"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Multi Mode">
<a-switch v-model="inbound.stream.grpc.multiMode"></a-switch>
</a-form-item>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -1,9 +1,5 @@
{{define "form/streamHTTP"}} {{define "form/streamHTTP"}}
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label="AcceptProxyProtocol">
<a-switch v-model="inbound.stream.http.sockopt.acceptProxyProtocol"></a-switch>
</a-form-item>
<br>
<a-form-item label='{{ i18n "path" }}'> <a-form-item label='{{ i18n "path" }}'>
<a-input v-model.trim="inbound.stream.http.path"></a-input> <a-input v-model.trim="inbound.stream.http.path"></a-input>
</a-form-item> </a-form-item>

View File

@@ -1,46 +1,38 @@
{{define "form/streamKCP"}} {{define "form/streamKCP"}}
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label='{{ i18n "camouflage" }}'> <a-form-item label='{{ i18n "camouflage" }}'>
<a-select v-model="inbound.stream.kcp.type" style="width: 280px;" :dropdown-class-name="themeSwitcher.darkCardClass"> <a-select v-model="inbound.stream.kcp.type" style="width: 280px;">
<a-select-option value="none">None (Not Camouflage)</a-select-option> <a-select-option value="none">NoneNot Camouflage</a-select-option>
<a-select-option value="srtp">SRTP (Camouflage Video Call)</a-select-option> <a-select-option value="srtp">SRTPCamouflage Video Call</a-select-option>
<a-select-option value="utp">UTP (Camouflage BT Download)</a-select-option> <a-select-option value="utp">UTPCamouflage BT Download</a-select-option>
<a-select-option value="wechat-video">Wechat-Video (Camouflage WeChat Video)</a-select-option> <a-select-option value="wechat-video">Wechat-VideoCamouflage WeChat Video</a-select-option>
<a-select-option value="dtls">DTLS (Camouflage DTLS 1.2 Packages)</a-select-option> <a-select-option value="dtls">DTLSCamouflage DTLS 1.2 Packages</a-select-option>
<a-select-option value="wireguard">Wireguard (Camouflage Wireguard Packages)</a-select-option> <a-select-option value="wireguard">WireguardCamouflage Wireguard Packages</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<br>
<a-form-item label='{{ i18n "password" }}'> <a-form-item label='{{ i18n "password" }}'>
<a-input v-model="inbound.stream.kcp.seed"></a-input> <a-input v-model.number="inbound.stream.kcp.seed"></a-input>
</a-form-item> </a-form-item>
<br>
<a-form-item label="MTU"> <a-form-item label="MTU">
<a-input-number v-model="inbound.stream.kcp.mtu"></a-input-number> <a-input type="number" v-model.number="inbound.stream.kcp.mtu"></a-input>
</a-form-item> </a-form-item>
<br>
<a-form-item label="TTI (ms)"> <a-form-item label="TTI (ms)">
<a-input-number v-model="inbound.stream.kcp.tti"></a-input-number> <a-input type="number" v-model.number="inbound.stream.kcp.tti"></a-input>
</a-form-item> </a-form-item>
<br>
<a-form-item label="Uplink Capacity (MB/S)"> <a-form-item label="Uplink Capacity (MB/S)">
<a-input-number v-model="inbound.stream.kcp.upCap"></a-input-number> <a-input type="number" v-model.number="inbound.stream.kcp.upCap"></a-input>
</a-form-item> </a-form-item>
<br>
<a-form-item label="Downlink Capacity (MB/S)"> <a-form-item label="Downlink Capacity (MB/S)">
<a-input-number v-model="inbound.stream.kcp.downCap"></a-input-number> <a-input type="number" v-model.number="inbound.stream.kcp.downCap"></a-input>
</a-form-item> </a-form-item>
<br>
<a-form-item label="Congestion"> <a-form-item label="Congestion">
<a-switch v-model="inbound.stream.kcp.congestion"></a-switch> <a-switch v-model="inbound.stream.kcp.congestion"></a-switch>
</a-form-item> </a-form-item>
<br>
<a-form-item label="Read Buffer Size (MB)"> <a-form-item label="Read Buffer Size (MB)">
<a-input-number v-model="inbound.stream.kcp.readBuffer"></a-input-number> <a-input type="number" v-model.number="inbound.stream.kcp.readBuffer"></a-input>
</a-form-item> </a-form-item>
<br>
<a-form-item label="Write Buffer Size (MB)"> <a-form-item label="Write Buffer Size (MB)">
<a-input-number v-model="inbound.stream.kcp.writeBuffer"></a-input-number> <a-input type="number" v-model.number="inbound.stream.kcp.writeBuffer"></a-input>
</a-form-item> </a-form-item>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -1,7 +1,7 @@
{{define "form/streamQUIC"}} {{define "form/streamQUIC"}}
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'> <a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'>
<a-select v-model="inbound.stream.quic.security" style="width: 165px;" :dropdown-class-name="themeSwitcher.darkCardClass"> <a-select v-model="inbound.stream.quic.security" style="width: 165px;">
<a-select-option value="none">none</a-select-option> <a-select-option value="none">none</a-select-option>
<a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option> <a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option>
<a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option> <a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option>
@@ -11,13 +11,13 @@
<a-input v-model.trim="inbound.stream.quic.key"></a-input> <a-input v-model.trim="inbound.stream.quic.key"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "camouflage" }}'> <a-form-item label='{{ i18n "camouflage" }}'>
<a-select v-model="inbound.stream.quic.type" style="width: 280px;" :dropdown-class-name="themeSwitcher.darkCardClass"> <a-select v-model="inbound.stream.quic.type" style="width: 280px;">
<a-select-option value="none">none (not camouflage)</a-select-option> <a-select-option value="none">nonenot camouflage</a-select-option>
<a-select-option value="srtp">srtp (camouflage video call)</a-select-option> <a-select-option value="srtp">srtpcamouflage video call</a-select-option>
<a-select-option value="utp">utp (camouflage BT download)</a-select-option> <a-select-option value="utp">utpcamouflage BT download</a-select-option>
<a-select-option value="wechat-video">wechat-video (camouflage WeChat video)</a-select-option> <a-select-option value="wechat-video">wechat-videocamouflage WeChat video</a-select-option>
<a-select-option value="dtls">dtls (camouflage DTLS 1.2 packages)</a-select-option> <a-select-option value="dtls">dtlscamouflage DTLS 1.2 packages</a-select-option>
<a-select-option value="wireguard">wireguard (camouflage wireguard packages)</a-select-option> <a-select-option value="wireguard">wireguardcamouflage wireguard packages</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</a-form> </a-form>

View File

@@ -2,11 +2,11 @@
<!-- select stream network --> <!-- select stream network -->
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label='{{ i18n "transmission" }}'> <a-form-item label='{{ i18n "transmission" }}'>
<a-select v-model="inbound.stream.network" @change="streamNetworkChange" :dropdown-class-name="themeSwitcher.darkCardClass"> <a-select v-model="inbound.stream.network" @change="streamNetworkChange">
<a-select-option value="tcp">TCP</a-select-option> <a-select-option value="tcp">TCP</a-select-option>
<a-select-option value="kcp">KCP</a-select-option> <a-select-option value="kcp">KCP</a-select-option>
<a-select-option value="ws">WS</a-select-option> <a-select-option value="ws">WS</a-select-option>
<a-select-option value="http">H2</a-select-option> <a-select-option value="http">HTTP</a-select-option>
<a-select-option value="quic">QUIC</a-select-option> <a-select-option value="quic">QUIC</a-select-option>
<a-select-option value="grpc">gRPC</a-select-option> <a-select-option value="grpc">gRPC</a-select-option>
</a-select> </a-select>

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 {{ i18n "camouflage" }}'> <a-form-item label="HTTP 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'">
@@ -13,7 +13,8 @@
</a-form> </a-form>
<!-- tcp request --> <!-- tcp request -->
<a-form v-if="inbound.stream.tcp.type === 'http'" layout="inline"> <a-form v-if="inbound.stream.tcp.type === 'http'"
layout="inline">
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestVersion" }}'> <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestVersion" }}'>
<a-input v-model.trim="inbound.stream.tcp.request.version"></a-input> <a-input v-model.trim="inbound.stream.tcp.request.version"></a-input>
</a-form-item> </a-form-item>
@@ -25,11 +26,12 @@
<a-input v-model.trim="inbound.stream.tcp.request.path[index]"></a-input> <a-input v-model.trim="inbound.stream.tcp.request.path[index]"></a-input>
</a-row> </a-row>
</a-form-item> </a-form-item>
<br> <a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
<a-form-item>
<a-row> <a-row>
<span>{{ i18n "pages.inbounds.stream.general.requestHeader" }}:</span> <a-button size="small"
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.tcp.request.addHeader('Host', 'xxx.com')">+</a-button> @click="inbound.stream.tcp.request.addHeader('Host', 'xxx.com')">
+
</a-button>
</a-row> </a-row>
<a-input-group v-for="(header, index) in inbound.stream.tcp.request.headers"> <a-input-group v-for="(header, index) in inbound.stream.tcp.request.headers">
<a-input style="width: 50%" v-model.trim="header.name" <a-input style="width: 50%" v-model.trim="header.name"
@@ -37,16 +39,19 @@
<a-input style="width: 50%" v-model.trim="header.value" <a-input style="width: 50%" v-model.trim="header.value"
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'> addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter"> <template slot="addonAfter">
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.tcp.request.removeHeader(index)">-</a-button> <a-button size="small"
@click="inbound.stream.tcp.request.removeHeader(index)">
-
</a-button>
</template> </template>
</a-input> </a-input>
</a-input-group> </a-input-group>
</a-form-item> </a-form-item>
</a-form> </a-form>
<!-- tcp response --> <!-- tcp response -->
<a-form v-if="inbound.stream.tcp.type === 'http'" layout="inline"> <a-form v-if="inbound.stream.tcp.type === 'http'"
layout="inline">
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseVersion" }}'> <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseVersion" }}'>
<a-input v-model.trim="inbound.stream.tcp.response.version"></a-input> <a-input v-model.trim="inbound.stream.tcp.response.version"></a-input>
</a-form-item> </a-form-item>
@@ -56,10 +61,12 @@
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseStatusDescription" }}'> <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseStatusDescription" }}'>
<a-input v-model.trim="inbound.stream.tcp.response.reason"></a-input> <a-input v-model.trim="inbound.stream.tcp.response.reason"></a-input>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}'>
<a-row> <a-row>
<span>{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}:</span> <a-button size="small"
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">+</a-button> @click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">
+
</a-button>
</a-row> </a-row>
<a-input-group v-for="(header, index) in inbound.stream.tcp.response.headers"> <a-input-group v-for="(header, index) in inbound.stream.tcp.response.headers">
<a-input style="width: 50%" v-model.trim="header.name" <a-input style="width: 50%" v-model.trim="header.name"
@@ -67,7 +74,10 @@
<a-input style="width: 50%" v-model.trim="header.value" <a-input style="width: 50%" v-model.trim="header.value"
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'> addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter"> <template slot="addonAfter">
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.tcp.response.removeHeader(index)">-</a-button> <a-button size="small"
@click="inbound.stream.tcp.response.removeHeader(index)">
-
</a-button>
</template> </template>
</a-input> </a-input>
</a-input-group> </a-input-group>

View File

@@ -3,15 +3,17 @@
<a-form-item label="AcceptProxyProtocol"> <a-form-item label="AcceptProxyProtocol">
<a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch> <a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch>
</a-form-item> </a-form-item>
<br> </a-form>
<a-form layout="inline">
<a-form-item label='{{ i18n "path" }}'> <a-form-item label='{{ i18n "path" }}'>
<a-input v-model.trim="inbound.stream.ws.path"></a-input> <a-input v-model.trim="inbound.stream.ws.path"></a-input>
</a-form-item> </a-form-item>
<br> <a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
<a-form-item>
<a-row> <a-row>
<span>{{ i18n "pages.inbounds.stream.general.requestHeader" }}:</span> <a-button size="small"
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.ws.addHeader('Host', '')">+</a-button> @click="inbound.stream.ws.addHeader('Host', '')">
+
</a-button>
</a-row> </a-row>
<a-input-group v-for="(header, index) in inbound.stream.ws.headers"> <a-input-group v-for="(header, index) in inbound.stream.ws.headers">
<a-input style="width: 50%" v-model.trim="header.name" <a-input style="width: 50%" v-model.trim="header.name"
@@ -19,7 +21,10 @@
<a-input style="width: 50%" v-model.trim="header.value" <a-input style="width: 50%" v-model.trim="header.value"
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'> addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter"> <template slot="addonAfter">
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.ws.removeHeader(index)">-</a-button> <a-button size="small"
@click="inbound.stream.ws.removeHeader(index)">
-
</a-button>
</template> </template>
</a-input> </a-input>
</a-input-group> </a-input-group>

View File

@@ -1,211 +1,74 @@
{{define "form/tlsSettings"}} {{define "form/tlsSettings"}}
<!-- tls enable --> <!-- tls enable -->
<a-form layout="inline" v-if="inbound.canSetTls()"> <a-form layout="inline" v-if="inbound.canSetTls()">
<a-form-item v-if="inbound.canEnableTls()" label="TLS"> <a-form-item label="TLS">
<a-switch v-model="inbound.tls"> <a-switch v-model="inbound.tls">
</a-switch> </a-switch>
</a-form-item> </a-form-item>
<a-form-item v-if="inbound.canEnableReality()"> <a-form-item v-if="inbound.canEnableXTLS()" label="XTLS">
<span slot="label"> <a-switch v-model="inbound.XTLS"></a-switch>
Reality
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.realityDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-switch v-model="inbound.reality"></a-switch>
</a-form-item>
<a-form-item v-if="inbound.canEnableXtls()">
<span slot="label">
XTLS
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.xtlsDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-switch v-model="inbound.xtls"></a-switch>
</a-form-item> </a-form-item>
</a-form> </a-form>
<!-- tls settings --> <!-- tls settings -->
<a-form v-if="inbound.tls" layout="inline"> <a-form v-if="inbound.tls || inbound.XTLS"layout="inline">
<a-form-item label='Multi Domain'> <a-form-item label="SNI" placeholder="Server Name Indication" v-if="inbound.tls">
<a-switch v-model="multiDomain"></a-switch> <a-input v-model.trim="inbound.stream.tls.settings[0].serverName"></a-input>
</a-form-item>
<a-form-item v-if="multiDomain">
<a-row>
<span>Domains:</span>
<a-button v-if="multiDomain" type="primary" size="small" @click="inbound.stream.tls.settings.domains.push({remark: '', domain: ''})" style="margin-left: 10px">+</a-button>
</a-row>
<a-input-group v-for="(row, index) in inbound.stream.tls.settings.domains">
<a-input style="width: 40%" v-model.trim="row.remark" addon-before='{{ i18n "remark" }}'></a-input>
<a-input style="width: 60%" v-model.trim="row.domain" addon-before='{{ i18n "host" }}'>
<template slot="addonAfter">
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.tls.settings.domains.splice(index, 1)">-</a-button>
</template>
</a-input>
</a-input-group>
</a-form-item>
<a-form-item v-else label='{{ i18n "domainName" }}'>
<a-input v-model.trim="inbound.stream.tls.server" style="width: 250px"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="CipherSuites"> <a-form-item label="CipherSuites">
<a-select v-model="inbound.stream.tls.cipherSuites" style="width: 300px" :dropdown-class-name="themeSwitcher.darkCardClass"> <a-select v-model="inbound.stream.tls.cipherSuites" style="width: 300px">
<a-select-option value="">auto</a-select-option> <a-select-option value="">auto</a-select-option>
<a-select-option v-for="key in TLS_CIPHER_OPTION" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in TLS_CIPHER_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="MinVersion"> <a-form-item label="MinVersion">
<a-select v-model="inbound.stream.tls.minVersion" style="width: 60px" :dropdown-class-name="themeSwitcher.darkCardClass"> <a-select v-model="inbound.stream.tls.minVersion" style="width: 60px">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="MaxVersion"> <a-form-item label="MaxVersion">
<a-select v-model="inbound.stream.tls.maxVersion" style="width: 60px" :dropdown-class-name="themeSwitcher.darkCardClass"> <a-select v-model="inbound.stream.tls.maxVersion" style="width: 60px">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="SNI" placeholder="Server Name Indication"> <a-form-item label="uTLS" v-if="inbound.tls" >
<a-input v-model.trim="inbound.stream.tls.settings.serverName" style="width: 250px"></a-input> <a-select v-model="inbound.stream.tls.settings[0].fingerprint" style="width: 135px">
</a-form-item> <a-select-option value=''>None</a-select-option>
<a-form-item label="uTLS"> <a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
<a-select v-model="inbound.stream.tls.settings.fingerprint" </a-select>
style="width: 170px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value=''>None</a-select-option>
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Alpn">
<a-checkbox-group v-model="inbound.stream.tls.alpn" style="width:200px">
<a-checkbox v-for="key,value in ALPN_OPTION" :value="key">[[ value ]]</a-checkbox>
</a-checkbox-group>
</a-form-item>
<br>
<a-form-item label="Allow insecure">
<a-switch v-model="inbound.stream.tls.settings.allowInsecure"></a-switch>
</a-form-item>
<br>
<a-form-item label="Reject Unknown SNI">
<a-switch v-model="inbound.stream.tls.rejectUnknownSni"></a-switch>
</a-form-item>
<template v-for="cert,index in inbound.stream.tls.certs">
<a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="cert.useFile" button-style="solid">
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
</a-radio-group>
<a-button v-if="index === 0" type="primary" size="small" @click="inbound.stream.tls.addCert()" style="margin-left: 10px">+</a-button>
<a-button v-if="inbound.stream.tls.certs.length>1" type="primary" size="small" @click="inbound.stream.tls.removeCert(index)" style="margin-left: 10px">-</a-button>
</a-form-item> </a-form-item>
<template v-if="cert.useFile">
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
<a-input v-model.trim="cert.certFile" style="width:300px;"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
<a-input v-model.trim="cert.keyFile" style="width:300px;"></a-input>
</a-form-item>
<a-button type="primary" icon="import" @click="setDefaultCertData(index)">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</template>
<template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="cert.cert"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="cert.key"></a-input>
</a-form-item>
</template>
</template>
</a-form>
<!-- xtls settings -->
<a-form v-else-if="inbound.xtls" layout="inline">
<a-form-item label='{{ i18n "domainName" }}'> <a-form-item label='{{ i18n "domainName" }}'>
<a-input v-model.trim="inbound.stream.xtls.server"></a-input> <a-input v-model.trim="inbound.stream.tls.server"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="SNI" placeholder="Server Name Indication"> <a-form-item label="Alpn">
<a-input v-model.trim="inbound.stream.xtls.settings.serverName" style="width: 250px"></a-input> <a-checkbox-group v-model="inbound.stream.tls.alpn" style="width:200px">
</a-form-item>
<a-form-item label="Alpn">
<a-checkbox-group v-model="inbound.stream.xtls.alpn" style="width:200px">
<a-checkbox v-for="key in ALPN_OPTION" :value="key">[[ key ]]</a-checkbox> <a-checkbox v-for="key in ALPN_OPTION" :value="key">[[ key ]]</a-checkbox>
</a-checkbox-group> </a-checkbox-group>
</a-form-item> </a-form-item>
<a-form-item label="Allow insecure"> <a-form-item label="Allow insecure">
<a-switch v-model="inbound.stream.xtls.settings.allowInsecure"></a-switch> <a-switch v-model="inbound.stream.tls.settings[0].allowInsecure"></a-switch>
</a-form-item> </a-form-item>
<template v-for="cert,index in inbound.stream.xtls.certs"> <a-form-item label='{{ i18n "certificate" }}'>
<a-form-item label='{{ i18n "certificate" }}'> <a-radio-group v-model="inbound.stream.tls.certs[0].useFile" button-style="solid">
<a-radio-group v-model="cert.useFile" button-style="solid"> <a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button> <a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button> </a-radio-group>
</a-radio-group> </a-form-item>
<a-button v-if="index === 0" type="primary" size="small" @click="inbound.stream.xtls.addCert()" style="margin-left: 10px">+</a-button> <template v-if="inbound.stream.tls.certs[0].useFile">
<a-button v-if="inbound.stream.xtls.certs.length>1" type="primary" size="small" @click="inbound.stream.xtls.removeCert(index)" style="margin-left: 10px">-</a-button> <a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
<a-input v-model.trim="inbound.stream.tls.certs[0].certFile" style="width:300px;"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
<a-input v-model.trim="inbound.stream.tls.certs[0].keyFile" style="width:300px;"></a-input>
</a-form-item>
</template>
<template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="inbound.stream.tls.certs[0].cert"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="inbound.stream.tls.certs[0].key"></a-input>
</a-form-item> </a-form-item>
<template v-if="cert.useFile">
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
<a-input v-model.trim="cert.certFile" style="width:300px;"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
<a-input v-model.trim="cert.keyFile" style="width:300px;"></a-input>
</a-form-item>
<a-button type="primary" icon="import" @click="setDefaultCertXtls(index)">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</template>
<template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="cert.cert"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="cert.key"></a-input>
</a-form-item>
</template>
</template> </template>
</a-form> </a-form>
<!-- reality settings -->
<a-form v-else-if="inbound.reality" layout="inline">
<a-form-item label="Show">
<a-switch v-model="inbound.stream.reality.show">
</a-switch>
</a-form-item>
<a-form-item label="xVer">
<a-input-number v-model="inbound.stream.reality.xver" :min="0" style="width: 60px"></a-input-number>
</a-form-item>
<a-form-item label="uTLS">
<a-select v-model="inbound.stream.reality.settings.fingerprint"
style="width: 135px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='{{ i18n "domainName" }}'>
<a-input v-model.trim="inbound.stream.reality.settings.serverName" style="width: 250px"></a-input>
</a-form-item>
<a-form-item label="Dest">
<a-input v-model.trim="inbound.stream.reality.dest" style="width: 300px"></a-input>
</a-form-item>
<a-form-item label="Server Names">
<a-input v-model.trim="inbound.stream.reality.serverNames" style="width: 300px"></a-input>
</a-form-item>
<a-form-item label="ShortIds">
<a-icon @click="inbound.stream.reality.shortIds = RandomUtil.randomShortId()" type="sync"> </a-icon>
<a-input v-model.trim="inbound.stream.reality.shortIds" style="width: 150px;"></a-input>
</a-form-item>
<br>
<a-form-item label="SpiderX">
<a-input v-model.trim="inbound.stream.reality.settings.spiderX" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item label="Private Key">
<a-input v-model.trim="inbound.stream.reality.privateKey" style="width: 300px"></a-input>
</a-form-item>
<a-form-item label="Public Key">
<a-input v-model.trim="inbound.stream.reality.settings.publicKey" style="width: 300px"></a-input>
</a-form-item>
<a-form-item>
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get New Key</a-button>
</a-form-item>
</a-form>
{{end}} {{end}}

View File

@@ -1,63 +0,0 @@
{{define "client_table"}}
<template slot="actions" slot-scope="text, client, index">
<a-tooltip>
<template slot="title">{{ i18n "qrCode" }}</template>
<a-icon style="font-size: 24px;" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record,index);"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title">{{ i18n "pages.client.edit" }}</template>
<a-icon style="font-size: 24px;" type="edit" @click="openEditClient(record.id,client);"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title">{{ i18n "info" }}</template>
<a-icon style="font-size: 24px;" type="info-circle" @click="showInfo(record,index);"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
<a-icon style="font-size: 24px;" type="retweet" @click="resetClientTraffic(client,record.id)" v-if="client.email.length > 0"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title"><span style="color: #FF4D4F"> {{ i18n "delete"}}</span></template>
<a-icon style="font-size: 24px;" type="delete" v-if="isRemovable(record.id)" @click="delClient(record.id,client)"></a-icon>
</a-tooltip>
</template>
<template slot="enable" slot-scope="text, client, index">
<a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch>
</template>
<template slot="client" slot-scope="text, client">
[[ client.email ]]
<a-tag v-if="!isClientEnabled(record, client.email)" color="red">{{ i18n "depleted" }}</a-tag>
</template>
<template slot="traffic" slot-scope="text, client">
<a-popover :overlay-class-name="themeSwitcher.darkClass">
<template slot="content" v-if="client.email">
<table cellpadding="2" width="100%">
<tr>
<td>↑[[ sizeFormat(getUpStats(record, client.email)) ]]</td>
<td>↓[[ sizeFormat(getDownStats(record, client.email)) ]]</td>
</tr>
<tr v-if="client.totalGB > 0">
<td>{{ i18n "remained" }}</td>
<td>[[ sizeFormat(client.totalGB - getUpStats(record, client.email) - getDownStats(record, client.email)) ]]</td>
</tr>
</table>
</template>
<a-tag :color="statsColor(record, client.email)">
[[ sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]] /
<template v-if="client.totalGB > 0">[[client._totalGB]]GB</template>
<template v-else></template>
</a-tag>
</a-popover>
</template>
<template slot="expiryTime" slot-scope="text, client, index">
<template v-if="client.expiryTime > 0">
<a-tag :color="usageColor(new Date().getTime(), app.expireDiff, client.expiryTime)">
[[ DateUtil.formatMillis(client._expiryTime) ]]
</a-tag>
</template>
<a-tag v-else-if="client.expiryTime < 0" color="cyan">
[[ client._expiryTime ]] {{ i18n "pages.client.days" }}
</a-tag>
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
</template>
{{end}}

View File

@@ -3,289 +3,149 @@
v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}' v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}'
:closable="true" :closable="true"
:mask-closable="true" :mask-closable="true"
:class="themeSwitcher.darkCardClass" :class="siderDrawer.isDarkTheme ? darkClass : ''"
:footer="null" :footer="null"
width="600px" width="600px"
> >
<table style="margin-bottom: 10px; width: 100%;"> <table style="margin-bottom: 10px; width: 100%;">
<tr><td> <tr><td>
<table> <table>
<tr><td>{{ i18n "protocol" }}</td><td><a-tag color="green">[[ dbInbound.protocol ]]</a-tag></td></tr> <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.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> <tr><td>{{ i18n "pages.inbounds.port" }}</td><td><a-tag color="green">[[ dbInbound.port ]]</a-tag></td></tr>
</table> </table>
</td> </td>
<td v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS"> <td v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
<table> <table>
<tr> <tr>
<td>{{ i18n "transmission" }}</td><td><a-tag color="green">[[ inbound.network ]]</a-tag></td> <td>{{ i18n "transmission" }}</td><td><a-tag color="green">[[ inbound.network ]]</a-tag></td>
</tr> </tr>
<template v-if="inbound.isTcp || inbound.isWs || inbound.isH2"> <template v-if="inbound.isTcp || inbound.isWs || inbound.isH2">
<tr v-if="inbound.host"><td>{{ i18n "host" }}</td><td><a-tag color="green">[[ inbound.host ]]</a-tag></td></tr> <tr v-if="inbound.host"><td>{{ i18n "host" }}</td><td><a-tag color="green">[[ inbound.host ]]</a-tag></td></tr>
<tr v-else><td>{{ i18n "host" }}</td><td><a-tag color="orange">{{ i18n "none" }}</a-tag></td></tr> <tr v-else><td>{{ i18n "host" }}</td><td><a-tag color="orange">{{ i18n "none" }}</a-tag></td></tr>
<tr v-if="inbound.path"><td>{{ i18n "path" }}</td><td><a-tag color="green">[[ inbound.path ]]</a-tag></td></tr> <tr v-if="inbound.path"><td>{{ i18n "path" }}</td><td><a-tag color="green">[[ inbound.path ]]</a-tag></td></tr>
<tr v-else><td>{{ i18n "path" }}</td><td><a-tag color="orange">{{ i18n "none" }}</a-tag></td></tr> <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">
<tr><td>quic {{ i18n "encryption" }}</td><td><a-tag color="green">[[ inbound.quicSecurity ]]</a-tag></td></tr> <tr><td>quic {{ i18n "encryption" }}</td><td><a-tag color="green">[[ inbound.quicSecurity ]]</a-tag></td></tr>
<tr><td>quic {{ i18n "password" }}</td><td><a-tag color="green">[[ inbound.quicKey ]]</a-tag></td></tr> <tr><td>quic {{ i18n "password" }}</td><td><a-tag color="green">[[ inbound.quicKey ]]</a-tag></td></tr>
<tr><td>quic {{ i18n "camouflage" }}</td><td><a-tag color="green">[[ inbound.quicType ]]</a-tag></td></tr> <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">
<tr><td>kcp {{ i18n "encryption" }}</td><td><a-tag color="green">[[ inbound.kcpType ]]</a-tag></td></tr> <tr><td>kcp {{ i18n "encryption" }}</td><td><a-tag color="green">[[ inbound.kcpType ]]</a-tag></td></tr>
<tr><td>kcp {{ i18n "password" }}</td><td><a-tag color="green">[[ inbound.kcpSeed ]]</a-tag></td></tr> <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">
<tr><td>grpc serviceName</td><td><a-tag color="green">[[ inbound.serviceName ]]</a-tag></td></tr> <tr><td>grpc serviceName</td><td><a-tag color="green">[[ inbound.serviceName ]]</a-tag></td></tr>
<tr><td>grpc multiMode</td><td><a-tag color="green">[[ inbound.stream.grpc.multiMode ]]</a-tag></td></tr> </template>
</template> </table>
</table> </td></tr>
</td></tr> <tr colspan="2">
<tr colspan="2" v-if="dbInbound.hasLink()"> <td v-if="inbound.tls">
<td v-if="inbound.tls"> tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br /> tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag> </td>
</td> <td v-else-if="inbound.xtls">
<td v-else-if="inbound.xtls"> xtls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
xtls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br /> xtls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
xtls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag> </td>
</td> <td v-else>tls: <a-tag color="red">{{ i18n "disabled" }}</a-tag>
<td v-else-if="inbound.reality">
reality: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
reality Destination: <a-tag :color="inbound.stream.reality.dest ? 'green' : 'orange'">[[ inbound.stream.reality.dest ]]</a-tag>
</td>
<td v-else>tls: <a-tag color="red">{{ i18n "disabled" }}</a-tag>
</td> </td>
</tr> </tr>
</table> </table>
<template v-if="infoModal.clientSettings"> <a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider> <table style="margin-bottom: 10px; width: 100%;">
<table style="margin-bottom: 10px;"> <tr><th>[[ Object.keys(infoModal.clientSettings)[0] ]]</th><th>[[ Object.keys(infoModal.clientSettings)[1] ]]</th><th>[[ Object.keys(infoModal.clientSettings)[2] ]]</th></tr>
<tr> <tr>
<td>{{ i18n "pages.inbounds.email" }}</td> <td><a-tag color="green">[[ Object.values(infoModal.clientSettings)[0] ]]</a-tag></td>
<td><a-tag color="green">[[ infoModal.clientSettings.email ]]</a-tag></td> <td><a-tag color="green">[[ Object.values(infoModal.clientSettings)[1] ]]</a-tag></td>
</tr> <td><a-tag color="green">[[ Object.values(infoModal.clientSettings)[2] ]]</a-tag></td>
<tr v-if="infoModal.clientSettings.id"> </tr>
<td>ID</td> </table>
<td><a-tag color="green">[[ infoModal.clientSettings.id ]]</a-tag></td> <table style="margin-bottom: 10px; width: 100%;">
</tr> <tr><th>{{ i18n "usage" }}</th><th>{{ i18n "pages.inbounds.totalFlow" }}</th><th>{{ i18n "pages.inbounds.expireDate" }}</th><th>{{ i18n "enable" }}</th></tr>
<tr v-if="infoModal.inbound.canEnableTlsFlow()">
<td>Flow</td>
<td><a-tag color="green">[[ infoModal.clientSettings.flow ]]</a-tag></td>
</tr>
<tr v-if="infoModal.clientSettings.password">
<td>Password</td>
<td><a-tag color="green">[[ infoModal.clientSettings.password ]]</a-tag></td>
</tr>
<tr>
<td>{{ i18n "status" }}</td>
<td>
<a-tag v-if="isEnable" color="blue">{{ i18n "enabled" }}</a-tag>
<a-tag v-else color="red">{{ i18n "disabled" }}</a-tag>
<a-tag v-if="!isActive" color="red">{{ i18n "depleted" }}</a-tag>
</td>
</tr>
<tr v-if="infoModal.clientStats">
<td>{{ i18n "usage" }}</td>
<td>
<a-tag color="green">[[ sizeFormat(infoModal.clientStats.up + infoModal.clientStats.down) ]]</a-tag>
<a-tag color="blue">↑ [[ sizeFormat(infoModal.clientStats.up) ]] / [[ sizeFormat(infoModal.clientStats.down) ]] ↓</a-tag>
</td>
</tr>
</table>
<table style="margin-bottom: 10px; width: 100%;">
<tr>
<th>{{ i18n "remained" }}</th>
<th>{{ i18n "pages.inbounds.totalFlow" }}</th>
<th>{{ i18n "pages.inbounds.expireDate" }}</th>
</tr>
<tr> <tr>
<td> <td>
<a-tag v-if="infoModal.clientStats && infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)"> <a-tag :color="statsColor(infoModal.clientStats)">
[[ sizeFormat(infoModal.clientSettings.totalGB - infoModal.clientStats.up - infoModal.clientStats.down) ]] [[ sizeFormat(infoModal.clientStats['up']) ]] /
[[ sizeFormat(infoModal.clientStats['down']) ]]
([[ sizeFormat(infoModal.clientStats['up'] + infoModal.clientStats['down']) ]])
</a-tag> </a-tag>
</td> </td>
<td> <td>
<a-tag v-if="infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)"> <a-tag v-if="infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)">[[ sizeFormat(infoModal.clientSettings.totalGB) ]]</a-tag>
[[ sizeFormat(infoModal.clientSettings.totalGB) ]]
</a-tag>
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag> <a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
</td> </td>
<td> <td>
<template v-if="infoModal.clientSettings.expiryTime > 0"> <template v-if="infoModal.clientSettings.expiryTime > 0">
<a-tag :color="usageColor(new Date().getTime(), app.expireDiff, infoModal.clientSettings.expiryTime)"> <a-tag :color="infoModal.isExpired ? 'red' : 'blue'">
[[ DateUtil.formatMillis(infoModal.clientSettings.expiryTime) ]] [[ DateUtil.formatMillis(infoModal.clientSettings.expiryTime) ]]
</a-tag> </a-tag>
</template> </template>
<a-tag v-else-if="infoModal.clientSettings.expiryTime < 0" color="cyan">[[ infoModal.clientSettings.expiryTime / -86400000 ]] {{ i18n "pages.client.days" }}</a-tag>
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag> <a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
</td> </td>
<td>
<a-tag v-if="infoModal.clientStats.enable" color="blue">{{ i18n "enabled" }}</a-tag>
<a-tag v-else color="red">{{ i18n "disabled" }}</a-tag>
</td>
</tr> </tr>
</table> </table>
<template v-if="app.subSettings.enable && infoModal.clientSettings.subId"> <div v-if="dbInbound.hasLink()">
<a-divider>Subscription link</a-divider>
<a-row>
<a-col :span="22"><a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a></a-col>
<a-col :span="2">
<a-tooltip title='{{ i18n "copy" }}'>
<button class="ant-btn ant-btn-primary" id="copy-sub-link" @click="copyToClipboard('copy-sub-link', infoModal.subLink)">
<a-icon type="snippets"></a-icon>
</button>
</a-tooltip>
</a-col>
</a-row>
</template>
<template v-if="app.tgBotEnable && infoModal.clientSettings.tgId">
<a-divider>Telegram Username</a-divider>
<a-row>
<a-col :span="22"><a :href="[[ infoModal.tgLink ]]" target="_blank">@[[ infoModal.clientSettings.tgId ]]</a></a-col>
<a-col :span="2">
<a-tooltip title='{{ i18n "copy" }}'>
<button class="ant-btn ant-btn-primary" id="copy-tg-link" @click="copyToClipboard('copy-tg-link', '@' + infoModal.clientSettings.tgId)">
<a-icon type="snippets"></a-icon>
</button>
</a-tooltip>
</a-col>
</a-row>
</template>
<template v-if="dbInbound.hasLink()">
<a-divider>URL</a-divider> <a-divider>URL</a-divider>
<a-row v-for="(link,index) in infoModal.links"> <p>[[ infoModal.link ]]</p>
<a-col :span="22"><a-tag color="cyan">[[ link.remark ]]</a-tag><br />[[ link.link ]]</a-col> <button class="ant-btn ant-btn-primary" id="copy-url-link"><a-icon type="snippets"></a-icon>{{ i18n "copy" }}</button>
<a-col :span="2" style="text-align: right;"> </div>
<a-tooltip title='{{ i18n "copy" }}'>
<button class="ant-btn ant-btn-primary" :id="'copy-url-link-'+index" @click="copyToClipboard('copy-url-link-'+index, link.link)">
<a-icon type="snippets"></a-icon>
</button>
</a-tooltip>
</a-col>
</a-row>
</template>
</template>
<template v-else>
<a-divider></a-divider>
<table v-if="inbound.protocol == Protocols.SHADOWSOCKS" style="margin-bottom: 10px; width: 100%;">
<tr>
<th>{{ i18n "encryption" }}</th>
<th>{{ i18n "password" }}</th>
<th>{{ i18n "pages.inbounds.network" }}</th>
</tr><tr>
<td><a-tag color="green">[[ inbound.settings.method ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.password ]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
</tr>
</table>
<table v-if="inbound.protocol == Protocols.DOKODEMO" style="margin-bottom: 10px; width: 100%;">
<tr>
<th>{{ i18n "pages.inbounds.targetAddress" }}</th>
<th>{{ i18n "pages.inbounds.destinationPort" }}</th>
<th>{{ i18n "pages.inbounds.network" }}</th>
<th>FollowRedirect</th>
</tr><tr>
<td><a-tag color="green">[[ inbound.settings.address ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.port ]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.followRedirect ]]</a-tag></td>
</tr>
</table>
</table>
<table v-if="inbound.protocol == Protocols.SOCKS" style="margin-bottom: 10px; width: 100%;">
<tr>
<th>{{ i18n "password" }} Auth</th>
<th>{{ i18n "pages.inbounds.enable" }} udp</th>
<th>IP</th>
</tr><tr>
<td><a-tag color="green">[[ inbound.settings.auth ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.udp]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.ip ]]</a-tag></td>
</tr><tr v-if="inbound.settings.auth == 'password'">
<td> </td>
<td>{{ i18n "username" }}</td>
<td>{{ i18n "password" }}</td>
</tr><tr v-for="account,index in inbound.settings.accounts">
<td><a-tag color="green">[[ index ]]</a-tag></td>
<td><a-tag color="blue">[[ account.user ]]</a-tag></td>
<td><a-tag color="green">[[ account.pass ]]</a-tag></td>
</tr>
</table>
</table>
<table v-if="inbound.protocol == Protocols.HTTP" style="margin-bottom: 10px; width: 100%;">
<tr>
<th> </th>
<th>{{ i18n "username" }}</th>
<th>{{ i18n "password" }}</th>
</tr><tr v-for="account,index in inbound.settings.accounts">
<td><a-tag color="green">[[ index ]]</a-tag></td>
<td><a-tag color="blue">[[ account.user ]]</a-tag></td>
<td><a-tag color="green">[[ account.pass ]]</a-tag></td>
</tr>
</table>
</table>
</template>
</a-modal> </a-modal>
<script> <script>
const infoModal = { const infoModal = {
visible: false, visible: false,
inbound: new Inbound(), inbound: new Inbound(),
dbInbound: new DBInbound(), dbInbound: new DBInbound(),
settings: null, clientSettings: new Inbound.Settings(),
clientSettings: null,
clientStats: [], clientStats: [],
upStats: 0, upStats: 0,
downStats: 0, downStats: 0,
clipboard: null, clipboard: null,
links: [], link: null,
index: null, index: 0,
isExpired: false, isExpired: false,
subLink: '', show(dbInbound, index=0) {
tgLink: '',
show(dbInbound, index) {
this.index = index; this.index = index;
this.inbound = dbInbound.toInbound(); this.inbound = dbInbound.toInbound();
this.dbInbound = new DBInbound(dbInbound); this.dbInbound = new DBInbound(dbInbound);
this.settings = JSON.parse(this.inbound.settings); this.link = dbInbound.genLink(index);
this.clientSettings = this.settings.clients ? Object.values(this.settings.clients)[index] : null; this.clientSettings = Object.values(JSON.parse(this.inbound.settings).clients)[index];
this.clientStats = dbInbound.clientStats;
this.isExpired = this.inbound.isExpiry(index); this.isExpired = this.inbound.isExpiry(index);
this.clientStats = this.settings.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : []; if(dbInbound.clientStats.length > 0)
remark = this.dbInbound.remark + "-" + this.clientSettings.email; {
address = this.dbInbound.address; for (const key in dbInbound.clientStats) {
this.links = []; if (Object.hasOwnProperty.call(dbInbound.clientStats, key)) {
if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) { if(dbInbound.clientStats[key]['email'] == this.clientSettings.email)
this.inbound.stream.tls.settings.domains.forEach((domain) => { this.clientStats = dbInbound.clientStats[key];
this.links.push({
remark: remark + "-" + domain.remark, }
link: this.inbound.genLink(domain.domain, remark + "-" + domain.remark, index)
});
});
} else {
this.links.push({
remark: remark,
link: this.inbound.genLink(address, remark, index)
});
}
if (this.clientSettings) {
if (this.clientSettings.subId) {
this.subLink = this.genSubLink(this.clientSettings.subId);
}
if (this.clientSettings.tgId) {
this.tgLink = "https://t.me/" + this.clientSettings.tgId;
} }
} }
this.visible = true; this.visible = true;
infoModalApp.$nextTick(() => {
if (this.clipboard === null) {
this.clipboard = new ClipboardJS('#copy-url-link', {
text: () => this.link,
});
this.clipboard.on('success', () => app.$message.success('{{ i18n "copySuccess" }}'));
}
});
}, },
close() { close() {
infoModal.visible = false; infoModal.visible = false;
}, },
genSubLink(subID) {
const { domain: host, port, tls: isTLS, path: base } = app.subSettings;
return buildURL({ host, port, isTLS, base, path: subID });
}
}; };
const infoModalApp = new Vue({ const infoModalApp = new Vue({
delimiters: ['[[', ']]'], delimiters: ['[[', ']]'],
el: '#inbound-info-modal', el: '#inbound-info-modal',
@@ -296,35 +156,32 @@
}, },
get inbound() { get inbound() {
return this.infoModal.inbound; return this.infoModal.inbound;
}, }
get isActive() {
if (infoModal.clientStats) {
return infoModal.clientStats.enable;
}
return infoModal.dbInbound.isEnable;
},
get isEnable() {
if (infoModal.clientSettings) {
return infoModal.clientSettings.enable;
}
return infoModal.dbInbound.isEnable;
},
}, },
methods: { methods: {
copyToClipboard(elmentId, content) { setQrCode(elmentId,index) {
content = infoModal.inbound.genLink(infoModal.dbInbound.address,infoModal.dbInbound.remark,index)
new QRious({
element: document.querySelector('#'+elmentId),
size: 260,
value: content,
});
},
copyTextToClipboard(elmentId,content) {
this.infoModal.clipboard = new ClipboardJS('#' + elmentId, { this.infoModal.clipboard = new ClipboardJS('#' + elmentId, {
text: () => content, text: () => content,
}); });
this.infoModal.clipboard.on('success', () => { this.infoModal.clipboard.on('success', () => {
app.$message.success('{{ i18n "copied" }}') app.$message.success('{{ i18n "copySuccess" }}')
this.infoModal.clipboard.destroy(); this.infoModal.clipboard.destroy();
}); });
}, },
statsColor(stats) { statsColor(stats) {
return usageColor(stats.up + stats.down, app.trafficDiff, stats.total); if(stats['total'] === 0) return 'blue'
else if(stats['total'] > 0 && (stats['down']+stats['up']) < stats['total']) return 'cyan'
else return 'red'
} }
}, },
}); });
</script> </script>
{{end}} {{end}}

View File

@@ -1,7 +1,7 @@
{{define "inboundModal"}} {{define "inboundModal"}}
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" @ok="inModal.ok" <a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" @ok="inModal.ok"
:confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false" :confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
:class="themeSwitcher.darkCardClass" :class="siderDrawer.isDarkTheme ? darkClass : ''"
:ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'> :ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
{{template "form/inbound"}} {{template "form/inbound"}}
</a-modal> </a-modal>
@@ -19,7 +19,7 @@
ok() { ok() {
ObjectUtil.execute(inModal.confirm, inModal.inbound, inModal.dbInbound); ObjectUtil.execute(inModal.confirm, inModal.inbound, inModal.dbInbound);
}, },
show({ title = '', okText = '{{ i18n "sure" }}', inbound = null, dbInbound = null, confirm = (inbound, dbInbound) => {}, isEdit = false }) { show({ title='', okText='{{ i18n "sure" }}', inbound=null, dbInbound=null, confirm=(inbound, dbInbound)=>{}, isEdit=false }) {
this.title = title; this.title = title;
this.okText = okText; this.okText = okText;
if (inbound) { if (inbound) {
@@ -43,15 +43,6 @@
loading(loading) { loading(loading) {
inModal.confirmLoading = loading; inModal.confirmLoading = loading;
}, },
getClients(protocol, clientSettings) {
switch (protocol) {
case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans;
case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
default: return null;
}
},
}; };
const protocols = { const protocols = {
@@ -71,7 +62,6 @@
inModal: inModal, inModal: inModal,
Protocols: protocols, Protocols: protocols,
SSMethods: SSMethods, SSMethods: SSMethods,
delayedStart: false,
get inbound() { get inbound() {
return inModal.inbound; return inModal.inbound;
}, },
@@ -80,55 +70,112 @@
}, },
get isEdit() { get isEdit() {
return inModal.isEdit; return inModal.isEdit;
},
get client() {
return inModal.getClients(this.inbound.protocol, this.inbound.settings)[0];
},
get delayedExpireDays() {
return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
},
set delayedExpireDays(days) {
this.client.expiryTime = -86400000 * days;
},
get multiDomain() {
return this.inbound.stream.tls.settings.domains.length > 0;
},
set multiDomain(value) {
if (value) {
inModal.inbound.stream.tls.server = "";
inModal.inbound.stream.tls.settings.domains = [{ remark: "", domain: window.location.hostname }];
} else {
inModal.inbound.stream.tls.server = "";
inModal.inbound.stream.tls.settings.domains = [];
}
} }
}, },
methods: { methods: {
streamNetworkChange() { streamNetworkChange(oldValue) {
if (!inModal.inbound.canSetTls()) { if (oldValue === 'kcp') {
this.inModal.inbound.stream.security = 'none'; this.inModal.inbound.tls = false;
}
if (!inModal.inbound.canEnableReality()) {
this.inModal.inbound.reality = false;
} }
}, },
setDefaultCertData(index) { addClient(protocol, clients) {
inModal.inbound.stream.tls.certs[index].certFile = app.defaultCert; switch (protocol) {
inModal.inbound.stream.tls.certs[index].keyFile = app.defaultKey; case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.Vmess());
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
default: return null;
}
}, },
setDefaultCertXtls(index) { removeClient(index, clients) {
inModal.inbound.stream.xtls.certs[index].certFile = app.defaultCert; clients.splice(index, 1);
inModal.inbound.stream.xtls.certs[index].keyFile = app.defaultKey;
}, },
async getNewX25519Cert() { async getDBClientIps(email, event) {
inModal.loading(true); const msg = await HttpUtil.post('/xui/inbound/clientIps/' + email);
const msg = await HttpUtil.post('/server/getNewX25519Cert'); if (!msg.success) {
inModal.loading(false); return;
}
try {
let ips = JSON.parse(msg.obj);
ips = ips.join(",");
event.target.value = ips;
} catch (error) {
event.target.value = msg.obj;
}
},
async clearDBClientIps(email,event) {
const msg = await HttpUtil.post('/xui/inbound/clearClientIps/'+ email);
if (!msg.success) { if (!msg.success) {
return; return;
} }
inModal.inbound.stream.reality.privateKey = msg.obj.privateKey; event.target.value = ""
inModal.inbound.stream.reality.settings.publicKey = msg.obj.publicKey; },
async resetClientTraffic(client, event) {
const msg = await HttpUtil.post(`/xui/inbound/resetClientTraffic/${client.email}`);
if (!msg.success) {
return;
}
const clientStats = this.inbound.clientStats;
if (clientStats.length > 0) {
for (let i = 0; i < clientStats.length; i++) {
if (clientStats[i].email === client.email) {
clientStats[i].up = 0;
clientStats[i].down = 0;
break; // Stop looping once we've found the matching client.
}
}
}
},
isExpiry(index) {
return this.inbound.isExpiry(index)
},
getUpStats(email) {
clientStats = this.inbound.clientStats
if(clientStats.length > 0)
{
for (const key in clientStats) {
if (Object.hasOwnProperty.call(clientStats, key)) {
if(clientStats[key]['email'] == email)
return clientStats[key]['up']
}
}
}
},
getDownStats(email) {
clientStats = this.inbound.clientStats
if(clientStats.length > 0)
{
for (const key in clientStats) {
if (Object.hasOwnProperty.call(clientStats, key)) {
if(clientStats[key]['email'] == email)
return clientStats[key]['down']
}
}
}
},
isClientEnable(email) {
clientStats = this.dbInbound.clientStats ? this.dbInbound.clientStats.find(stats => stats.email === email) : null
return clientStats ? clientStats['enable'] : true
},
getHeaderText(email) {
if(email == "")
return "Add Client"
return email + (this.isClientEnable(email) == true ? ' Active' : ' Deactive')
},
getHeaderStyle(email) {
return (this.isClientEnable(email) == true ? '' : 'deactive-client')
},
getNewEmail(client) {
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
var string = '';
var len = 7 + Math.floor(Math.random() * 5)
for(var ii=0; ii<len; ii++){
string += chars[Math.floor(Math.random() * chars.length)];
}
client.email = string
} }
}, },
}); });

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
{{define "client_row"}}
<template slot="actions" slot-scope="text, client, index">
<a-tooltip>
<template slot="title">{{ i18n "qrCode" }}</template>
<a-icon style="font-size: 24px;" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record,index);"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title">{{ i18n "info" }}</template>
<a-icon style="font-size: 24px;" type="info-circle" @click="showInfo(record,index);"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
<a-icon style="font-size: 24px;" type="retweet" @click="resetClientTraffic(client,record,$event)" v-if="client.email != ''"></a-icon>
</a-tooltip>
</template>
<template slot="client" slot-scope="text, client">
[[ client.email ]]
<a-tag v-if="!isClientEnabled(record, client.email)" color="red">{{ i18n "disabled" }}</a-tag>
</template>
<template slot="traffic" slot-scope="text, client">
<a-tag color="blue">[[ sizeFormat(getUpStats(record, client.email)) ]] / [[ sizeFormat(getDownStats(record, client.email)) ]]</a-tag>
<template v-if="client._totalGB > 0">
<a-tag v-if="isTrafficExhausted(record, client.email)" color="red">[[client._totalGB]]GB</a-tag>
<a-tag v-else color="cyan">[[client._totalGB]]GB</a-tag>
</template>
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
</template>
<template slot="expiryTime" slot-scope="text, client, index">
<template v-if="client._expiryTime > 0">
<a-tag :color="isExpiry(record, index)? 'red' : 'blue'">
[[ DateUtil.formatMillis(client._expiryTime) ]]
</a-tag>
</template>
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
</template>
{{end}}

View File

@@ -11,36 +11,30 @@
.ant-col-sm-24 { .ant-col-sm-24 {
margin-top: 10px; margin-top: 10px;
} }
.ant-card-dark h2 {
color: hsla(0, 0%, 100%, .65);
}
</style> </style>
<body> <body>
<a-layout id="app" v-cloak> <a-layout id="app" v-cloak>
{{ template "commonSider" . }} {{ template "commonSider" . }}
<a-layout id="content-layout" :style="themeSwitcher.bgStyle"> <a-layout id="content-layout" :style="siderDrawer.isDarkTheme ? bgDarkStyle : ''">
<a-layout-content> <a-layout-content>
<a-spin :spinning="spinning" :delay="200" :tip="loadingTip"/> <a-spin :spinning="spinning" :delay="200" :tip="loadingTip"/>
<transition name="list" appear> <transition name="list" appear>
<a-row> <a-row>
<a-card hoverable :class="themeSwitcher.darkCardClass"> <a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
<a-row> <a-row>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-row> <a-row>
<a-col :span="12" style="text-align: center"> <a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal" <a-progress type="dashboard" status="normal"
:stroke-color="status.cpu.color" :stroke-color="status.cpu.color"
:class="themeSwitcher.darkCardClass" :class="siderDrawer.isDarkTheme ? darkClass : ''"
:percent="status.cpu.percent"></a-progress> :percent="status.cpu.percent"></a-progress>
<div>CPU: [[ cpuCoreFormat(status.cpuCores) ]]</div> <div>CPU</div>
<div>Speed: [[ cpuSpeedFormat(status.cpuSpeedMhz) ]]</div>
</a-col> </a-col>
<a-col :span="12" style="text-align: center"> <a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal" <a-progress type="dashboard" status="normal"
:stroke-color="status.mem.color" :stroke-color="status.mem.color"
:class="themeSwitcher.darkCardClass" :class="siderDrawer.isDarkTheme ? darkClass : ''"
:percent="status.mem.percent"></a-progress> :percent="status.mem.percent"></a-progress>
<div> <div>
{{ i18n "pages.index.memory"}}: [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]] {{ i18n "pages.index.memory"}}: [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]]
@@ -53,7 +47,7 @@
<a-col :span="12" style="text-align: center"> <a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal" <a-progress type="dashboard" status="normal"
:stroke-color="status.swap.color" :stroke-color="status.swap.color"
:class="themeSwitcher.darkCardClass" :class="siderDrawer.isDarkTheme ? darkClass : ''"
:percent="status.swap.percent"></a-progress> :percent="status.swap.percent"></a-progress>
<div> <div>
Swap: [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]] Swap: [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]]
@@ -62,7 +56,7 @@
<a-col :span="12" style="text-align: center"> <a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal" <a-progress type="dashboard" status="normal"
:stroke-color="status.disk.color" :stroke-color="status.disk.color"
:class="themeSwitcher.darkCardClass" :class="siderDrawer.isDarkTheme ? darkClass : ''"
:percent="status.disk.percent"></a-progress> :percent="status.disk.percent"></a-progress>
<div> <div>
{{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]] {{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]]
@@ -77,22 +71,7 @@
<transition name="list" appear> <transition name="list" appear>
<a-row> <a-row>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass"> <a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
3X: <a href="https://github.com/MHSanaei/3x-ui/releases" target="_blank"><a-tag color="green">v{{ .cur_ver }}</a-tag></a>
Xray: <a-tag color="green" style="cursor: pointer;" @click="openSelectV2rayVersion">v[[ status.xray.version ]]</a-tag>
<a href="https://t.me/panel3xui" target="_blank"><a-tag color="green">@panel3xui</a-tag></a>
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass">
{{ i18n "menu.link" }}:
<a-tag color="blue" style="cursor: pointer;" @click="openLogs(logModal.rows, logModal.logLevel)">{{ i18n "pages.index.logs" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="openBackup">{{ i18n "pages.index.backup" }}</a-tag>
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass">
{{ i18n "pages.index.xrayStatus" }}: {{ i18n "pages.index.xrayStatus" }}:
<a-tag :color="status.xray.color">[[ status.xray.state ]]</a-tag> <a-tag :color="status.xray.color">[[ status.xray.state ]]</a-tag>
<a-tooltip v-if="status.xray.state === State.Error"> <a-tooltip v-if="status.xray.state === State.Error">
@@ -101,84 +80,46 @@
</template> </template>
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
<a-tag color="green" style="cursor: pointer;" @click="openSelectV2rayVersion">[[ status.xray.version ]]</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="stopXrayService">{{ i18n "pages.index.stopXray" }}</a-tag> <a-tag color="blue" style="cursor: pointer;" @click="stopXrayService">{{ i18n "pages.index.stopXray" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="restartXrayService">{{ i18n "pages.index.restartXray" }}</a-tag> <a-tag color="blue" style="cursor: pointer;" @click="restartXrayService">{{ i18n "pages.index.restartXray" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch" }}</a-tag> <a-tag color="blue" style="cursor: pointer;" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch" }}</a-tag>
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass"> <a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
<a-row> {{ i18n "pages.index.operationHours" }}:
<a-col :span="12"> <a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag>
{{ i18n "pages.index.systemLoad" }}: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]] <a-tooltip>
<a-tooltip> <template slot="title">
<template slot="title"> {{ i18n "pages.index.operationHoursDesc" }}
{{ i18n "pages.index.systemLoadDesc" }} </template>
</template> <a-icon type="question-circle" theme="filled"></a-icon>
<a-icon type="question-circle" theme="filled"></a-icon> </a-tooltip>
</a-tooltip>
</a-col>
<a-col :span="12">
{{ i18n "pages.index.operationHours" }}:
<a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag>
</a-col>
</a-row>
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass"> <a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
<a-row> {{ i18n "pages.index.systemLoad" }}: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
<a-col :span="12">
IPv4:
<a-tooltip>
<template slot="title">
[[ status.publicIP.ipv4 ]]
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</a-col>
<a-col :span="12">
IPv6:
<a-tooltip>
<template slot="title">
[[ status.publicIP.ipv6 ]]
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</a-col>
</a-row>
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass"> <a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
<a-row> TCP / UDP {{ i18n "pages.index.connectionCount" }}: [[ status.tcpCount ]] / [[ status.udpCount ]]
<a-col :span="12"> <a-tooltip>
TCP: [[ status.tcpCount ]] <template slot="title">
<a-tooltip> {{ i18n "pages.index.connectionCountDesc" }}
<template slot="title"> </template>
{{ i18n "pages.index.connectionTcpCountDesc" }} <a-icon type="question-circle" theme="filled"></a-icon>
</template> </a-tooltip>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</a-col>
<a-col :span="12">
UDP: [[ status.udpCount ]]
<a-tooltip>
<template slot="title">
{{ i18n "pages.index.connectionUdpCountDesc" }}
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</a-col>
</a-row>
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass"> <a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
<a-row> <a-row>
<a-col :span="12"> <a-col :span="12">
<a-icon type="arrow-up"></a-icon> <a-icon type="arrow-up"></a-icon>
[[ sizeFormat(status.netIO.up) ]]/S [[ sizeFormat(status.netIO.up) ]] / S
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
{{ i18n "pages.index.upSpeed" }} {{ i18n "pages.index.upSpeed" }}
@@ -188,7 +129,7 @@
</a-col> </a-col>
<a-col :span="12"> <a-col :span="12">
<a-icon type="arrow-down"></a-icon> <a-icon type="arrow-down"></a-icon>
[[ sizeFormat(status.netIO.down) ]]/S [[ sizeFormat(status.netIO.down) ]] / S
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
{{ i18n "pages.index.downSpeed" }} {{ i18n "pages.index.downSpeed" }}
@@ -200,7 +141,7 @@
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass"> <a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
<a-row> <a-row>
<a-col :span="12"> <a-col :span="12">
<a-icon type="cloud-upload"></a-icon> <a-icon type="cloud-upload"></a-icon>
@@ -229,11 +170,9 @@
</transition> </transition>
</a-layout-content> </a-layout-content>
</a-layout> </a-layout>
<a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}' <a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}'
:closable="true" @ok="() => versionModal.visible = false" :closable="true" @ok="() => versionModal.visible = false"
:class="themeSwitcher.darkCardClass" ok-text='{{ i18n "confirm" }}' cancel-text='{{ i18n "cancel"}}'>
footer="">
<h2>{{ i18n "pages.index.xraySwitchClick"}}</h2> <h2>{{ i18n "pages.index.xraySwitchClick"}}</h2>
<h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2> <h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2>
<template v-for="version, index in versionModal.versions"> <template v-for="version, index in versionModal.versions">
@@ -243,71 +182,8 @@
</a-tag> </a-tag>
</template> </template>
</a-modal> </a-modal>
<a-modal id="log-modal" v-model="logModal.visible" title="X-UI logs"
:closable="true" @ok="() => logModal.visible = false" @cancel="() => logModal.visible = false"
:class="themeSwitcher.darkCardClass"
width="800px"
footer="">
<a-form layout="inline">
<a-form-item label="Count">
<a-select v-model="logModal.rows"
style="width: 80px"
@change="openLogs(logModal.rows, logModal.logLevel)"
:dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="10">10</a-select-option>
<a-select-option value="20">20</a-select-option>
<a-select-option value="50">50</a-select-option>
<a-select-option value="100">100</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Log Level">
<a-select v-model="logModal.logLevel"
style="width: 120px"
@change="openLogs(logModal.rows, logModal.logLevel)"
:dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="debug">Debug</a-select-option>
<a-select-option value="info">Info</a-select-option>
<a-select-option value="notice">Notice</a-select-option>
<a-select-option value="warning">Warning</a-select-option>
<a-select-option value="err">Error</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<button class="ant-btn ant-btn-primary" @click="openLogs(logModal.rows, logModal.logLevel)"><a-icon type="sync"></a-icon> Reload</button>
</a-form-item>
<a-form-item>
<a-button type="primary" style="margin-bottom: 10px;"
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(logModal.logs)" download="x-ui.log">
{{ i18n "download" }} x-ui.log
</a-button>
</a-form-item>
</a-form>
<a-input type="textarea" v-model="logModal.logs" disabled="true"
:autosize="{ minRows: 10, maxRows: 22}"></a-input>
</a-modal>
<a-modal id="backup-modal" v-model="backupModal.visible" :title="backupModal.title"
:closable="true" :class="themeSwitcher.darkCardClass"
@ok="() => backupModal.hide()" @cancel="() => backupModal.hide()">
<p style="color: inherit; font-size: 16px; padding: 4px 2px;">
<a-icon type="warning" style="color: inherit; font-size: 20px;"></a-icon>
[[ backupModal.description ]]
</p>
<a-space direction="horizontal" style="text-align: center" style="margin-bottom: 10px;">
<a-button type="primary" @click="exportDatabase()">
[[ backupModal.exportText ]]
</a-button>
<a-button type="primary" @click="importDatabase()">
[[ backupModal.importText ]]
</a-button>
</a-space>
</a-modal>
</a-layout> </a-layout>
{{template "js" .}} {{template "js" .}}
{{template "component/themeSwitcher" .}}
{{template "textModal"}}
<script> <script>
const State = { const State = {
@@ -346,32 +222,26 @@
class Status { class Status {
constructor(data) { constructor(data) {
this.cpu = new CurTotal(0, 0); this.cpu = new CurTotal(0, 0);
this.cpuCores = 0;
this.cpuSpeedMhz = 0;
this.disk = new CurTotal(0, 0); this.disk = new CurTotal(0, 0);
this.loads = [0, 0, 0]; this.loads = [0, 0, 0];
this.mem = new CurTotal(0, 0); this.mem = new CurTotal(0, 0);
this.netIO = { up: 0, down: 0 }; this.netIO = {up: 0, down: 0};
this.netTraffic = { sent: 0, recv: 0 }; this.netTraffic = {sent: 0, recv: 0};
this.publicIP = { ipv4: 0, ipv6: 0 };
this.swap = new CurTotal(0, 0); this.swap = new CurTotal(0, 0);
this.tcpCount = 0; this.tcpCount = 0;
this.udpCount = 0; this.udpCount = 0;
this.uptime = 0; this.uptime = 0;
this.xray = { state: State.Stop, errorMsg: "", version: "", color: "" }; this.xray = {state: State.Stop, errorMsg: "", version: "", color: ""};
if (data == null) { if (data == null) {
return; return;
} }
this.cpu = new CurTotal(data.cpu, 100); this.cpu = new CurTotal(data.cpu, 100);
this.cpuCores = data.cpuCores;
this.cpuSpeedMhz = data.cpuSpeedMhz;
this.disk = new CurTotal(data.disk.current, data.disk.total); this.disk = new CurTotal(data.disk.current, data.disk.total);
this.loads = data.loads.map(load => toFixed(load, 2)); this.loads = data.loads.map(load => toFixed(load, 2));
this.mem = new CurTotal(data.mem.current, data.mem.total); this.mem = new CurTotal(data.mem.current, data.mem.total);
this.netIO = data.netIO; this.netIO = data.netIO;
this.netTraffic = data.netTraffic; this.netTraffic = data.netTraffic;
this.publicIP = data.publicIP;
this.swap = new CurTotal(data.swap.current, data.swap.total); this.swap = new CurTotal(data.swap.current, data.swap.total);
this.tcpCount = data.tcpCount; this.tcpCount = data.tcpCount;
this.udpCount = data.udpCount; this.udpCount = data.udpCount;
@@ -405,54 +275,13 @@
}, },
}; };
const logModal = {
visible: false,
logs: '',
rows: 20,
logLevel: 'info',
show(logs, rows) {
this.visible = true;
this.rows = rows;
this.logs = logs.join("\n");
},
hide() {
this.visible = false;
},
};
const backupModal = {
visible: false,
title: '',
description: '',
exportText: '',
importText: '',
show({
title = '{{ i18n "pages.index.backupTitle" }}',
description = '{{ i18n "pages.index.backupDescription" }}',
exportText = '{{ i18n "pages.index.exportDatabase" }}',
importText = '{{ i18n "pages.index.importDatabase" }}',
}) {
this.title = title;
this.description = description;
this.exportText = exportText;
this.importText = importText;
this.visible = true;
},
hide() {
this.visible = false;
},
};
const app = new Vue({ const app = new Vue({
delimiters: ['[[', ']]'], delimiters: ['[[', ']]'],
el: '#app', el: '#app',
data: { data: {
siderDrawer, siderDrawer,
themeSwitcher,
status: new Status(), status: new Status(),
versionModal, versionModal,
logModal,
backupModal,
spinning: false, spinning: false,
loadingTip: '{{ i18n "loading"}}', loadingTip: '{{ i18n "loading"}}',
}, },
@@ -462,13 +291,9 @@
this.loadingTip = tip; this.loadingTip = tip;
}, },
async getStatus() { async getStatus() {
try { const msg = await HttpUtil.post('/server/status');
const msg = await HttpUtil.post('/server/status'); if (msg.success) {
if (msg.success) { this.setStatus(msg.obj);
this.setStatus(msg.obj);
}
} catch (e) {
console.error("Failed to get status:", e);
} }
}, },
setStatus(data) { setStatus(data) {
@@ -488,16 +313,16 @@
title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}', title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}',
content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}' + ` ${version}?`, content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}' + ` ${version}?`,
okText: '{{ i18n "confirm"}}', okText: '{{ i18n "confirm"}}',
class: themeSwitcher.darkCardClass,
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: async () => { onOk: async () => {
versionModal.hide(); versionModal.hide();
this.loading(true, '{{ i18n "pages.index.dontRefresh"}}'); this.loading(true, '{{ i18n "pages.index.dontRefreshh"}}');
await HttpUtil.post(`/server/installXray/${version}`); await HttpUtil.post(`/server/installXray/${version}`);
this.loading(false); this.loading(false);
}, },
}); });
}, },
//here add stop xray function
async stopXrayService() { async stopXrayService() {
this.loading(true); this.loading(true);
const msg = await HttpUtil.post('server/stopXrayService'); const msg = await HttpUtil.post('server/stopXrayService');
@@ -506,6 +331,7 @@
return; return;
} }
}, },
//here add restart xray function
async restartXrayService() { async restartXrayService() {
this.loading(true); this.loading(true);
const msg = await HttpUtil.post('server/restartXrayService'); const msg = await HttpUtil.post('server/restartXrayService');
@@ -514,77 +340,13 @@
return; return;
} }
}, },
async openLogs(rows, logLevel) {
this.loading(true);
const msg = await HttpUtil.post('server/logs/' + rows, { logLevel: `${logLevel}` });
this.loading(false);
if (!msg.success) {
return;
}
logModal.show(msg.obj, rows);
},
async openConfig() {
this.loading(true);
const msg = await HttpUtil.post('server/getConfigJson');
this.loading(false);
if (!msg.success) {
return;
}
txtModal.show('config.json', JSON.stringify(msg.obj, null, 2), 'config.json');
},
openBackup() {
backupModal.show({
title: '{{ i18n "pages.index.backupTitle" }}',
description: '{{ i18n "pages.index.backupDescription" }}',
exportText: '{{ i18n "pages.index.exportDatabase" }}',
importText: '{{ i18n "pages.index.importDatabase" }}',
});
},
exportDatabase() {
window.location = basePath + 'server/getDb';
},
importDatabase() {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.db';
fileInput.addEventListener('change', async (event) => {
const dbFile = event.target.files[0];
if (dbFile) {
const formData = new FormData();
formData.append('db', dbFile);
backupModal.hide();
this.loading(true);
const uploadMsg = await HttpUtil.post('server/importDB', formData, {
headers: {
'Content-Type': 'multipart/form-data',
}
});
this.loading(false);
if (!uploadMsg.success) {
return;
}
this.loading(true);
const restartMsg = await HttpUtil.post("/panel/setting/restartPanel");
this.loading(false);
if (restartMsg.success) {
this.loading(true);
await PromiseUtil.sleep(5000);
location.reload();
}
}
});
fileInput.click();
},
}, },
async mounted() { async mounted() {
let retries = 0; while (true) {
while (retries < 5) {
try { try {
await this.getStatus(); await this.getStatus();
retries = 0;
} catch (e) { } catch (e) {
console.error("Error occurred while fetching status:", e); console.error(e);
retries++;
} }
await PromiseUtil.sleep(2000); await PromiseUtil.sleep(2000);
} }

Some files were not shown because too many files have changed in this diff Show More