Compare commits
264 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52a17e217a | ||
|
|
6a49e99a2c | ||
|
|
6062031de4 | ||
|
|
fb2b58110d | ||
|
|
c385662783 | ||
|
|
4a05188a7f | ||
|
|
1454c4ebc5 | ||
|
|
4b1c76e972 | ||
|
|
f1f5d323e8 | ||
|
|
e37f2d3222 | ||
|
|
4f2f855c04 | ||
|
|
dcab4e6f9c | ||
|
|
e703055793 | ||
|
|
7efe1d60d5 | ||
|
|
1d84284b6d | ||
|
|
8404b33232 | ||
|
|
41d39dfaa8 | ||
|
|
761eb5f384 | ||
|
|
e72f67ca54 | ||
|
|
b8df15171e | ||
|
|
5f531f2de1 | ||
|
|
8335238eb3 | ||
|
|
aaf68ecb21 | ||
|
|
db6781f311 | ||
|
|
a85b02c6ab | ||
|
|
19a832cad8 | ||
|
|
f0dd6152fd | ||
|
|
decac2ef74 | ||
|
|
c3ce1da0d6 | ||
|
|
02bc488c1c | ||
|
|
b22f859c38 | ||
|
|
98869c7169 | ||
|
|
150c89db72 | ||
|
|
fe0a8375a3 | ||
|
|
80cfbefd75 | ||
|
|
f2ee18235f | ||
|
|
f48df1e5c0 | ||
|
|
b24855082e | ||
|
|
27434f3235 | ||
|
|
cdb6eac0e6 | ||
|
|
9e13513205 | ||
|
|
b09f52357c | ||
|
|
ac08e86747 | ||
|
|
c9c8abe97b | ||
|
|
0b8beafc89 | ||
|
|
8b6e3491c4 | ||
|
|
6cf2b56f9b | ||
|
|
19b95829e0 | ||
|
|
4bea427c79 | ||
|
|
da7e4d51d6 | ||
|
|
43713fbdf8 | ||
|
|
dc29e858c9 | ||
|
|
c30c6f08f3 | ||
|
|
7c892ac051 | ||
|
|
75dd7b93f5 | ||
|
|
d5de8e1bf3 | ||
|
|
16b4795956 | ||
|
|
e5835c299c | ||
|
|
4fdef3cfde | ||
|
|
cf6a8bd463 | ||
|
|
59a84e844c | ||
|
|
6b0c9a5fad | ||
|
|
77edea5419 | ||
|
|
521870df0a | ||
|
|
dc1c1eb998 | ||
|
|
e78427245a | ||
|
|
fbcab5bc52 | ||
|
|
89c79c3ec3 | ||
|
|
566cd9e9c4 | ||
|
|
ad78cec7c7 | ||
|
|
176ab5f48e | ||
|
|
9c4fa23931 | ||
|
|
2938694c45 | ||
|
|
88d0fb9753 | ||
|
|
d23a7f81ef | ||
|
|
2e363445fc | ||
|
|
33d983bc20 | ||
|
|
3e7c7831bc | ||
|
|
374d49eb92 | ||
|
|
663cf5649f | ||
|
|
095ebccbb0 | ||
|
|
f4bb6b0517 | ||
|
|
575234ac37 | ||
|
|
16cbd86371 | ||
|
|
2d3666e339 | ||
|
|
47987b7785 | ||
|
|
2e1461e6dc | ||
|
|
272457740f | ||
|
|
58c721e7d2 | ||
|
|
b4baf35ed8 | ||
|
|
2001d96148 | ||
|
|
5c390341fb | ||
|
|
c8b1b162ff | ||
|
|
a55645584b | ||
|
|
f536307914 | ||
|
|
4494ffabb6 | ||
|
|
2dc59a601c | ||
|
|
4ad04e2032 | ||
|
|
f2ebe128cc | ||
|
|
f0165c1ad8 | ||
|
|
898f80cb30 | ||
|
|
5d7de0858c | ||
|
|
c0b88fe736 | ||
|
|
fa43248e30 | ||
|
|
cb3da25bc8 | ||
|
|
a40058bb0b | ||
|
|
6ab3bbe7bd | ||
|
|
9e73c82eb3 | ||
|
|
6b3b1b6cbc | ||
|
|
b3b433f84b | ||
|
|
7f16a53309 | ||
|
|
2471bda211 | ||
|
|
cd49971535 | ||
|
|
0013f8989b | ||
|
|
de8c80597f | ||
|
|
9dcc55ea1b | ||
|
|
8255390131 | ||
|
|
438a9684ee | ||
|
|
a6000f22a2 | ||
|
|
2ec5eeb442 | ||
|
|
d319476eb6 | ||
|
|
93d52bc86c | ||
|
|
bda5c2c915 | ||
|
|
b838fe2e74 | ||
|
|
604b9be4a0 | ||
|
|
7b8ef98846 | ||
|
|
2c7c6c260a | ||
|
|
b8c3555b09 | ||
|
|
2d2b30daf1 | ||
|
|
3e3ed4ed52 | ||
|
|
d8d9c64847 | ||
|
|
c489673130 | ||
|
|
2544305fb9 | ||
|
|
519d228db2 | ||
|
|
4cd89f4379 | ||
|
|
fdfc29f6cd | ||
|
|
4ec104c5ee | ||
|
|
bae89272b0 | ||
|
|
8408a45eff | ||
|
|
a37b1bde4c | ||
|
|
953b5d3dea | ||
|
|
c7906e8598 | ||
|
|
e1bc43da5f | ||
|
|
0630642849 | ||
|
|
8bd3827b41 | ||
|
|
24b367b82f | ||
|
|
011443bfc1 | ||
|
|
7417c52c8f | ||
|
|
4d4eef8d8a | ||
|
|
9de8b4acaf | ||
|
|
f21c293693 | ||
|
|
56159d9c52 | ||
|
|
6cbdf64013 | ||
|
|
bb9b9100a8 | ||
|
|
816adfc3ea | ||
|
|
0a95b0c7b2 | ||
|
|
d298f4ffbd | ||
|
|
315e8af025 | ||
|
|
de985263f5 | ||
|
|
dfe0bbd371 | ||
|
|
60cb328698 | ||
|
|
3d7f13225a | ||
|
|
76fdfb2ef2 | ||
|
|
8018023187 | ||
|
|
ea9d5dc2d5 | ||
|
|
96e43fa195 | ||
|
|
f1500a5d31 | ||
|
|
c9a218d060 | ||
|
|
6d18a15c4e | ||
|
|
c0f86d2f38 | ||
|
|
5227fefaeb | ||
|
|
7a51d2f2cc | ||
|
|
02ae61fe6b | ||
|
|
24b9e5bfa3 | ||
|
|
9ff7f14b6e | ||
|
|
c3b42b8ea4 | ||
|
|
5afb8d85fc | ||
|
|
767ee4ec2b | ||
|
|
21b64beb96 | ||
|
|
b84e3ef338 | ||
|
|
86586b7e8f | ||
|
|
f9792632d4 | ||
|
|
a9ec24f811 | ||
|
|
2da7dda794 | ||
|
|
39aae6fd16 | ||
|
|
f355ab5758 | ||
|
|
3847bc0a78 | ||
|
|
546d676472 | ||
|
|
6fb6241c3c | ||
|
|
f481ab993e | ||
|
|
3ef4ab423f | ||
|
|
b5a32ef57e | ||
|
|
4033001798 | ||
|
|
2486b5ff43 | ||
|
|
58647c6496 | ||
|
|
0b8a28d56d | ||
|
|
5d007435ab | ||
|
|
9c05aa514b | ||
|
|
7f2c11220f | ||
|
|
52b02fdef9 | ||
|
|
a4b76929f4 | ||
|
|
33082a271f | ||
|
|
28ede36a10 | ||
|
|
5036e9e28a | ||
|
|
4ca481d071 | ||
|
|
8259024fbe | ||
|
|
e275adbccd | ||
|
|
f96df5bc3d | ||
|
|
a09701c17b | ||
|
|
713285457f | ||
|
|
e1ef746cab | ||
|
|
fc714962ad | ||
|
|
2f46d42214 | ||
|
|
1b9360dfaf | ||
|
|
25b910c6d9 | ||
|
|
7ac79446c7 | ||
|
|
fdf805f264 | ||
|
|
baf8c94b2e | ||
|
|
adcfccbe45 | ||
|
|
c422214ae8 | ||
|
|
bd2d7bce62 | ||
|
|
ca4f83c7b1 | ||
|
|
b6bb0b1787 | ||
|
|
c0b5d5506f | ||
|
|
a2f6d3b8dc | ||
|
|
80cd793154 | ||
|
|
d070a82b3d | ||
|
|
5ec16301a6 | ||
|
|
07245d614a | ||
|
|
6e734553e2 | ||
|
|
275370e32c | ||
|
|
e7c59adc59 | ||
|
|
68c9b55447 | ||
|
|
70f9e32f28 | ||
|
|
5aae32c888 | ||
|
|
36cade2a02 | ||
|
|
907c30f743 | ||
|
|
5202fb2df4 | ||
|
|
73a19a45d7 | ||
|
|
0fb4094fe6 | ||
|
|
d9254fec03 | ||
|
|
2f02b71ed4 | ||
|
|
559aad9967 | ||
|
|
a1c15e9578 | ||
|
|
be9747dcbc | ||
|
|
94eb27d2c4 | ||
|
|
d1f67f7f2f | ||
|
|
bb6757df0f | ||
|
|
e094c351a3 | ||
|
|
fb31ecfa55 | ||
|
|
93a865f010 | ||
|
|
01a3a7b862 | ||
|
|
e233fdb092 | ||
|
|
55bf0c3e55 | ||
|
|
3129fdc103 | ||
|
|
2d20983690 | ||
|
|
490048a975 | ||
|
|
3950f596d8 | ||
|
|
835cf2801c | ||
|
|
e794d3d87f | ||
|
|
2cf762642b | ||
|
|
db24d21621 | ||
|
|
3d5c06bf08 | ||
|
|
a3a2d7a6a3 |
14
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
polar: # Replace with a single Polar username
|
||||
buy_me_a_coffee: mhsanaei
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
24
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the 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.
|
||||
|
||||
**Version (please complete the following information):**
|
||||
- 3X-UI Version : [e.g. 2.3.5]
|
||||
- Xray Version : [e.g. 1.8.13]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
56
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -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
|
||||
7
.github/workflows/docker.yml
vendored
@@ -1,9 +1,10 @@
|
||||
name: Release 3X-UI for Docker
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
workflow_dispatch:
|
||||
- "v*.*.*"
|
||||
|
||||
jobs:
|
||||
build_and_push:
|
||||
@@ -32,7 +33,7 @@ jobs:
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
16
.github/workflows/release.yml
vendored
@@ -1,10 +1,10 @@
|
||||
name: Release 3X-UI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
workflow_dispatch:
|
||||
- "v*.*.*"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.22'
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
cd x-ui/bin
|
||||
|
||||
# Download dependencies
|
||||
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v1.8.10/"
|
||||
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v24.9.30/"
|
||||
if [ "${{ matrix.platform }}" == "amd64" ]; then
|
||||
wget ${Xray_URL}Xray-linux-64.zip
|
||||
unzip Xray-linux-64.zip
|
||||
@@ -125,7 +125,13 @@ jobs:
|
||||
|
||||
- name: Package
|
||||
run: tar -zcvf x-ui-linux-${{ matrix.platform }}.tar.gz x-ui
|
||||
|
||||
|
||||
- name: Upload files to Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: x-ui-linux-${{ matrix.platform }}
|
||||
path: ./x-ui-linux-${{ matrix.platform }}.tar.gz
|
||||
|
||||
- name: Upload files to GH release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
|
||||
@@ -27,7 +27,7 @@ case $1 in
|
||||
esac
|
||||
mkdir -p build/bin
|
||||
cd build/bin
|
||||
wget "https://github.com/XTLS/Xray-core/releases/download/v1.8.10/Xray-linux-${ARCH}.zip"
|
||||
wget "https://github.com/XTLS/Xray-core/releases/download/v24.9.30/Xray-linux-${ARCH}.zip"
|
||||
unzip "Xray-linux-${ARCH}.zip"
|
||||
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
|
||||
mv xray "xray-linux-${FNAME}"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# ========================================================
|
||||
# Stage: Builder
|
||||
# ========================================================
|
||||
FROM golang:1.22-alpine AS builder
|
||||
FROM golang:1.23-alpine AS builder
|
||||
WORKDIR /app
|
||||
ARG TARGETARCH
|
||||
|
||||
|
||||
232
README.es_ES.md
@@ -1,6 +1,4 @@
|
||||
# 3X-UI
|
||||
|
||||
[English](/README.md) | [Chinese](/README.zh.md) | [Español](/README.es_ES.md)
|
||||
[English](/README.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
|
||||
|
||||
<p align="center"><a href="#"><img src="./media/3X-UI.png" alt="Image"></a></p>
|
||||
|
||||
@@ -16,9 +14,15 @@
|
||||
|
||||
**Si este proyecto te es útil, podrías considerar darle una**:star2:
|
||||
|
||||
<p align="left"><a href="#"><img width="125" src="https://github.com/MHSanaei/3x-ui/assets/115543613/7aa895dd-048a-42e7-989b-afd41a74e2e1" alt="Image"></a></p>
|
||||
<p align="left">
|
||||
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
|
||||
<img src="./media/buymeacoffe.png" alt="Image">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
||||
|
||||
## Instalar y Actualizar
|
||||
|
||||
@@ -28,10 +32,10 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
|
||||
|
||||
## Instalar una Versión Personalizada
|
||||
|
||||
Para instalar la versión deseada, agrega la versión al final del comando de instalación. Por ejemplo, ver `v2.2.8`:
|
||||
Para instalar la versión deseada, agrega la versión al final del comando de instalación. Por ejemplo, ver `v2.4.3`:
|
||||
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.2.8
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.4.3
|
||||
```
|
||||
|
||||
## Certificado SSL
|
||||
@@ -173,18 +177,58 @@ eliminar 3x-ui de docker
|
||||
|
||||
</details>
|
||||
|
||||
## Configuración de Nginx
|
||||
<details>
|
||||
<summary>Haga clic aquí para configurar el proxy inverso</summary>
|
||||
|
||||
#### Proxy inverso Nginx
|
||||
```nginx
|
||||
location / {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Range $http_range;
|
||||
proxy_set_header If-Range $http_if_range;
|
||||
proxy_redirect off;
|
||||
proxy_pass http://127.0.0.1:2053;
|
||||
}
|
||||
```
|
||||
|
||||
#### Nginx sub-path
|
||||
- EAsegúrese de que la "Ruta Raíz de la URL del Panel" en la configuración del panel `/sub` es la misma.
|
||||
- El `url` en la configuración del panel debe terminar con `/`.
|
||||
|
||||
```nginx
|
||||
location /sub {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Range $http_range;
|
||||
proxy_set_header If-Range $http_if_range;
|
||||
proxy_redirect off;
|
||||
proxy_pass http://127.0.0.1:2053;
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
## SO Recomendados
|
||||
|
||||
- Ubuntu 20.04+
|
||||
- Debian 11+
|
||||
- CentOS 8+
|
||||
- OpenEuler 22.03+
|
||||
- Fedora 36+
|
||||
- Arch Linux
|
||||
- Parch Linux
|
||||
- Manjaro
|
||||
- Armbian
|
||||
- AlmaLinux 9+
|
||||
- Rockylinux 9+
|
||||
- AlmaLinux 8.0+
|
||||
- Rocky Linux 8+
|
||||
- Oracle Linux 8+
|
||||
- OpenSUSE Tubleweed
|
||||
- Amazon Linux 2023
|
||||
|
||||
## Arquitecturas y Dispositivos Compatibles
|
||||
|
||||
@@ -236,88 +280,103 @@ Nuestra plataforma ofrece compatibilidad con una amplia gama de arquitecturas y
|
||||
- Soporta exportar/importar base de datos desde el panel
|
||||
|
||||
|
||||
## Configuraciones por Defecto
|
||||
## Configuración Predeterminada del Panel
|
||||
|
||||
<details>
|
||||
<summary>Haz clic para detalles de las configuraciones por defecto</summary>
|
||||
<summary>Haz clic para ver los detalles de la configuración predeterminada</summary>
|
||||
|
||||
### Información
|
||||
### Nombre de usuario, Contraseña, Puerto y Ruta Base Web
|
||||
|
||||
Si elige no modificar estas configuraciones, se generarán aleatoriamente (esto no se aplica a Docker).
|
||||
|
||||
**Configuraciones predeterminadas para Docker:**
|
||||
- **Nombre de usuario:** admin
|
||||
- **Contraseña:** admin
|
||||
- **Puerto:** 2053
|
||||
- **Usuario y Contraseña:** Se generarán aleatoriamente si omites la modificación.
|
||||
|
||||
### Gestión de la Base de Datos:
|
||||
|
||||
Puedes realizar copias de seguridad y restauraciones de la base de datos directamente desde el panel.
|
||||
|
||||
- **Ruta de la Base de Datos:**
|
||||
- /etc/x-ui/x-ui.db
|
||||
- **Ruta de Configuración de Xray:**
|
||||
- /usr/local/x-ui/bin/config.json
|
||||
- **Ruta del Panel Web sin Implementar SSL:**
|
||||
- http://ip:2053/panel
|
||||
- http://domain:2053/panel
|
||||
- **Ruta del Panel Web con Implementación de SSL:**
|
||||
- https://domain:2053/panel
|
||||
|
||||
- `/etc/x-ui/x-ui.db`
|
||||
|
||||
### Ruta Base Web
|
||||
|
||||
1. **Restablecer la Ruta Base Web:**
|
||||
- Abre tu terminal.
|
||||
- Ejecuta el comando `x-ui`.
|
||||
- Selecciona la opción `Restablecer la Ruta Base Web`.
|
||||
|
||||
2. **Generar o Personalizar la Ruta:**
|
||||
- La ruta se generará aleatoriamente, o puedes ingresar una ruta personalizada.
|
||||
|
||||
3. **Ver Configuración Actual:**
|
||||
- Para ver tu configuración actual, utiliza el comando `x-ui settings` en el terminal o selecciona `Ver Configuración Actual` en `x-ui`.
|
||||
|
||||
### Recomendación de Seguridad:
|
||||
- Para mayor seguridad, utiliza una palabra larga y aleatoria en la estructura de tu URL.
|
||||
|
||||
**Ejemplos:**
|
||||
- `http://ip:port/*webbasepath*/panel`
|
||||
- `http://domain:port/*webbasepath*/panel`
|
||||
|
||||
</details>
|
||||
|
||||
## [Configuración WARP](https://gitlab.com/fscarmen/warp)
|
||||
## Configuración de WARP
|
||||
|
||||
<details>
|
||||
<summary>Haz clic para detalles de la configuración WARP</summary>
|
||||
<summary>Haz clic para ver los detalles de la configuración de WARP</summary>
|
||||
|
||||
#### Uso
|
||||
|
||||
Si deseas usar enrutamiento a WARP antes de la versión v2.1.0, sigue los pasos a continuación:
|
||||
**Para versiones `v2.1.0` y posteriores:**
|
||||
|
||||
**1.** Instala WARP en **Modo de Proxy SOCKS**:
|
||||
|
||||
```sh
|
||||
bash <(curl -sSL https://raw.githubusercontent.com/hamid-gh98/x-ui-scripts/main/install_warp_proxy.sh)
|
||||
```
|
||||
|
||||
**2.** Si ya instalaste warp, puedes desinstalarlo usando el siguiente comando:
|
||||
|
||||
```sh
|
||||
warp u
|
||||
```
|
||||
|
||||
**3.** Activa la configuración que necesites en el panel
|
||||
|
||||
Características de Configuración:
|
||||
|
||||
- Bloquear Anuncios
|
||||
- Enrutar Google + Netflix + Spotify + OpenAI (ChatGPT) a WARP
|
||||
- Corregir error 403 de Google
|
||||
WARP está integrado, no se requiere instalación adicional. Simplemente habilita la configuración necesaria en el panel.
|
||||
|
||||
</details>
|
||||
|
||||
## Límite de IP
|
||||
|
||||
<details>
|
||||
<summary>Haz clic para más detalles del límite de IP</summary>
|
||||
<summary>Haz clic para ver los detalles del límite de IP</summary>
|
||||
|
||||
#### Uso
|
||||
|
||||
**Nota:** El Límite de IP no funcionará correctamente cuando se use IP Tunnel
|
||||
|
||||
- Para versiones hasta `v1.6.1`:
|
||||
**Nota:** El Límite de IP no funcionará correctamente cuando uses Túnel IP.
|
||||
|
||||
- **Para versiones hasta `v1.6.1`:**
|
||||
- El límite de IP está integrado en el panel.
|
||||
|
||||
- Para versiones `v1.7.0` y posteriores:
|
||||
**Para versiones `v1.7.0` y posteriores:**
|
||||
|
||||
- Para que el Límite de IP funcione correctamente, necesitas instalar fail2ban y sus archivos requeridos siguiendo estos pasos:
|
||||
Para habilitar la funcionalidad de límite de IP, necesitas instalar `fail2ban` y los archivos requeridos siguiendo estos pasos:
|
||||
|
||||
1. Usa el comando `x-ui` dentro de la terminal.
|
||||
2. Selecciona `Gestión de Límite de IP`.
|
||||
3. Elige las opciones apropiadas según tus necesidades.
|
||||
|
||||
- asegúrate de tener ./access.log en tu Configuración de Xray después de la v2.1.3 tenemos una opción para ello
|
||||
|
||||
```sh
|
||||
1. Ejecuta el comando `x-ui` en el terminal, luego elige `Gestión de Límite de IP`.
|
||||
2. Verás las siguientes opciones:
|
||||
|
||||
- **Cambiar la Duración del Bloqueo:** Ajustar la duración de los bloqueos.
|
||||
- **Desbloquear a Todos:** Levantar todos los bloqueos actuales.
|
||||
- **Revisar los Registros:** Revisar los registros.
|
||||
- **Estado de Fail2ban:** Verificar el estado de `fail2ban`.
|
||||
- **Reiniciar Fail2ban:** Reiniciar el servicio `fail2ban`.
|
||||
- **Desinstalar Fail2ban:** Desinstalar Fail2ban con la configuración.
|
||||
|
||||
3. Agrega una ruta para el registro de acceso en el panel configurando `Xray Configs/log/Access log` a `./access.log`, luego guarda y reinicia Xray.
|
||||
|
||||
- **Para versiones anteriores a `v2.1.3`:**
|
||||
- Necesitas configurar manualmente la ruta del registro de acceso en tu configuración de Xray:
|
||||
|
||||
```sh
|
||||
"log": {
|
||||
"access": "./access.log",
|
||||
"dnsLog": false,
|
||||
"loglevel": "warning"
|
||||
},
|
||||
```
|
||||
```
|
||||
|
||||
- **Para versiones `v2.1.3` y posteriores:**
|
||||
- Hay una opción para configurar `access.log` directamente desde el panel.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -368,7 +427,7 @@ El panel web admite tráfico diario, inicio de sesión en el panel, copia de seg
|
||||
|
||||
- Inicia [Botfather](https://t.me/BotFather) en tu cuenta de Telegram:
|
||||

|
||||
|
||||
|
||||
- Crea un nuevo bot usando el comando /newbot: Te hará 2 preguntas, Un nombre y un nombre de usuario para tu bot. Ten en cuenta que el nombre de usuario debe terminar con la palabra "bot".
|
||||

|
||||
|
||||
@@ -393,28 +452,29 @@ Ingresa el ID de chat de usuario en el campo de entrada número 4. Las cuentas d
|
||||
|
||||
#### Uso
|
||||
|
||||
- [Documentación de API](https://documenter.getpostman.com/view/5146551/2sAXxP8Y12)
|
||||
- `/login` con `POST` datos de usuario: `{username: '', password: ''}` para iniciar sesión
|
||||
- `/panel/api/inbounds` base para las siguientes acciones:
|
||||
|
||||
| Método | Ruta | Acción |
|
||||
| :----: | ---------------------------------- | -------------------------------------------------------- |
|
||||
| `GET` | `"/list"` | Obtener todas los Entradas |
|
||||
| `GET` | `"/get/:id"` | Obtener Entrada con inbound.id |
|
||||
| `GET` | `"/getClientTraffics/:email"` | Obtener Tráficos del Cliente con email |
|
||||
| `GET` | `"/createbackup"` | El bot de Telegram envía copia de seguridad a los admins |
|
||||
| `POST` | `"/add"` | Agregar Entrada |
|
||||
| `POST` | `"/del/:id"` | Eliminar Entrada |
|
||||
| `POST` | `"/update/:id"` | Actualizar Entrada |
|
||||
| `POST` | `"/clientIps/:email"` | Dirección IP del Cliente |
|
||||
| `POST` | `"/clearClientIps/:email"` | Borrar Dirección IP del Cliente |
|
||||
| `POST` | `"/addClient"` | Agregar Cliente a la Entrada |
|
||||
| `POST` | `"/:id/delClient/:clientId"` | Eliminar Cliente por clientId\* |
|
||||
| `POST` | `"/updateClient/:clientId"` | Actualizar Cliente por clientId\* |
|
||||
| `POST` | `"/:id/resetClientTraffic/:email"` | Restablecer Tráfico del Cliente |
|
||||
| `POST` | `"/resetAllTraffics"` | Restablecer tráfico de todos las Entradas |
|
||||
| `POST` | `"/resetAllClientTraffics/:id"` | Restablecer tráfico de todos los clientes en una Entrada |
|
||||
| `POST` | `"/delDepletedClients/:id"` | Eliminar clientes agotados de la entrada (-1: todos) |
|
||||
| `POST` | `"/onlines"` | Obtener usuarios en línea (lista de correos electrónicos)|
|
||||
| Método | Ruta | Acción |
|
||||
| :----: | ---------------------------------- | --------------------------------------------------------- |
|
||||
| `GET` | `"/list"` | Obtener todas los Entradas |
|
||||
| `GET` | `"/get/:id"` | Obtener Entrada con inbound.id |
|
||||
| `GET` | `"/getClientTraffics/:email"` | Obtener Tráficos del Cliente con email |
|
||||
| `GET` | `"/createbackup"` | El bot de Telegram envía copia de seguridad a los admins |
|
||||
| `POST` | `"/add"` | Agregar Entrada |
|
||||
| `POST` | `"/del/:id"` | Eliminar Entrada |
|
||||
| `POST` | `"/update/:id"` | Actualizar Entrada |
|
||||
| `POST` | `"/clientIps/:email"` | Dirección IP del Cliente |
|
||||
| `POST` | `"/clearClientIps/:email"` | Borrar Dirección IP del Cliente |
|
||||
| `POST` | `"/addClient"` | Agregar Cliente a la Entrada |
|
||||
| `POST` | `"/:id/delClient/:clientId"` | Eliminar Cliente por clientId\* |
|
||||
| `POST` | `"/updateClient/:clientId"` | Actualizar Cliente por clientId\* |
|
||||
| `POST` | `"/:id/resetClientTraffic/:email"` | Restablecer Tráfico del Cliente |
|
||||
| `POST` | `"/resetAllTraffics"` | Restablecer tráfico de todos las Entradas |
|
||||
| `POST` | `"/resetAllClientTraffics/:id"` | Restablecer tráfico de todos los clientes en una Entrada |
|
||||
| `POST` | `"/delDepletedClients/:id"` | Eliminar clientes agotados de la entrada (-1: todos) |
|
||||
| `POST` | `"/onlines"` | Obtener usuarios en línea (lista de correos electrónicos) |
|
||||
|
||||
\*- El campo `clientId` debe llenarse por:
|
||||
|
||||
@@ -422,9 +482,7 @@ Ingresa el ID de chat de usuario en el campo de entrada número 4. Las cuentas d
|
||||
- `client.password` para TROJAN
|
||||
- `client.email` para Shadowsocks
|
||||
|
||||
|
||||
- [Documentación de API](https://documenter.getpostman.com/view/16802678/2s9YkgD5jm)
|
||||
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415%26entityType%3Dcollection%26workspaceId%3D2cd38c01-c851-4a15-a972-f181c23359d9)
|
||||
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://god.gw.postman.com/run-collection/5146551-e6aac565-e0e2-46df-acff-2607a51bbd04?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D5146551-e6aac565-e0e2-46df-acff-2607a51bbd04%26entityType%3Dcollection%26workspaceId%3Dd64f609f-485a-4951-9b8f-876b3f917124)
|
||||
</details>
|
||||
|
||||
## Variables de Entorno
|
||||
@@ -434,13 +492,13 @@ Ingresa el ID de chat de usuario en el campo de entrada número 4. Las cuentas d
|
||||
|
||||
#### Uso
|
||||
|
||||
| Variable | Tipo | Predeterminado|
|
||||
| -------------- | :--------------------------------------------: | :------------ |
|
||||
| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` |
|
||||
| XUI_DEBUG | `boolean` | `false` |
|
||||
| XUI_BIN_FOLDER | `string` | `"bin"` |
|
||||
| XUI_DB_FOLDER | `string` | `"/etc/x-ui"` |
|
||||
| XUI_LOG_FOLDER | `string` | `"/var/log"` |
|
||||
| Variable | Tipo | Predeterminado |
|
||||
| -------------- | :--------------------------------------------: | :------------- |
|
||||
| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` |
|
||||
| XUI_DEBUG | `boolean` | `false` |
|
||||
| XUI_BIN_FOLDER | `string` | `"bin"` |
|
||||
| XUI_DB_FOLDER | `string` | `"/etc/x-ui"` |
|
||||
| XUI_LOG_FOLDER | `string` | `"/var/log"` |
|
||||
|
||||
Ejemplo:
|
||||
|
||||
|
||||
273
README.md
@@ -1,6 +1,4 @@
|
||||
# 3X-UI
|
||||
|
||||
[English](/README.md) | [Chinese](/README.zh.md) | [Español](/README.es_ES.md)
|
||||
[English](/README.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
|
||||
|
||||
<p align="center"><a href="#"><img src="./media/3X-UI.png" alt="Image"></a></p>
|
||||
|
||||
@@ -16,9 +14,15 @@
|
||||
|
||||
**If this project is helpful to you, you may wish to give it a**:star2:
|
||||
|
||||
<p align="left"><a href="#"><img width="125" src="https://github.com/MHSanaei/3x-ui/assets/115543613/7aa895dd-048a-42e7-989b-afd41a74e2e1" alt="Image"></a></p>
|
||||
<p align="left">
|
||||
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
|
||||
<img src="./media/buymeacoffe.png" alt="Image">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
||||
|
||||
## Install & Upgrade
|
||||
|
||||
@@ -28,48 +32,59 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
|
||||
|
||||
## Install Custom Version
|
||||
|
||||
To install your desired version, add the version to the end of the installation command. e.g., ver `v2.3.0`:
|
||||
To install your desired version, add the version to the end of the installation command. e.g., ver `v2.4.3`:
|
||||
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.3.0
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.4.3
|
||||
```
|
||||
|
||||
## SSL Certificate
|
||||
|
||||
<details>
|
||||
<summary>Click for SSL Certificate</summary>
|
||||
<summary>Click for SSL Certificate details</summary>
|
||||
|
||||
### Cloudflare
|
||||
### ACME
|
||||
|
||||
The Management script has a built-in SSL certificate application for Cloudflare. To use this script to apply for a certificate, you need the following:
|
||||
To manage SSL certificates using ACME:
|
||||
|
||||
- Cloudflare registered email
|
||||
- Cloudflare Global API Key
|
||||
- The domain name has been resolved to the current server through cloudflare
|
||||
1. Ensure your domain is correctly resolved to the server.
|
||||
2. Run the `x-ui` command in the terminal, then choose `SSL Certificate Management`.
|
||||
3. You will be presented with the following options:
|
||||
|
||||
How to get the Cloudflare Global API Key:
|
||||
|
||||
1. Run the`x-ui`command on the terminal, then choose `Cloudflare SSL Certificate`.
|
||||
|
||||
2. Visit the link https://dash.cloudflare.com/profile/api-tokens
|
||||
|
||||
3. Click on View Global API Key (See the screenshot below)
|
||||

|
||||
|
||||
4. You may have to re-authenticate your account. After that, the API Key will be shown (See the screenshot below)\
|
||||

|
||||
|
||||
When using, just enter `domain name`, `email`, `API KEY`, the diagram is as follows:
|
||||

|
||||
- **Get SSL:** Obtain SSL certificates.
|
||||
- **Revoke:** Revoke existing SSL certificates.
|
||||
- **Force Renew:** Force renewal of SSL certificates.
|
||||
|
||||
### Certbot
|
||||
```
|
||||
|
||||
To install and use Certbot:
|
||||
|
||||
```sh
|
||||
apt-get install certbot -y
|
||||
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
|
||||
certbot renew --dry-run
|
||||
```
|
||||
|
||||
***Tip:*** *Certbot is also built into the Management script. You can run the `x-ui` command, then choose `SSL Certificate Management`.*
|
||||
### Cloudflare
|
||||
|
||||
The management script includes a built-in SSL certificate application for Cloudflare. To use this script to apply for a certificate, you need the following:
|
||||
|
||||
- Cloudflare registered email
|
||||
- Cloudflare Global API Key
|
||||
- The domain name must be resolved to the current server through Cloudflare
|
||||
|
||||
**How to get the Cloudflare Global API Key:**
|
||||
|
||||
1. Run the `x-ui` command in the terminal, then choose `Cloudflare SSL Certificate`.
|
||||
2. Visit the link: [Cloudflare API Tokens](https://dash.cloudflare.com/profile/api-tokens).
|
||||
3. Click on "View Global API Key" (see the screenshot below):
|
||||

|
||||
4. You may need to re-authenticate your account. After that, the API Key will be shown (see the screenshot below):
|
||||

|
||||
|
||||
When using, just enter your `domain name`, `email`, and `API KEY`. The diagram is as follows:
|
||||

|
||||
|
||||
|
||||
</details>
|
||||
|
||||
@@ -135,26 +150,28 @@ systemctl restart x-ui
|
||||
|
||||
#### Usage
|
||||
|
||||
1. Install Docker:
|
||||
1. **Install Docker:**
|
||||
|
||||
```sh
|
||||
bash <(curl -sSL https://get.docker.com)
|
||||
```
|
||||
|
||||
2. Clone the Project Repository:
|
||||
2. **Clone the Project Repository:**
|
||||
|
||||
```sh
|
||||
git clone https://github.com/MHSanaei/3x-ui.git
|
||||
cd 3x-ui
|
||||
```
|
||||
|
||||
3. Start the Service
|
||||
3. **Start the Service:**
|
||||
|
||||
```sh
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
OR
|
||||
Add ```--pull always``` flag to make docker automatically recreate container if a newer image is pulled. See https://docs.docker.com/reference/cli/docker/container/run/#pull for more info.
|
||||
|
||||
**OR**
|
||||
|
||||
```sh
|
||||
docker run -itd \
|
||||
@@ -167,40 +184,78 @@ systemctl restart x-ui
|
||||
ghcr.io/mhsanaei/3x-ui:latest
|
||||
```
|
||||
|
||||
update to latest version
|
||||
4. **Update to the Latest Version:**
|
||||
|
||||
```sh
|
||||
cd 3x-ui
|
||||
docker compose down
|
||||
docker compose pull 3x-ui
|
||||
docker compose up -d
|
||||
cd 3x-ui
|
||||
docker compose down
|
||||
docker compose pull 3x-ui
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
remove 3x-ui from docker
|
||||
5. **Remove 3x-ui from Docker:**
|
||||
|
||||
```sh
|
||||
docker stop 3x-ui
|
||||
docker rm 3x-ui
|
||||
cd --
|
||||
rm -r 3x-ui
|
||||
docker stop 3x-ui
|
||||
docker rm 3x-ui
|
||||
cd --
|
||||
rm -r 3x-ui
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Nginx Settings
|
||||
<details>
|
||||
<summary>Click for Reverse Proxy Configuration</summary>
|
||||
|
||||
#### Nginx Reverse Proxy
|
||||
```nginx
|
||||
location / {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Range $http_range;
|
||||
proxy_set_header If-Range $http_if_range;
|
||||
proxy_redirect off;
|
||||
proxy_pass http://127.0.0.1:2053;
|
||||
}
|
||||
```
|
||||
|
||||
#### Nginx sub-path
|
||||
- Ensure that the "URI Path" in the `/sub` panel settings is the same.
|
||||
- The `url` in the panel settings needs to end with `/`.
|
||||
|
||||
```nginx
|
||||
location /sub {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Range $http_range;
|
||||
proxy_set_header If-Range $http_if_range;
|
||||
proxy_redirect off;
|
||||
proxy_pass http://127.0.0.1:2053;
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
## Recommended OS
|
||||
|
||||
- Ubuntu 20.04+
|
||||
- Debian 11+
|
||||
- CentOS 8+
|
||||
- OpenEuler 22.03+
|
||||
- Fedora 36+
|
||||
- Arch Linux
|
||||
- Parch Linux
|
||||
- Manjaro
|
||||
- Armbian
|
||||
- AlmaLinux 9+
|
||||
- Rocky Linux 9+
|
||||
- AlmaLinux 8.0+
|
||||
- Rocky Linux 8+
|
||||
- Oracle Linux 8+
|
||||
- OpenSUSE Tubleweed
|
||||
- Amazon Linux 2023
|
||||
|
||||
## Supported Architectures and Devices
|
||||
|
||||
@@ -232,8 +287,10 @@ Our platform offers compatibility with a diverse range of architectures and devi
|
||||
- Russian
|
||||
- Vietnamese
|
||||
- Spanish
|
||||
- Indonesian
|
||||
- Indonesian
|
||||
- Ukrainian
|
||||
- Turkish
|
||||
- Português (Brazil)
|
||||
|
||||
|
||||
## Features
|
||||
@@ -242,7 +299,7 @@ Our platform offers compatibility with a diverse range of architectures and devi
|
||||
- Search within all inbounds and clients
|
||||
- Dark/Light theme
|
||||
- Supports multi-user and multi-protocol
|
||||
- Supports protocols, including VMess, VLESS, Trojan, Shadowsocks, Dokodemo-door, Socks, HTTP, wireguard
|
||||
- Supports protocols, including VMESS, VLESS, Trojan, Shadowsocks, Dokodemo-door, Socks, HTTP, wireguard
|
||||
- Supports XTLS native Protocols, including RPRX-Direct, Vision, REALITY
|
||||
- Traffic statistics, traffic limit, expiration time limit
|
||||
- Customizable Xray configuration templates
|
||||
@@ -254,55 +311,60 @@ Our platform offers compatibility with a diverse range of architectures and devi
|
||||
- Supports export/import database from the panel
|
||||
|
||||
|
||||
## Default Settings
|
||||
## Default Panel Settings
|
||||
|
||||
<details>
|
||||
<summary>Click for default settings details</summary>
|
||||
|
||||
### Information
|
||||
### Username, Password, Port, and Web Base Path
|
||||
|
||||
If you choose not to modify these settings, they will be generated randomly (this does not apply to Docker).
|
||||
|
||||
**Default Settings for Docker:**
|
||||
- **Username:** admin
|
||||
- **Password:** admin
|
||||
- **Port:** 2053
|
||||
- **Username & Password:** It will be generated randomly if you skip modifying.
|
||||
|
||||
### Database Management:
|
||||
|
||||
You can conveniently perform database Backups and Restores directly from the panel.
|
||||
|
||||
- **Database Path:**
|
||||
- /etc/x-ui/x-ui.db
|
||||
- **Xray Config Path:**
|
||||
- /usr/local/x-ui/bin/config.json
|
||||
- **Web Panel Path w/o Deploying SSL:**
|
||||
- http://ip:2053/panel
|
||||
- http://domain:2053/panel
|
||||
- **Web Panel Path w/ Deploying SSL:**
|
||||
- https://domain:2053/panel
|
||||
|
||||
- `/etc/x-ui/x-ui.db`
|
||||
|
||||
|
||||
### Web Base Path
|
||||
|
||||
1. **Reset Web Base Path:**
|
||||
- Open your terminal.
|
||||
- Run the `x-ui` command.
|
||||
- Select the option to `Reset Web Base Path`.
|
||||
|
||||
2. **Generate or Customize Path:**
|
||||
- The path will be randomly generated, or you can enter a custom path.
|
||||
|
||||
3. **View Current Settings:**
|
||||
- To view your current settings, use the `x-ui settings` command in the terminal or `View Current Settings` in `x-ui`
|
||||
|
||||
### Security Recommendation:
|
||||
- For enhanced security, use a long, random word in your URL structure.
|
||||
|
||||
**Examples:**
|
||||
- `http://ip:port/*webbasepath*/panel`
|
||||
- `http://domain:port/*webbasepath*/panel`
|
||||
|
||||
</details>
|
||||
|
||||
## [WARP Configuration](https://gitlab.com/fscarmen/warp)
|
||||
## WARP Configuration
|
||||
|
||||
<details>
|
||||
<summary>Click for WARP configuration details</summary>
|
||||
|
||||
#### Usage
|
||||
|
||||
If you want to use routing to WARP before v2.1.0 follow steps as below:
|
||||
**For versions `v2.1.0` and later:**
|
||||
|
||||
**1.** Install WARP on **SOCKS Proxy Mode**:
|
||||
|
||||
```sh
|
||||
bash <(curl -sSL https://raw.githubusercontent.com/hamid-gh98/x-ui-scripts/main/install_warp_proxy.sh)
|
||||
```
|
||||
|
||||
**2.** If you already installed warp, you can uninstall using below command:
|
||||
|
||||
```sh
|
||||
warp u
|
||||
```
|
||||
|
||||
**3.** Turn on the config you need in panel
|
||||
|
||||
Config Features:
|
||||
|
||||
- Block Ads
|
||||
- Route Google + Netflix + Spotify + OpenAI (ChatGPT) to WARP
|
||||
- Fix Google 403 error
|
||||
WARP is built-in, and no additional installation is required. Simply turn on the necessary configuration in the panel.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -313,29 +375,40 @@ If you want to use routing to WARP before v2.1.0 follow steps as below:
|
||||
|
||||
#### Usage
|
||||
|
||||
**Note:** IP Limit won't work correctly when using IP Tunnel
|
||||
**Note:** IP Limit won't work correctly when using IP Tunnel.
|
||||
|
||||
- For versions up to `v1.6.1`:
|
||||
- **For versions up to `v1.6.1`:**
|
||||
- The IP limit is built-in to the panel
|
||||
|
||||
- IP limit is built-in into the panel.
|
||||
**For versions `v1.7.0` and newer:**
|
||||
|
||||
- For versions `v1.7.0` and newer:
|
||||
To enable the IP Limit functionality, you need to install `fail2ban` and its required files by following these steps:
|
||||
|
||||
- To make IP Limit work properly, you need to install fail2ban and its required files by following these steps:
|
||||
1. Run the `x-ui` command in the terminal, then choose `IP Limit Management`.
|
||||
2. You will see the following options:
|
||||
|
||||
1. Use the `x-ui` command inside the shell.
|
||||
2. Select `IP Limit Management`.
|
||||
3. Choose the appropriate options based on your needs.
|
||||
|
||||
- make sure you have ./access.log on your Xray Configuration after v2.1.3 we have an option for it
|
||||
|
||||
```sh
|
||||
- **Change Ban Duration:** Adjust the duration of bans.
|
||||
- **Unban Everyone:** Lift all current bans.
|
||||
- **Check Logs:** Review the logs.
|
||||
- **Fail2ban Status:** Check the status of `fail2ban`.
|
||||
- **Restart Fail2ban:** Restart the `fail2ban` service.
|
||||
- **Uninstall Fail2ban:** Uninstall Fail2ban with configuration.
|
||||
|
||||
3. Add a path for the access log on the panel by setting `Xray Configs/log/Access log` to `./access.log` then save and restart xray.
|
||||
|
||||
- **For versions before `v2.1.3`:**
|
||||
- You need to set the access log path manually in your Xray configuration:
|
||||
|
||||
```sh
|
||||
"log": {
|
||||
"access": "./access.log",
|
||||
"dnsLog": false,
|
||||
"loglevel": "warning"
|
||||
},
|
||||
```
|
||||
```
|
||||
|
||||
- **For versions `v2.1.3` and newer:**
|
||||
- There is an option for configuring `access.log` directly from the panel.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -374,19 +447,19 @@ The web panel supports daily traffic, panel login, database backup, system statu
|
||||
- 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 telegram traffic report searched with UUID (VMESS/VLESS) or Password (TROJAN) - anonymously
|
||||
- Menu based bot
|
||||
- Search client by email ( only admin )
|
||||
- Menu-based bot
|
||||
- Search client by email (only admin)
|
||||
- Check all inbounds
|
||||
- Check server status
|
||||
- Check depleted users
|
||||
- Receive backup by request and in periodic reports
|
||||
- Multi language bot
|
||||
- Multi-language bot
|
||||
|
||||
### Setting up Telegram bot
|
||||
|
||||
- Start [Botfather](https://t.me/BotFather) in your Telegram account:
|
||||

|
||||
|
||||
|
||||
- Create a new Bot using /newbot command: It will ask you 2 questions, A name and a username for your bot. Note that the username has to end with the word "bot".
|
||||

|
||||
|
||||
@@ -411,6 +484,7 @@ Enter the user ID in input field number 4. The Telegram accounts with this id wi
|
||||
|
||||
#### Usage
|
||||
|
||||
- [API Documentation](https://documenter.getpostman.com/view/5146551/2sAXxP8Y12)
|
||||
- `/login` with `POST` user data: `{username: '', password: ''}` for login
|
||||
- `/panel/api/inbounds` base for following actions:
|
||||
|
||||
@@ -419,6 +493,7 @@ Enter the user ID in input field number 4. The Telegram accounts with this id wi
|
||||
| `GET` | `"/list"` | Get all inbounds |
|
||||
| `GET` | `"/get/:id"` | Get inbound with inbound.id |
|
||||
| `GET` | `"/getClientTraffics/:email"` | Get Client Traffics with email |
|
||||
| `GET` | `"/getClientTrafficsById/:id"` | Get client's traffic By ID |
|
||||
| `GET` | `"/createbackup"` | Telegram bot sends backup to admins |
|
||||
| `POST` | `"/add"` | Add inbound |
|
||||
| `POST` | `"/del/:id"` | Delete Inbound |
|
||||
@@ -432,7 +507,7 @@ Enter the user ID in input field number 4. The Telegram accounts with this id wi
|
||||
| `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) |
|
||||
| `POST` | `"/onlines"` | Get Online users ( list of emails ) |
|
||||
| `POST` | `"/onlines"` | Get Online users ( list of emails ) |
|
||||
|
||||
\*- The field `clientId` should be filled by:
|
||||
|
||||
@@ -440,9 +515,7 @@ Enter the user ID in input field number 4. The Telegram accounts with this id wi
|
||||
- `client.password` for TROJAN
|
||||
- `client.email` for Shadowsocks
|
||||
|
||||
|
||||
- [API Documentation](https://documenter.getpostman.com/view/16802678/2s9YkgD5jm)
|
||||
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415%26entityType%3Dcollection%26workspaceId%3D2cd38c01-c851-4a15-a972-f181c23359d9)
|
||||
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://god.gw.postman.com/run-collection/5146551-e6aac565-e0e2-46df-acff-2607a51bbd04?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D5146551-e6aac565-e0e2-46df-acff-2607a51bbd04%26entityType%3Dcollection%26workspaceId%3Dd64f609f-485a-4951-9b8f-876b3f917124)
|
||||
</details>
|
||||
|
||||
## Environment Variables
|
||||
|
||||
563
README.ru_RU.md
Normal file
@@ -0,0 +1,563 @@
|
||||
[English](/README.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
|
||||
|
||||
<p align="center"><a href="#"><img src="./media/3X-UI.png" alt="Image"></a></p>
|
||||
|
||||
**Продвинутая веб-панель • Построена на основе Xray Core**
|
||||
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
[](#)
|
||||
[](#)
|
||||
[](#)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
> **Отказ от ответственности:** Этот проект предназначен только для личного обучения и общения. Пожалуйста, не используйте его в незаконных целях и не применяйте в производственной среде.
|
||||
|
||||
**Если этот проект оказался полезным для вас, вы можете оценить его, поставив звёздочку** :star2:
|
||||
|
||||
<p align="left">
|
||||
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
|
||||
<img src="./media/buymeacoffe.png" alt="Image">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
||||
|
||||
## Установка и обновление
|
||||
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
|
||||
```
|
||||
|
||||
## Установка определённой версии
|
||||
|
||||
Чтобы установить нужную вам версию, добавьте номер версии в конец команды установки. Например, `v2.4.3`:
|
||||
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.4.3
|
||||
```
|
||||
|
||||
## SSL Сертификат
|
||||
|
||||
<details>
|
||||
<summary>Нажмите для получения информации об SSL сертификате</summary>
|
||||
|
||||
### ACME
|
||||
|
||||
Для управления SSL сертификатами с помощью ACME:
|
||||
|
||||
1. Убедитесь, что ваш домен правильно настроен и указывает на сервер.
|
||||
2. Выполните команду `x-ui` в терминале, затем выберите `SSL Certificate Management`.
|
||||
3. Вам будут предложены следующие опции:
|
||||
|
||||
- **Get SSL:** Получить SSL сертификаты.
|
||||
- **Revoke:** Отозвать существующие SSL сертификаты.
|
||||
- **Force Renew:** Принудительно перевыпустить SSL сертификаты.
|
||||
|
||||
### Certbot
|
||||
|
||||
Для установки и использования Certbot:
|
||||
|
||||
```sh
|
||||
apt-get install certbot -y
|
||||
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d вашдомен.com
|
||||
certbot renew --dry-run
|
||||
```
|
||||
|
||||
### Cloudflare
|
||||
|
||||
Скрипт управления включает встроенное приложение для получения SSL сертификата через Cloudflare. Чтобы использовать этот скрипт для запроса сертификата, вам потребуется следующее:
|
||||
|
||||
- Email, зарегистрированный в Cloudflare
|
||||
- Глобальный API-ключ Cloudflare
|
||||
- Доменное имя должно указывать на текущий сервер через Cloudflare
|
||||
|
||||
**Как получить глобальный API-ключ Cloudflare:**
|
||||
|
||||
1. Выполните команду `x-ui` в терминале, затем выберите `Cloudflare SSL Certificate`.
|
||||
2. Перейдите по ссылке: [Cloudflare API Tokens](https://dash.cloudflare.com/profile/api-tokens).
|
||||
3. Нажмите на "View Global API Key" (см. скриншот ниже):
|
||||

|
||||
4. Возможно, вам потребуется повторно пройти аутентификацию. После этого ключ API будет отображён (см. скриншот ниже):
|
||||

|
||||
|
||||
При использовании просто введите ваше `доменное имя`, `email` и `API-ключ`. Схема приведена ниже:
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
## Ручная установка и обновление
|
||||
|
||||
<details>
|
||||
<summary>Нажмите для получения информации о ручной установке</summary>
|
||||
|
||||
#### Использование
|
||||
|
||||
1. Чтобы скачать последнюю версию архива напрямую на ваш сервер, выполните следующую команду:
|
||||
|
||||
```sh
|
||||
ARCH=$(uname -m)
|
||||
case "${ARCH}" in
|
||||
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
|
||||
i*86 | x86) XUI_ARCH="386" ;;
|
||||
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
|
||||
armv7* | armv7) XUI_ARCH="armv7" ;;
|
||||
armv6* | armv6) XUI_ARCH="armv6" ;;
|
||||
armv5* | armv5) XUI_ARCH="armv5" ;;
|
||||
s390x) echo 's390x' ;;
|
||||
*) XUI_ARCH="amd64" ;;
|
||||
esac
|
||||
|
||||
|
||||
wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-${XUI_ARCH}.tar.gz
|
||||
```
|
||||
|
||||
2. После загрузки архива выполните следующие команды для установки или обновления x-ui:
|
||||
|
||||
```sh
|
||||
ARCH=$(uname -m)
|
||||
case "${ARCH}" in
|
||||
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
|
||||
i*86 | x86) XUI_ARCH="386" ;;
|
||||
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
|
||||
armv7* | armv7) XUI_ARCH="armv7" ;;
|
||||
armv6* | armv6) XUI_ARCH="armv6" ;;
|
||||
armv5* | armv5) XUI_ARCH="armv5" ;;
|
||||
s390x) echo 's390x' ;;
|
||||
*) XUI_ARCH="amd64" ;;
|
||||
esac
|
||||
|
||||
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>
|
||||
|
||||
## Установка с помощью Docker
|
||||
|
||||
<details>
|
||||
<summary>Нажмите для получения информации о Docker</summary>
|
||||
|
||||
#### Использование
|
||||
|
||||
1. **Установите Docker:**
|
||||
|
||||
```sh
|
||||
bash <(curl -sSL https://get.docker.com)
|
||||
```
|
||||
|
||||
2. **Склонируйте репозиторий проекта:**
|
||||
|
||||
```sh
|
||||
git clone https://github.com/MHSanaei/3x-ui.git
|
||||
cd 3x-ui
|
||||
```
|
||||
|
||||
3. **Запустите сервис:**
|
||||
|
||||
```sh
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Добавьте параметр ```--pull always``` для автоматического обновления контейнера, когда публикуется новый образ. Подробности: https://docs.docker.com/reference/cli/docker/container/run/#pull
|
||||
|
||||
**ИЛИ**
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
4. **Обновление до последней версии:**
|
||||
|
||||
```sh
|
||||
cd 3x-ui
|
||||
docker compose down
|
||||
docker compose pull 3x-ui
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
5. **Удаление 3x-ui из Docker:**
|
||||
|
||||
```sh
|
||||
docker stop 3x-ui
|
||||
docker rm 3x-ui
|
||||
cd --
|
||||
rm -r 3x-ui
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Настройки Nginx
|
||||
<details>
|
||||
<summary>Нажмите чтобы просмотреть конфигурацию обратного прокси-сервера</summary>
|
||||
|
||||
#### Обратный прокси-сервер Nginx
|
||||
```nginx
|
||||
location / {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Range $http_range;
|
||||
proxy_set_header If-Range $http_if_range;
|
||||
proxy_redirect off;
|
||||
proxy_pass http://127.0.0.1:2053;
|
||||
}
|
||||
```
|
||||
|
||||
#### Nginx sub-path
|
||||
- Убедитесь, что "корневой путь URL адреса панели" в настройках панели и `/sub` совпадают.
|
||||
- В настройках панели `url` должен заканчиваться на `/`.
|
||||
|
||||
```nginx
|
||||
location /sub {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Range $http_range;
|
||||
proxy_set_header If-Range $http_if_range;
|
||||
proxy_redirect off;
|
||||
proxy_pass http://127.0.0.1:2053;
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
## Рекомендуемые ОС
|
||||
|
||||
- Ubuntu 20.04+
|
||||
- Debian 11+
|
||||
- CentOS 8+
|
||||
- OpenEuler 22.03+
|
||||
- Fedora 36+
|
||||
- Arch Linux
|
||||
- Parch Linux
|
||||
- Manjaro
|
||||
- Armbian
|
||||
- AlmaLinux 8.0+
|
||||
- Rocky Linux 8+
|
||||
- Oracle Linux 8+
|
||||
- OpenSUSE Tubleweed
|
||||
- Amazon Linux 2023
|
||||
|
||||
## Поддерживаемые архитектуры и устройства
|
||||
|
||||
<details>
|
||||
<summary>Нажмите для получения информации о поддерживаемых архитектурах и устройствах</summary>
|
||||
|
||||
Наша платформа поддерживает разнообразные архитектуры и устройства, обеспечивая гибкость в различных вычислительных средах. Вот основные архитектуры, которые мы поддерживаем:
|
||||
|
||||
- **amd64**: Эта распространенная архитектура является стандартом для персональных компьютеров и серверов, обеспечивая беспроблемную работу большинства современных операционных систем.
|
||||
|
||||
- **x86 / i386**: Широко используется в настольных и портативных компьютерах. Эта архитектура имеет широкую поддержку со стороны множества операционных систем и приложений, включая, но не ограничиваясь, Windows, macOS и Linux.
|
||||
|
||||
- **armv8 / arm64 / aarch64**: Предназначена для современных мобильных и встроенных устройств, таких как смартфоны и планшеты. Эта архитектура представлена устройствами, такими как Raspberry Pi 4, Raspberry Pi 3, Raspberry Pi Zero 2/Zero 2 W, Orange Pi 3 LTS и другими.
|
||||
|
||||
- **armv7 / arm / arm32**: Служит архитектурой для старых мобильных и встроенных устройств, оставаясь широко используемой в таких устройствах, как Orange Pi Zero LTS, Orange Pi PC Plus, Raspberry Pi 2 и других.
|
||||
|
||||
- **armv6 / arm / arm32**: Ориентирована на очень старые встроенные устройства, эта архитектура, хотя и менее распространенная, всё ещё используется. Например, такие устройства, как Raspberry Pi 1, Raspberry Pi Zero/Zero W, полагаются на эту архитектуру.
|
||||
|
||||
- **armv5 / arm / arm32**: Более старая архитектура, ассоциируемая с ранними встроенными системами, сегодня менее распространена, но всё ещё может быть найдена в устаревших устройствах, таких как ранние версии Raspberry Pi и некоторые старые смартфоны.
|
||||
|
||||
- **s390x**: Эта архитектура обычно используется в мейнфреймах IBM и обеспечивает высокую производительность и надежность для корпоративных рабочих нагрузок.
|
||||
</details>
|
||||
|
||||
## Языки
|
||||
|
||||
- Английский
|
||||
- Фарси
|
||||
- Китайский
|
||||
- Русский
|
||||
- Вьетнамский
|
||||
- Испанский
|
||||
- Индонезийский
|
||||
- Украинский
|
||||
- Турецкий
|
||||
- Португальский (Бразилия)
|
||||
|
||||
## Возможности
|
||||
|
||||
- Мониторинг состояния системы
|
||||
- Поиск по всем входящим подключениям и клиентам
|
||||
- Тёмная/светлая тема
|
||||
- Поддержка нескольких пользователей и протоколов
|
||||
- Поддержка протоколов, включая VMESS, VLESS, Trojan, Shadowsocks, Dokodemo-door, Socks, HTTP, WireGuard
|
||||
- Поддержка протоколов XTLS, включая RPRX-Direct, Vision, REALITY
|
||||
- Статистика трафика, ограничение трафика, ограничение по времени истечения
|
||||
- Настраиваемые шаблоны конфигурации Xray
|
||||
- Поддержка HTTPS доступа к панели (ваше доменное имя + SSL сертификат)
|
||||
- Поддержка установки SSL-сертификата в один клик и автоматического перевыпуска
|
||||
- Для получения более продвинутых настроек обращайтесь к панели
|
||||
- Исправляет маршруты API (настройка пользователя будет создана через API)
|
||||
- Поддержка изменения конфигураций по различным элементам, предоставленным в панели
|
||||
- Поддержка экспорта/импорта базы данных из панели
|
||||
|
||||
## Настройки панели по умолчанию
|
||||
|
||||
<details>
|
||||
<summary>Нажмите для получения информации о настройках по умолчанию</summary>
|
||||
|
||||
### Имя пользователя, Пароль, Порт и Web Base Path
|
||||
|
||||
Если вы не измените эти настройки, они будут сгенерированы случайным образом (это не относится к Docker).
|
||||
|
||||
**Настройки по умолчанию для Docker:**
|
||||
- **Имя пользователя:** admin
|
||||
- **Пароль:** admin
|
||||
- **Порт:** 2053
|
||||
|
||||
### Управление базой данных:
|
||||
|
||||
Вы можете удобно выполнять резервное копирование и восстановление базы данных прямо из панели.
|
||||
|
||||
- **Путь к базе данных:**
|
||||
- `/etc/x-ui/x-ui.db`
|
||||
|
||||
### Webbasepath
|
||||
|
||||
1. **Сбросить webbasepath:**
|
||||
- Откройте терминал.
|
||||
- Выполните команду `x-ui`.
|
||||
- Выберите опцию `Reset Web Base Path`.
|
||||
|
||||
2. **Генерация или настройка пути:**
|
||||
- Путь будет сгенерирован случайным образом, или вы можете ввести собственный путь.
|
||||
|
||||
3. **Просмотр текущих настроек:**
|
||||
- Чтобы просмотреть текущие настройки, используйте команду `x-ui settings` в терминале или опцию `View Current Settings` в `x-ui`.
|
||||
|
||||
### Рекомендации по безопасности:
|
||||
- Для повышения безопасности используйте длинное случайное слово в структуре вашего URL.
|
||||
|
||||
**Примеры:**
|
||||
- `http://ip_адрес:порт/*webbasepath*/panel`
|
||||
- `http://домен:порт/*webbasepath*/panel`
|
||||
|
||||
</details>
|
||||
|
||||
## Настройка WARP
|
||||
|
||||
<details>
|
||||
<summary>Нажмите для получения информации о настройке WARP</summary>
|
||||
|
||||
#### Использование
|
||||
|
||||
**Для версий `v2.1.0` и новее:**
|
||||
|
||||
WARP встроен, и дополнительная установка не требуется. Просто включите необходимую конфигурацию в панели.
|
||||
|
||||
</details>
|
||||
|
||||
## Ограничение IP
|
||||
|
||||
<details>
|
||||
<summary>Нажмите для получения информации об ограничении IP</summary>
|
||||
|
||||
#### Использование
|
||||
|
||||
**Примечание:** Ограничение IP не будет работать корректно при использовании IP Tunnel.
|
||||
|
||||
- **Для версий до `v1.6.1`:**
|
||||
- Ограничение IP встроено в панель.
|
||||
|
||||
**Для версий `v1.7.0` и новее:**
|
||||
|
||||
Чтобы включить функциональность ограничения IP, вам нужно установить `fail2ban` и его необходимые файлы, выполнив следующие шаги:
|
||||
|
||||
1. Выполните команду `x-ui` в терминале, затем выберите `IP Limit Management`.
|
||||
2. Вам будут предложены следующие опции:
|
||||
|
||||
- **Change Ban Duration:** Отрегулировать длительность блокировок.
|
||||
- **Unban Everyone:** Снять все текущие блокировки.
|
||||
- **Check Logs:** Просмотреть логи.
|
||||
- **Fail2ban Status:** Проверить статус `fail2ban`.
|
||||
- **Restart Fail2ban:** Перезапустить службу `fail2ban`.
|
||||
- **Uninstall Fail2ban:** Удалить Fail2ban с его конфигурацией.
|
||||
|
||||
3. Добавьте путь к логам доступа в панели, установив `Xray Configs/log/Access log` в `./access.log`, затем сохраните и перезапустите xray.
|
||||
|
||||
- **Для версий до `v2.1.3`:**
|
||||
- Вам нужно вручную установить путь к логам доступа в вашей конфигурации Xray:
|
||||
|
||||
```sh
|
||||
"log": {
|
||||
"access": "./access.log",
|
||||
"dnsLog": false,
|
||||
"loglevel": "warning"
|
||||
},
|
||||
```
|
||||
|
||||
- **Для версий `v2.1.3` и новее:**
|
||||
- Есть возможность настройки `access.log` непосредственно из панели.
|
||||
|
||||
</details>
|
||||
|
||||
## Телеграм-бот
|
||||
|
||||
<details>
|
||||
<summary>Нажмите для получения информации о телеграм-боте</summary>
|
||||
|
||||
#### Использование
|
||||
|
||||
Веб-панель поддерживает уведомления и функции, такие как ежедневный трафик, вход в панель, резервное копирование базы данных, состояние системы, информация о клиентах и другие, через телеграм-бота. Чтобы использовать бота, вам нужно настроить параметры, связанные с ботом, в панели, включая:
|
||||
|
||||
- Токен Telegram
|
||||
- ID чата админа(-ов)
|
||||
- Время уведомлений (в синтаксисе cron)
|
||||
- Уведомления о дате истечения
|
||||
- Уведомления о лимите трафика
|
||||
- Резервное копирование базы данных
|
||||
- Уведомления о загрузке CPU
|
||||
|
||||
**Примеры синтаксиса:**
|
||||
|
||||
- `30 * * * * *` - Уведомлять на 30-й секунде каждого часа
|
||||
- `0 */10 * * * *` - Уведомлять на первой секунде каждых 10 минут
|
||||
- `@hourly` - Ежечасное уведомление
|
||||
- `@daily` - Ежедневное уведомление (в 00:00)
|
||||
- `@weekly` - Еженедельное уведомление
|
||||
- `@every 8h` - Уведомлять каждые 8 часов
|
||||
|
||||
### Возможности телеграм-бота
|
||||
|
||||
- Периодические отчеты
|
||||
- Уведомления о входе
|
||||
- Уведомления о пороге загруженности процессора
|
||||
- Уведомления о времени истечения и трафике заранее
|
||||
- Поддерживает меню отчетов клиента, если имя пользователя телеграм клиента добавлено в конфигурации пользователя
|
||||
- Поддержка отчета о трафике через Telegram, поиск по UUID (VMESS/VLESS) или паролю (TROJAN) - анонимно
|
||||
- Бот, основанный на меню
|
||||
- Поиск клиента по email (только администратор)
|
||||
- Проверка всех входящих соединений
|
||||
- Проверка состояния сервера
|
||||
- Проверка истекших пользователей
|
||||
- Получение резервных копий по запросу и в периодических отчётах
|
||||
- Многоязычный бот
|
||||
|
||||
### Настройка телеграм-бота
|
||||
|
||||
- Запустите [Botfather](https://t.me/BotFather) в вашем аккаунте Telegram:
|
||||

|
||||
|
||||
- Создайте нового бота с помощью команды /newbot: у вас спросят 2 вопроса: отображаемое имя и имя пользователя для вашего бота. Обратите внимание, что имя пользователя должно заканчиваться на слово "bot".
|
||||

|
||||
|
||||
- Запустите созданного бота. Ссылку на вашего бота можно найти здесь.
|
||||

|
||||
|
||||
- Перейдите в панель и настройте параметры телеграм-бота следующим образом:
|
||||

|
||||
|
||||
Введите токен вашего бота в поле ввода номер 3.
|
||||
Введите ID пользователя в поле ввода номер 4. Telegram-аккаунты с этим ID будут администраторами бота. (Вы можете ввести несколько ID, разделяя их запятой)
|
||||
|
||||
- Как получить ID пользователя Telegram? Используйте этот [бот](https://t.me/useridinfobot). Запустите бота, и он отобразит ваш ID пользователя Telegram.
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
## Маршруты API
|
||||
|
||||
<details>
|
||||
<summary>Нажмите для получения информации о маршрутах API</summary>
|
||||
|
||||
#### Использование
|
||||
|
||||
- [API документация](https://documenter.getpostman.com/view/5146551/2sAXxP8Y12)
|
||||
- `/login` с `POST`-данными: `{username: '', password: ''}` для входа
|
||||
- `/panel/api/inbounds` это базовый путь для следующих действий:
|
||||
|
||||
| Метод | Путь | Действие
|
||||
| :----: | -----------------------------------| -------------------------------------------
|
||||
| `GET` | `"/list"` | Получить все входящие соединения
|
||||
| `GET` | `"/get/:id"` | Получить входящее соединение с inbound.id
|
||||
| `GET` | `"/getClientTraffics/:email"` | Получить трафик клиента по email
|
||||
| `GET` | `"/getClientTrafficsById/:id"` | Получить трафик клиента по ID
|
||||
| `GET` | `"/createbackup"` | Telegram-бот отправит резервную копию администраторам
|
||||
| `POST` | `"/add"` | Добавить входящее соединение
|
||||
| `POST` | `"/del/:id"` | Удалить входящее соединение
|
||||
| `POST` | `"/update/:id"` | Обновить входящее соединение
|
||||
| `POST` | `"/clientIps/:email"` | IP-адрес клиента
|
||||
| `POST` | `"/clearClientIps/:email"` | Очистить IP-адреса клиента
|
||||
| `POST` | `"/addClient"` | Добавить клиента к входящему соединению
|
||||
| `POST` | `"/:id/delClient/:clientId"` | Удалить клиента по clientId\*
|
||||
| `POST` | `"/updateClient/:clientId"` | Обновить клиента по clientId\*
|
||||
| `POST` | `"/:id/resetClientTraffic/:email"` | Сбросить трафик клиента
|
||||
| `POST` | `"/resetAllTraffics"` | Сбросить трафик всех входящих соединений
|
||||
| `POST` | `"/resetAllClientTraffics/:id"` | Сбросить трафик всех клиентов в входящем соединении
|
||||
| `POST` | `"/delDepletedClients/:id"` | Удалить истекших клиентов в входящем соединении (-1: всех)
|
||||
| `POST` | `"/onlines"` | Получить пользователей, которые находятся онлайн (список email'ов)
|
||||
|
||||
\*- Поле `clientId` должно быть заполнено следующим образом:
|
||||
|
||||
- `client.id` для VMESS и VLESS
|
||||
- `client.password` для TROJAN
|
||||
- `client.email` для Shadowsocks
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://god.gw.postman.com/run-collection/5146551-e6aac565-e0e2-46df-acff-2607a51bbd04?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D5146551-e6aac565-e0e2-46df-acff-2607a51bbd04%26entityType%3Dcollection%26workspaceId%3Dd64f609f-485a-4951-9b8f-876b3f917124)
|
||||
</details>
|
||||
|
||||
## Переменные среды
|
||||
|
||||
<details>
|
||||
<summary>Нажмите для получения информации о переменных среды</summary>
|
||||
|
||||
#### Использование
|
||||
|
||||
| Переменная | Тип | Значение по умолчанию |
|
||||
| ---------------- | :------------------------------------------: | :-------------------- |
|
||||
| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` |
|
||||
| XUI_DEBUG | `boolean` | `false` |
|
||||
| XUI_BIN_FOLDER | `string` | `"bin"` |
|
||||
| XUI_DB_FOLDER | `string` | `"/etc/x-ui"` |
|
||||
| XUI_LOG_FOLDER | `string` | `"/var/log"` |
|
||||
|
||||
Пример:
|
||||
|
||||
```sh
|
||||
XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Предварительный Просмотр
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## Особая благодарность
|
||||
|
||||
- [alireza0](https://github.com/alireza0/)
|
||||
|
||||
## Благодарности
|
||||
|
||||
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (License: **GPL-3.0**): _Enhanced v2ray/xray and v2ray/xray-clients routing rules with built-in Iranian domains and a focus on security and adblocking._
|
||||
- [Vietnam Adblock rules](https://github.com/vuong2023/vn-v2ray-rules) (License: **GPL-3.0**): _A hosted domain hosted in Vietnam and blocklist with the most efficiency for Vietnamese._
|
||||
|
||||
## Число звёзд со временем
|
||||
|
||||
[](https://starchart.cc/MHSanaei/3x-ui)
|
||||
@@ -1,6 +1,4 @@
|
||||
# 3X-UI
|
||||
|
||||
[English](/README.md) | [Chinese](/README.zh.md) | [Español](/README.es_ES.md)
|
||||
[English](/README.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
|
||||
|
||||
<p align="center"><a href="#"><img src="./media/3X-UI.png" alt="Image"></a></p>
|
||||
|
||||
@@ -16,9 +14,15 @@
|
||||
|
||||
**如果此项目对你有用,请给一个**:star2:
|
||||
|
||||
<p align="left"><a href="#"><img width="125" src="https://github.com/MHSanaei/3x-ui/assets/115543613/7aa895dd-048a-42e7-989b-afd41a74e2e1" alt="Image"></a></p>
|
||||
<p align="left">
|
||||
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
|
||||
<img src="./media/buymeacoffe.png" alt="Image">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
||||
|
||||
## 安装 & 升级
|
||||
|
||||
@@ -28,36 +32,58 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
|
||||
|
||||
## 安装指定版本
|
||||
|
||||
要安装所需的版本,请将该版本添加到安装命令的末尾。 e.g., ver `v2.2.8`:
|
||||
要安装所需的版本,请将该版本添加到安装命令的末尾。 e.g., ver `v2.4.3`:
|
||||
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.2.8
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.4.3
|
||||
```
|
||||
|
||||
## SSL 认证
|
||||
### SSL证书
|
||||
|
||||
<details>
|
||||
<summary>点击查看 SSL 认证</summary>
|
||||
<summary>点击查看SSL证书详情</summary>
|
||||
|
||||
### Cloudflare
|
||||
### ACME
|
||||
|
||||
管理脚本具有用于 Cloudflare 的内置 SSL 证书应用程序。若要使用此脚本申请证书,需要满足以下条件:
|
||||
使用ACME管理SSL证书:
|
||||
|
||||
- Cloudflare 邮箱地址
|
||||
- Cloudflare Global API Key
|
||||
- 域名已通过 cloudflare 解析到当前服务器
|
||||
|
||||
**1:** 在终端中运行`x-ui`, 选择 `Cloudflare SSL Certificate`.
|
||||
1. 确保您的域名正确解析到服务器。
|
||||
2. 在终端中运行 `x-ui` 命令,然后选择 `SSL证书管理`。
|
||||
3. 您将看到以下选项:
|
||||
|
||||
- **获取SSL证书:** 获取SSL证书。
|
||||
- **吊销:** 吊销现有的SSL证书。
|
||||
- **强制更新:** 强制更新SSL证书。
|
||||
|
||||
### Certbot
|
||||
```
|
||||
|
||||
安装并使用Certbot:
|
||||
|
||||
```sh
|
||||
apt-get install certbot -y
|
||||
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
|
||||
certbot renew --dry-run
|
||||
```
|
||||
|
||||
***Tip:*** *管理脚本具有 Certbot 。使用 `x-ui` 命令, 选择 `SSL Certificate Management`.*
|
||||
### Cloudflare
|
||||
|
||||
管理脚本内置了Cloudflare的SSL证书申请。要使用此脚本申请证书,您需要以下信息:
|
||||
|
||||
- Cloudflare注册的电子邮件
|
||||
- Cloudflare全局API密钥
|
||||
- 域名必须通过Cloudflare解析到当前服务器
|
||||
|
||||
**如何获取Cloudflare全局API密钥:**
|
||||
|
||||
1. 在终端中运行 `x-ui` 命令,然后选择 `Cloudflare SSL证书`。
|
||||
2. 访问链接:[Cloudflare API Tokens](https://dash.cloudflare.com/profile/api-tokens)。
|
||||
3. 点击“查看全局API密钥”(参见下图):
|
||||

|
||||
4. 您可能需要重新验证您的账户。之后将显示API密钥(参见下图):
|
||||

|
||||
|
||||
使用时,只需输入您的 `域名`、`电子邮件` 和 `API密钥`。如下图所示:
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
@@ -162,7 +188,7 @@ systemctl restart x-ui
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
从Docker中删除3x-ui
|
||||
从Docker中删除3x-ui
|
||||
|
||||
```sh
|
||||
docker stop 3x-ui
|
||||
@@ -174,17 +200,58 @@ systemctl restart x-ui
|
||||
</details>
|
||||
|
||||
|
||||
## Nginx 设置
|
||||
<details>
|
||||
<summary>点击查看 反向代理配置</summary>
|
||||
|
||||
#### Nginx反向代理
|
||||
```nginx
|
||||
location / {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Range $http_range;
|
||||
proxy_set_header If-Range $http_if_range;
|
||||
proxy_redirect off;
|
||||
proxy_pass http://127.0.0.1:2053;
|
||||
}
|
||||
```
|
||||
|
||||
#### Nginx子路径
|
||||
- 确保 `/sub` 面板设置中的"面板url根路径"一致
|
||||
- 面板设置中的 `url` 需要以 `/` 结尾
|
||||
|
||||
```nginx
|
||||
location /sub {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Range $http_range;
|
||||
proxy_set_header If-Range $http_if_range;
|
||||
proxy_redirect off;
|
||||
proxy_pass http://127.0.0.1:2053;
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
## 建议使用的操作系统
|
||||
|
||||
- Ubuntu 20.04+
|
||||
- Debian 11+
|
||||
- CentOS 8+
|
||||
- OpenEuler 22.03+
|
||||
- Fedora 36+
|
||||
- Arch Linux
|
||||
- Parch Linux
|
||||
- Manjaro
|
||||
- Armbian
|
||||
- AlmaLinux 9+
|
||||
- Rockylinux 9+
|
||||
- AlmaLinux 8.0+
|
||||
- Rocky Linux 8+
|
||||
- Oracle Linux 8+
|
||||
- OpenSUSE Tubleweed
|
||||
- Amazon Linux 2023
|
||||
|
||||
## 支持的架构和设备
|
||||
<details>
|
||||
@@ -235,88 +302,103 @@ systemctl restart x-ui
|
||||
- 支持从面板导出/导入数据库
|
||||
|
||||
|
||||
## 默认设置
|
||||
## 默认面板设置
|
||||
|
||||
<details>
|
||||
<summary>点击查看 默认设置</summary>
|
||||
<summary>点击查看默认设置详情</summary>
|
||||
|
||||
### 信息
|
||||
### 用户名、密码、端口和 Web Base Path
|
||||
|
||||
如果您选择不修改这些设置,它们将随机生成(不适用于 Docker)。
|
||||
|
||||
**Docker 的默认设置:**
|
||||
- **用户名:** admin
|
||||
- **密码:** admin
|
||||
- **端口:** 2053
|
||||
- **用户名 & 密码:** 当您跳过设置时,此项会随机生成。
|
||||
- **数据库路径:**
|
||||
- /etc/x-ui/x-ui.db
|
||||
- **Xray 配置路径:**
|
||||
- /usr/local/x-ui/bin/config.json
|
||||
- **面板链接(无SSL):**
|
||||
- http://ip:2053/panel
|
||||
- http://domain:2053/panel
|
||||
- **面板链接(有SSL):**
|
||||
- https://domain:2053/panel
|
||||
|
||||
|
||||
### 数据库管理:
|
||||
|
||||
您可以直接在面板中方便地进行数据库备份和还原。
|
||||
|
||||
- **数据库路径:**
|
||||
- `/etc/x-ui/x-ui.db`
|
||||
|
||||
### Web 基础路径
|
||||
|
||||
1. **重置 Web 基础路径:**
|
||||
- 打开终端。
|
||||
- 运行 `x-ui` 命令。
|
||||
- 选择 `重置 Web 基础路径` 选项。
|
||||
|
||||
2. **生成或自定义路径:**
|
||||
- 路径将会随机生成,或者您可以输入自定义路径。
|
||||
|
||||
3. **查看当前设置:**
|
||||
- 要查看当前设置,请在终端中使用 `x-ui settings` 命令,或在 `x-ui` 面板中点击 `查看当前设置`。
|
||||
|
||||
### 安全建议:
|
||||
- 为了提高安全性,建议在URL结构中使用一个长的随机词。
|
||||
|
||||
**示例:**
|
||||
- `http://ip:port/*webbasepath*/panel`
|
||||
- `http://domain:port/*webbasepath*/panel`
|
||||
|
||||
</details>
|
||||
|
||||
## [WARP 配置](https://gitlab.com/fscarmen/warp)
|
||||
## WARP 配置
|
||||
|
||||
<details>
|
||||
<summary>点击查看 WARP 配置</summary>
|
||||
<summary>点击查看 WARP 配置详情</summary>
|
||||
|
||||
#### 使用
|
||||
#### 使用方法
|
||||
|
||||
如果要在 v2.1.0 之前使用 WARP 路由,请按照以下步骤操作:
|
||||
**对于 `v2.1.0` 及之后的版本:**
|
||||
|
||||
**1.** 在 **SOCKS Proxy Mode** 模式中安装Wrap
|
||||
|
||||
```sh
|
||||
bash <(curl -sSL https://raw.githubusercontent.com/hamid-gh98/x-ui-scripts/main/install_warp_proxy.sh)
|
||||
```
|
||||
|
||||
**2.** 如果您已经安装了 warp,您可以使用以下命令卸载:
|
||||
|
||||
```sh
|
||||
warp u
|
||||
```
|
||||
|
||||
**3.** 在面板中打开您需要的配置
|
||||
|
||||
配置:
|
||||
|
||||
- Block Ads
|
||||
- Route Google + Netflix + Spotify + OpenAI (ChatGPT) to WARP
|
||||
- Fix Google 403 error
|
||||
WARP 已内置,无需额外安装。只需在面板中开启相关配置即可。
|
||||
|
||||
</details>
|
||||
|
||||
## IP 限制
|
||||
|
||||
<details>
|
||||
<summary>点击查看 IP 限制</summary>
|
||||
<summary>点击查看 IP 限制详情</summary>
|
||||
|
||||
#### 使用
|
||||
#### 使用方法
|
||||
|
||||
**注意:** 使用 IP 隧道时,IP 限制无法正常工作。
|
||||
**注意:** 当使用 IP 隧道时,IP 限制将无法正常工作。
|
||||
|
||||
- 适用于最高 `v1.6.1` :
|
||||
- **对于 `v1.6.1` 及之前的版本:**
|
||||
- IP 限制功能已内置于面板中。
|
||||
|
||||
- IP 限制 已被集成在面板中。
|
||||
**对于 `v1.7.0` 及更新的版本:**
|
||||
|
||||
- 适用于 `v1.7.0` 以及更新的版本:
|
||||
要启用 IP 限制功能,您需要安装 `fail2ban` 及其所需的文件,步骤如下:
|
||||
|
||||
- 要使 IP 限制正常工作,您需要按照以下步骤安装 fail2ban 及其所需的文件:
|
||||
1. 在终端中运行 `x-ui` 命令,然后选择 `IP 限制管理`。
|
||||
2. 您将看到以下选项:
|
||||
|
||||
1. 使用面板内置的 `x-ui` 指令
|
||||
2. 选择 `IP Limit Management`.
|
||||
3. 根据您的需要选择合适的选项。
|
||||
|
||||
- 确保您的 Xray 配置上有 ./access.log 。在 v2.1.3 之后,我们有一个选项。
|
||||
|
||||
```sh
|
||||
- **更改封禁时长:** 调整封禁时长。
|
||||
- **解除所有封禁:** 解除当前的所有封禁。
|
||||
- **查看日志:** 查看日志。
|
||||
- **Fail2ban 状态:** 检查 `fail2ban` 的状态。
|
||||
- **重启 Fail2ban:** 重启 `fail2ban` 服务。
|
||||
- **卸载 Fail2ban:** 卸载带有配置的 Fail2ban。
|
||||
|
||||
3. 在面板中通过设置 `Xray 配置/log/访问日志` 为 `./access.log` 添加访问日志路径,然后保存并重启 Xray。
|
||||
|
||||
- **对于 `v2.1.3` 之前的版本:**
|
||||
- 您需要在 Xray 配置中手动设置访问日志路径:
|
||||
|
||||
```sh
|
||||
"log": {
|
||||
"access": "./access.log",
|
||||
"dnsLog": false,
|
||||
"loglevel": "warning"
|
||||
},
|
||||
```
|
||||
```
|
||||
|
||||
- **对于 `v2.1.3` 及之后的版本:**
|
||||
- 面板中直接提供了配置 `access.log` 的选项。
|
||||
|
||||
</details>
|
||||
|
||||
@@ -367,7 +449,7 @@ Web 面板通过 Telegram Bot 支持每日流量、面板登录、数据库备
|
||||
|
||||
- 与 [Botfather](https://t.me/BotFather) 对话:
|
||||

|
||||
|
||||
|
||||
- 使用 /newbot 创建新机器人:你需要提供机器人名称以及用户名,注意名称中末尾要包含“bot”
|
||||

|
||||
|
||||
@@ -392,28 +474,29 @@ Web 面板通过 Telegram Bot 支持每日流量、面板登录、数据库备
|
||||
|
||||
#### 使用
|
||||
|
||||
- [API 文档](https://documenter.getpostman.com/view/5146551/2sAXxP8Y12)
|
||||
- `/login` 使用 `POST` 用户名称 & 密码: `{username: '', password: ''}` 登录
|
||||
- `/panel/api/inbounds` 以下操作的基础:
|
||||
|
||||
| 方法 | 路径 | 操作 |
|
||||
| :----: | ---------------------------------- | ------------------------------------------- |
|
||||
| `GET` | `"/list"` | 获取所有入站 |
|
||||
| `GET` | `"/get/:id"` | 获取所有入站以及inbound.id |
|
||||
| `GET` | `"/getClientTraffics/:email"` | 通过电子邮件获取客户端流量 |
|
||||
| `GET` | `"/createbackup"` | Telegram 机器人向管理员发送备份 |
|
||||
| `POST` | `"/add"` | 添加入站 |
|
||||
| `POST` | `"/del/:id"` | 删除入站 |
|
||||
| `POST` | `"/update/:id"` | 更新入站 |
|
||||
| `POST` | `"/clientIps/:email"` | 客户端 IP 地址 |
|
||||
| `POST` | `"/clearClientIps/:email"` | 清除客户端 IP 地址 |
|
||||
| `POST` | `"/addClient"` | 将客户端添加到入站 |
|
||||
| `POST` | `"/:id/delClient/:clientId"` | 通过 clientId\* 删除客户端 |
|
||||
| `POST` | `"/updateClient/:clientId"` | 通过 clientId\* 更新客户端 |
|
||||
| `POST` | `"/:id/resetClientTraffic/:email"` | 重置客户端的流量 |
|
||||
| `POST` | `"/resetAllTraffics"` | 重置所有入站的流量 |
|
||||
| `POST` | `"/resetAllClientTraffics/:id"` | 重置入站中所有客户端的流量 |
|
||||
| `POST` | `"/delDepletedClients/:id"` | 删除入站耗尽的客户端 (-1: all) |
|
||||
| `POST` | `"/onlines"` | 获取在线用户 ( 电子邮件列表 ) |
|
||||
| 方法 | 路径 | 操作 |
|
||||
| :----: | ---------------------------------- | --------------------------------- |
|
||||
| `GET` | `"/list"` | 获取所有入站 |
|
||||
| `GET` | `"/get/:id"` | 获取所有入站以及inbound.id |
|
||||
| `GET` | `"/getClientTraffics/:email"` | 通过电子邮件获取客户端流量 |
|
||||
| `GET` | `"/createbackup"` | Telegram 机器人向管理员发送备份 |
|
||||
| `POST` | `"/add"` | 添加入站 |
|
||||
| `POST` | `"/del/:id"` | 删除入站 |
|
||||
| `POST` | `"/update/:id"` | 更新入站 |
|
||||
| `POST` | `"/clientIps/:email"` | 客户端 IP 地址 |
|
||||
| `POST` | `"/clearClientIps/:email"` | 清除客户端 IP 地址 |
|
||||
| `POST` | `"/addClient"` | 将客户端添加到入站 |
|
||||
| `POST` | `"/:id/delClient/:clientId"` | 通过 clientId\* 删除客户端 |
|
||||
| `POST` | `"/updateClient/:clientId"` | 通过 clientId\* 更新客户端 |
|
||||
| `POST` | `"/:id/resetClientTraffic/:email"` | 重置客户端的流量 |
|
||||
| `POST` | `"/resetAllTraffics"` | 重置所有入站的流量 |
|
||||
| `POST` | `"/resetAllClientTraffics/:id"` | 重置入站中所有客户端的流量 |
|
||||
| `POST` | `"/delDepletedClients/:id"` | 删除入站耗尽的客户端 (-1: all) |
|
||||
| `POST` | `"/onlines"` | 获取在线用户 ( 电子邮件列表 ) |
|
||||
|
||||
\*- `clientId` 项应该使用下列数据
|
||||
|
||||
@@ -421,9 +504,7 @@ Web 面板通过 Telegram Bot 支持每日流量、面板登录、数据库备
|
||||
- `client.password` TROJAN
|
||||
- `client.email` Shadowsocks
|
||||
|
||||
|
||||
- [API 文档](https://documenter.getpostman.com/view/16802678/2s9YkgD5jm)
|
||||
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415%26entityType%3Dcollection%26workspaceId%3D2cd38c01-c851-4a15-a972-f181c23359d9)
|
||||
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://god.gw.postman.com/run-collection/5146551-e6aac565-e0e2-46df-acff-2607a51bbd04?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D5146551-e6aac565-e0e2-46df-acff-2607a51bbd04%26entityType%3Dcollection%26workspaceId%3Dd64f609f-485a-4951-9b8f-876b3f917124)
|
||||
</details>
|
||||
|
||||
## 环境变量
|
||||
@@ -433,7 +514,7 @@ Web 面板通过 Telegram Bot 支持每日流量、面板登录、数据库备
|
||||
|
||||
#### Usage
|
||||
|
||||
| 变量 | Type | 默认 |
|
||||
| 变量 | Type | 默认 |
|
||||
| -------------- | :--------------------------------------------: | :------------ |
|
||||
| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` |
|
||||
| XUI_DEBUG | `boolean` | `false` |
|
||||
@@ -1 +1 @@
|
||||
2.3.0
|
||||
2.4.4
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
@@ -18,54 +19,51 @@ import (
|
||||
|
||||
var db *gorm.DB
|
||||
|
||||
var initializers = []func() error{
|
||||
initUser,
|
||||
initInbound,
|
||||
initOutbound,
|
||||
initSetting,
|
||||
initInboundClientIps,
|
||||
initClientTraffic,
|
||||
const (
|
||||
defaultUsername = "admin"
|
||||
defaultPassword = "admin"
|
||||
defaultSecret = ""
|
||||
)
|
||||
|
||||
func initModels() error {
|
||||
models := []interface{}{
|
||||
&model.User{},
|
||||
&model.Inbound{},
|
||||
&model.OutboundTraffics{},
|
||||
&model.Setting{},
|
||||
&model.InboundClientIps{},
|
||||
&xray.ClientTraffic{},
|
||||
}
|
||||
for _, model := range models {
|
||||
if err := db.AutoMigrate(model); err != nil {
|
||||
log.Printf("Error auto migrating model: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func initUser() error {
|
||||
err := db.AutoMigrate(&model.User{})
|
||||
empty, err := isTableEmpty("users")
|
||||
if err != nil {
|
||||
log.Printf("Error checking if users table is empty: %v", err)
|
||||
return err
|
||||
}
|
||||
var count int64
|
||||
err = db.Model(&model.User{}).Count(&count).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
if empty {
|
||||
user := &model.User{
|
||||
Username: "admin",
|
||||
Password: "admin",
|
||||
LoginSecret: "",
|
||||
Username: defaultUsername,
|
||||
Password: defaultPassword,
|
||||
LoginSecret: defaultSecret,
|
||||
}
|
||||
return db.Create(user).Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func initInbound() error {
|
||||
return db.AutoMigrate(&model.Inbound{})
|
||||
}
|
||||
|
||||
func initOutbound() error {
|
||||
return db.AutoMigrate(&model.OutboundTraffics{})
|
||||
}
|
||||
|
||||
func initSetting() error {
|
||||
return db.AutoMigrate(&model.Setting{})
|
||||
}
|
||||
|
||||
func initInboundClientIps() error {
|
||||
return db.AutoMigrate(&model.InboundClientIps{})
|
||||
}
|
||||
|
||||
func initClientTraffic() error {
|
||||
return db.AutoMigrate(&xray.ClientTraffic{})
|
||||
func isTableEmpty(tableName string) (bool, error) {
|
||||
var count int64
|
||||
err := db.Table(tableName).Count(&count).Error
|
||||
return count == 0, err
|
||||
}
|
||||
|
||||
func InitDB(dbPath string) error {
|
||||
@@ -91,15 +89,27 @@ func InitDB(dbPath string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, initialize := range initializers {
|
||||
if err := initialize(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := initModels(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := initUser(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CloseDB() error {
|
||||
if db != nil {
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sqlDB.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetDB() *gorm.DB {
|
||||
return db
|
||||
}
|
||||
|
||||
@@ -10,12 +10,14 @@ import (
|
||||
type Protocol string
|
||||
|
||||
const (
|
||||
VMess Protocol = "vmess"
|
||||
VMESS Protocol = "vmess"
|
||||
VLESS Protocol = "vless"
|
||||
Dokodemo Protocol = "Dokodemo-door"
|
||||
Http Protocol = "http"
|
||||
DOKODEMO Protocol = "dokodemo-door"
|
||||
HTTP Protocol = "http"
|
||||
Trojan Protocol = "trojan"
|
||||
Shadowsocks Protocol = "shadowsocks"
|
||||
Socks Protocol = "socks"
|
||||
WireGuard Protocol = "wireguard"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
@@ -44,6 +46,7 @@ type Inbound struct {
|
||||
StreamSettings string `json:"streamSettings" form:"streamSettings"`
|
||||
Tag string `json:"tag" form:"tag" gorm:"unique"`
|
||||
Sniffing string `json:"sniffing" form:"sniffing"`
|
||||
Allocate string `json:"allocate" form:"allocate"`
|
||||
}
|
||||
|
||||
type OutboundTraffics struct {
|
||||
@@ -73,6 +76,7 @@ func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
|
||||
StreamSettings: json_util.RawMessage(i.StreamSettings),
|
||||
Tag: i.Tag,
|
||||
Sniffing: json_util.RawMessage(i.Sniffing),
|
||||
Allocate: json_util.RawMessage(i.Allocate),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +88,7 @@ type Setting struct {
|
||||
|
||||
type Client struct {
|
||||
ID string `json:"id"`
|
||||
Security string `json:"security"`
|
||||
Password string `json:"password"`
|
||||
Flow string `json:"flow"`
|
||||
Email string `json:"email"`
|
||||
|
||||
121
go.mod
@@ -1,99 +1,102 @@
|
||||
module x-ui
|
||||
|
||||
go 1.22.2
|
||||
go 1.23.2
|
||||
|
||||
require (
|
||||
github.com/Calidity/gin-sessions v1.3.1
|
||||
github.com/gin-contrib/gzip v1.0.0
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/goccy/go-json v0.10.2
|
||||
github.com/mymmrac/telego v0.29.2
|
||||
github.com/gin-contrib/gzip v1.0.1
|
||||
github.com/gin-contrib/sessions v1.0.1
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/goccy/go-json v0.10.3
|
||||
github.com/mymmrac/telego v0.31.3
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.0
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/pelletier/go-toml/v2 v2.2.1
|
||||
github.com/pelletier/go-toml/v2 v2.2.3
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/shirou/gopsutil/v3 v3.24.3
|
||||
github.com/valyala/fasthttp v1.52.0
|
||||
github.com/xtls/xray-core v1.8.10
|
||||
github.com/shirou/gopsutil/v4 v4.24.9
|
||||
github.com/valyala/fasthttp v1.56.0
|
||||
github.com/xtls/xray-core v1.8.25-0.20241005021528-c30f5d47964b
|
||||
go.uber.org/atomic v1.11.0
|
||||
golang.org/x/text v0.14.0
|
||||
google.golang.org/grpc v1.63.2
|
||||
gorm.io/driver/sqlite v1.5.5
|
||||
gorm.io/gorm v1.25.9
|
||||
golang.org/x/text v0.19.0
|
||||
google.golang.org/grpc v1.67.1
|
||||
gorm.io/driver/sqlite v1.5.6
|
||||
gorm.io/gorm v1.25.12
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/bytedance/sonic v1.11.3 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||
github.com/chenzhuoyu/iasm v0.9.1 // indirect
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/bytedance/sonic v1.12.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.0 // indirect
|
||||
github.com/cloudflare/circl v1.5.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect
|
||||
github.com/fasthttp/router v1.5.0 // indirect
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/ebitengine/purego v0.8.0 // indirect
|
||||
github.com/fasthttp/router v1.5.2 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.5 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.19.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 // indirect
|
||||
github.com/go-playground/validator/v10 v10.22.1 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/pprof v0.0.0-20241009165004-a3522334989c // indirect
|
||||
github.com/gorilla/context v1.1.2 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/gorilla/sessions v1.2.2 // indirect
|
||||
github.com/gorilla/websocket v1.5.1 // indirect
|
||||
github.com/gorilla/sessions v1.4.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/grbit/go-json v0.11.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.17.7 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/klauspost/compress v1.17.10 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.24 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.16.0 // indirect
|
||||
github.com/pires/go-proxyproto v0.7.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
|
||||
github.com/quic-go/quic-go v0.42.0 // indirect
|
||||
github.com/refraction-networking/utls v1.6.3 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.20.2 // indirect
|
||||
github.com/pires/go-proxyproto v0.8.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.47.0 // indirect
|
||||
github.com/refraction-networking/utls v1.6.7 // indirect
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||
github.com/sagernet/sing v0.3.8 // indirect
|
||||
github.com/sagernet/sing-shadowsocks v0.2.6 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 // 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.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||
github.com/sagernet/sing v0.4.3 // indirect
|
||||
github.com/sagernet/sing-shadowsocks v0.2.7 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
||||
github.com/tklauser/numcpus v0.9.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fastjson v1.6.4 // indirect
|
||||
github.com/vishvananda/netlink v1.2.1-beta.2.0.20230316163032-ced5aaba43e3 // indirect
|
||||
github.com/vishvananda/netlink v1.3.0 // indirect
|
||||
github.com/vishvananda/netns v0.0.4 // indirect
|
||||
github.com/xtls/reality v0.0.0-20231112171332-de1173cf2b19 // indirect
|
||||
github.com/xtls/reality v0.0.0-20240909153216-e26ae2305463 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/arch v0.7.0 // indirect
|
||||
golang.org/x/crypto v0.22.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
|
||||
golang.org/x/mod v0.16.0 // indirect
|
||||
golang.org/x/net v0.24.0 // indirect
|
||||
golang.org/x/sys v0.19.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.19.0 // indirect
|
||||
golang.org/x/arch v0.11.0 // indirect
|
||||
golang.org/x/crypto v0.28.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
|
||||
golang.org/x/mod v0.21.0 // indirect
|
||||
golang.org/x/net v0.30.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/time v0.7.0 // indirect
|
||||
golang.org/x/tools v0.26.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect
|
||||
google.golang.org/protobuf v1.35.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20231104011432-48a6d7d5bd0b // indirect
|
||||
lukechampine.com/blake3 v1.2.1 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489 // indirect
|
||||
lukechampine.com/blake3 v1.3.0 // indirect
|
||||
)
|
||||
|
||||
441
go.sum
@@ -1,66 +1,45 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
|
||||
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
|
||||
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
|
||||
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
|
||||
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
|
||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/Calidity/gin-sessions v1.3.1 h1:nF3dCBWa7TZ4j26iYLwGRmzZy9YODhWoOS3fmi+snyE=
|
||||
github.com/Calidity/gin-sessions v1.3.1/go.mod h1:I0+QE6qkO50TeN/n6If6novvxHk4Isvr23U8EdvPdns=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
|
||||
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
|
||||
github.com/bytedance/sonic v1.11.3 h1:jRN+yEjakWh8aK5FzrciUHG8OFXK+4/KrAX/ysEtHAA=
|
||||
github.com/bytedance/sonic v1.11.3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
|
||||
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
||||
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
|
||||
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
||||
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
||||
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0 h1:Wo41lDOevRJSGpevP+8Pk5bANX7fJacO2w04aqLiC5I=
|
||||
github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0/go.mod h1:FVGavL/QEBQDcBpr3fAojoK17xX5k9bicBphrOpP7uM=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU=
|
||||
github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
|
||||
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys=
|
||||
github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
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/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/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/fasthttp/router v1.5.0 h1:3Qbbo27HAPzwbpRzgiV5V9+2faPkPt3eNuRaDV6LYDA=
|
||||
github.com/fasthttp/router v1.5.0/go.mod h1:FddcKNXFZg1imHcy+uKB0oo/o6yE9zD3wNguqlhWDak=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
|
||||
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE=
|
||||
github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/fasthttp/router v1.5.2 h1:ckJCCdV7hWkkrMeId3WfEhz+4Gyyf6QPwxi/RHIMZ6I=
|
||||
github.com/fasthttp/router v1.5.2/go.mod h1:C8EY53ozOwpONyevc/V7Gr8pqnEjwnkFFqPo1alAGs0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4=
|
||||
github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
|
||||
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/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
|
||||
github.com/gin-contrib/gzip v1.0.0 h1:UKN586Po/92IDX6ie5CWLgMI81obiIp5nSP85T3wlTk=
|
||||
github.com/gin-contrib/gzip v1.0.0/go.mod h1:CtG7tQrPB3vIBo6Gat9FVUsis+1emjvQqd66ME5TdnE=
|
||||
github.com/gin-contrib/gzip v1.0.1 h1:HQ8ENHODeLY7a4g1Au/46Z92bdGFl74OhxcZble9WJE=
|
||||
github.com/gin-contrib/gzip v1.0.1/go.mod h1:njt428fdUNRvjuJf16tZMYZ2Yl+WQB53X5wmhDwXvC4=
|
||||
github.com/gin-contrib/sessions v1.0.1 h1:3hsJyNs7v7N8OtelFmYXFrulAf6zSR7nW/putcPEHxI=
|
||||
github.com/gin-contrib/sessions v1.0.1/go.mod h1:ouxSFM24/OgIud5MJYQJLpy6AwxQ5EYO9yLhbtObGkM=
|
||||
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-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
@@ -70,197 +49,121 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
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/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
|
||||
github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
|
||||
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
|
||||
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 h1:y3N7Bm7Y9/CtpiVkw/ZWj6lSlDF3F74SfKwfTCer72Q=
|
||||
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/google/pprof v0.0.0-20241009165004-a3522334989c h1:NDovD0SMpBYXlE1zJmS1q55vWB/fUQBcPAqAboZSccA=
|
||||
github.com/google/pprof v0.0.0-20241009165004-a3522334989c/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
|
||||
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
|
||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
|
||||
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
|
||||
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grbit/go-json v0.11.0 h1:bAbyMdYrYl/OjYsSqLH99N2DyQ291mHy726Mx+sYrnc=
|
||||
github.com/grbit/go-json v0.11.0/go.mod h1:IYpHsdybQ386+6g3VE6AXQ3uTGa5mquBme5/ZWmtzek=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
|
||||
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/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
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/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
|
||||
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0=
|
||||
github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed h1:036IscGBfJsFIgJQzlui7nK1Ncm0tp2ktmPj8xO4N/0=
|
||||
github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
|
||||
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
|
||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||
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/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
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/mymmrac/telego v0.29.2 h1:5+fQ/b8d8Ld6ihCJ0OLe1CwUdT3t1sIUl3RaSaSvRJs=
|
||||
github.com/mymmrac/telego v0.29.2/go.mod h1:BsKr+GF9BHqaVaLBwsZeDnfuJcJx2olWuDEtKm4zHMc=
|
||||
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
||||
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||
github.com/mymmrac/telego v0.31.3 h1:yZlD+dm+1W6p3OmCG8K+MbS02Y6paUgwPnqfZN3RWQQ=
|
||||
github.com/mymmrac/telego v0.31.3/go.mod h1:coOoqXVmjFnwBlzusjfEezbQ7RH9wQnDowJdMm+bnEo=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4=
|
||||
github.com/onsi/ginkgo/v2 v2.16.0 h1:7q1w9frJDzninhXxjZd+Y/x54XNjG/UlRLIYPZafsPM=
|
||||
github.com/onsi/ginkgo/v2 v2.16.0/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
|
||||
github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
|
||||
github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
|
||||
github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4=
|
||||
github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag=
|
||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||
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/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg=
|
||||
github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
|
||||
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/pires/go-proxyproto v0.8.0 h1:5unRmEAPbHXHuLjDg01CxJWf91cw3lKHc/0xzKpXEe0=
|
||||
github.com/pires/go-proxyproto v0.8.0/go.mod h1:iknsfgnH8EkjrMeMyvfKByp9TiBZCKZM0jx2xmKqnVY=
|
||||
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/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/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/quic-go/quic-go v0.42.0 h1:uSfdap0eveIl8KXnipv9K7nlwZ5IqLlYOpJ58u5utpM=
|
||||
github.com/quic-go/quic-go v0.42.0/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M=
|
||||
github.com/refraction-networking/utls v1.6.3 h1:MFOfRN35sSx6K5AZNIoESsBuBxS2LCgRilRIdHb6fDc=
|
||||
github.com/refraction-networking/utls v1.6.3/go.mod h1:yil9+7qSl+gBwJqztoQseO6Pr3h62pQoY1lXiNR/FPs=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.47.0 h1:yXs3v7r2bm1wmPTYNLKAAJTHMYkPEsfYJmTazXrCZ7Y=
|
||||
github.com/quic-go/quic-go v0.47.0/go.mod h1:3bCapYsJvXGZcipOHuu7plYtaV6tnF+z7wIFsU0WK9E=
|
||||
github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM=
|
||||
github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
|
||||
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/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/sagernet/sing v0.3.8 h1:gm4JKalPhydMYX2zFOTnnd4TXtM/16WFRqSjMepYQQk=
|
||||
github.com/sagernet/sing v0.3.8/go.mod h1:+60H3Cm91RnL9dpVGWDPHt0zTQImO9Vfqt9a4rSambI=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.6 h1:xr7ylAS/q1cQYS8oxKKajhuQcchd5VJJ4K4UZrrpp0s=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.6/go.mod h1:j2YZBIpWIuElPFL/5sJAj470bcn/3QQ5lxZUNKLDNAM=
|
||||
github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 h1:KanIMPX0QdEdB4R3CiimCAbxFrhB3j7h0/OvpYGVQa8=
|
||||
github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
|
||||
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/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shirou/gopsutil/v3 v3.24.3 h1:eoUGJSmdfLzJ3mxIhmOAhgKEKgQkeOwKpz1NbhVnuPE=
|
||||
github.com/shirou/gopsutil/v3 v3.24.3/go.mod h1:JpND7O217xa72ewWz9zN2eIIkPWsDN/3pl0H8Qt0uwg=
|
||||
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/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
|
||||
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
|
||||
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
|
||||
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
||||
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
|
||||
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
|
||||
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
|
||||
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
|
||||
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
|
||||
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
|
||||
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
|
||||
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
|
||||
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
|
||||
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
|
||||
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
|
||||
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
|
||||
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
|
||||
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
|
||||
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
|
||||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/sagernet/sing v0.4.3 h1:Ty/NAiNnVd6844k7ujlL5lkzydhcTH5Psc432jXA4Y8=
|
||||
github.com/sagernet/sing v0.4.3/go.mod h1:ieZHA/+Y9YZfXs2I3WtuwgyCZ6GPsIR7HdKb1SdEnls=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE=
|
||||
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc=
|
||||
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4=
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
|
||||
github.com/shirou/gopsutil/v4 v4.24.9 h1:KIV+/HaHD5ka5f570RZq+2SaeFsb/pq+fp2DGNWYoOI=
|
||||
github.com/shirou/gopsutil/v4 v4.24.9/go.mod h1:3fkaHNeYsUFCGZ8+9vZVWtbyM1k2eRnlL+bWO8Bxa/Q=
|
||||
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.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
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.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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
|
||||
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
|
||||
github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
|
||||
github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
|
||||
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/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
@@ -269,152 +172,80 @@ github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0=
|
||||
github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ=
|
||||
github.com/valyala/fasthttp v1.56.0 h1:bEZdJev/6LCBlpdORfrLu/WOZXXxvrUQSiyniuaoW8U=
|
||||
github.com/valyala/fasthttp v1.56.0/go.mod h1:sReBt3XZVnudxuLOx4J/fMrJVorWRiWY2koQKgABiVI=
|
||||
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
|
||||
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
||||
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
|
||||
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
|
||||
github.com/vishvananda/netlink v1.2.1-beta.2.0.20230316163032-ced5aaba43e3 h1:tkMT5pTye+1NlKIXETU78NXw0fyjnaNHmJyyLyzw8+U=
|
||||
github.com/vishvananda/netlink v1.2.1-beta.2.0.20230316163032-ced5aaba43e3/go.mod h1:cAAsePK2e15YDAMJNyOpGYEWNe4sIghTY7gpz4cX/Ik=
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
|
||||
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
|
||||
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/xtls/reality v0.0.0-20231112171332-de1173cf2b19 h1:capMfFYRgH9BCLd6A3Er/cH3A9Nz3CU2KwxwOQZIePI=
|
||||
github.com/xtls/reality v0.0.0-20231112171332-de1173cf2b19/go.mod h1:dm4y/1QwzjGaK17ofi0Vs6NpKAHegZky8qk6J2JJZAE=
|
||||
github.com/xtls/xray-core v1.8.10 h1:qxae6gSteonpPI7EZyOyqw5HmRVRzmU07qs0l1GNqz4=
|
||||
github.com/xtls/xray-core v1.8.10/go.mod h1:Mc1t+kLBPE5a1EpsUNKjMLviGz3Y0XywxeEraJZAMlI=
|
||||
github.com/xtls/reality v0.0.0-20240909153216-e26ae2305463 h1:g1Cj7d+my6k/HHxLAyxPwyX8i7FGRr6ulBDMkBzg2BM=
|
||||
github.com/xtls/reality v0.0.0-20240909153216-e26ae2305463/go.mod h1:BjIOLmkEEtAgloAiVUcYj0Mt+YU00JARZw8AEU0IwAg=
|
||||
github.com/xtls/xray-core v1.8.25-0.20241005021528-c30f5d47964b h1:bWuePNnzV4ptnSYJkY96dAg3WgjYbfVgGnasLe3++9w=
|
||||
github.com/xtls/xray-core v1.8.25-0.20241005021528-c30f5d47964b/go.mod h1:YSvBScSqyzAocGDvzHBbEeoHNrFy8nV6gityRVDvHaM=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
|
||||
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
|
||||
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/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-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4=
|
||||
golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220804214406-8e32c043e418/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
|
||||
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
||||
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
|
||||
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
|
||||
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be h1:LG9vZxsWGOmUKieR8wPAUR3u3MpnYFQZROPIMaXh7/A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
|
||||
google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
|
||||
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
|
||||
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
|
||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
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/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E=
|
||||
gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE=
|
||||
gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8=
|
||||
gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
|
||||
gvisor.dev/gvisor v0.0.0-20231104011432-48a6d7d5bd0b h1:yqkg3pTifuKukuWanp8spDsL4irJkHF5WI0J47hU87o=
|
||||
gvisor.dev/gvisor v0.0.0-20231104011432-48a6d7d5bd0b/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI=
|
||||
lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
|
||||
gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
|
||||
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489 h1:ze1vwAdliUAr68RQ5NtufWaXaOg8WUO2OACzEV+TNdE=
|
||||
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk=
|
||||
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
|
||||
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
|
||||
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
||||
|
||||
175
install.sh
@@ -39,41 +39,51 @@ arch() {
|
||||
echo "arch: $(arch)"
|
||||
|
||||
os_version=""
|
||||
os_version=$(grep -i version_id /etc/os-release | cut -d \" -f2 | cut -d . -f1)
|
||||
os_version=$(grep "^VERSION_ID" /etc/os-release | cut -d '=' -f2 | tr -d '"' | tr -d '.')
|
||||
|
||||
if [[ "${release}" == "arch" ]]; then
|
||||
echo "Your OS is Arch Linux"
|
||||
elif [[ "${release}" == "parch" ]]; then
|
||||
echo "Your OS is Parch linux"
|
||||
echo "Your OS is Parch Linux"
|
||||
elif [[ "${release}" == "manjaro" ]]; then
|
||||
echo "Your OS is Manjaro"
|
||||
elif [[ "${release}" == "armbian" ]]; then
|
||||
echo "Your OS is Armbian"
|
||||
elif [[ "${release}" == "opensuse-tumbleweed" ]]; then
|
||||
echo "Your OS is OpenSUSE Tumbleweed"
|
||||
elif [[ "${release}" == "openEuler" ]]; then
|
||||
if [[ ${os_version} -lt 2203 ]]; then
|
||||
echo -e "${red} Please use OpenEuler 22.03 or higher ${plain}\n" && exit 1
|
||||
fi
|
||||
elif [[ "${release}" == "centos" ]]; then
|
||||
if [[ ${os_version} -lt 8 ]]; then
|
||||
echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1
|
||||
fi
|
||||
elif [[ "${release}" == "ubuntu" ]]; then
|
||||
if [[ ${os_version} -lt 20 ]]; then
|
||||
if [[ ${os_version} -lt 2004 ]]; then
|
||||
echo -e "${red} Please use Ubuntu 20 or higher version!${plain}\n" && exit 1
|
||||
fi
|
||||
elif [[ "${release}" == "fedora" ]]; then
|
||||
if [[ ${os_version} -lt 36 ]]; then
|
||||
echo -e "${red} Please use Fedora 36 or higher version!${plain}\n" && exit 1
|
||||
fi
|
||||
elif [[ "${release}" == "amzn" ]]; then
|
||||
if [[ ${os_version} != "2023" ]]; then
|
||||
echo -e "${red} Please use Amazon Linux 2023!${plain}\n" && exit 1
|
||||
fi
|
||||
elif [[ "${release}" == "debian" ]]; then
|
||||
if [[ ${os_version} -lt 11 ]]; then
|
||||
echo -e "${red} Please use Debian 11 or higher ${plain}\n" && exit 1
|
||||
fi
|
||||
elif [[ "${release}" == "almalinux" ]]; then
|
||||
if [[ ${os_version} -lt 9 ]]; then
|
||||
echo -e "${red} Please use AlmaLinux 9 or higher ${plain}\n" && exit 1
|
||||
if [[ ${os_version} -lt 80 ]]; then
|
||||
echo -e "${red} Please use AlmaLinux 8.0 or higher ${plain}\n" && exit 1
|
||||
fi
|
||||
elif [[ "${release}" == "rocky" ]]; then
|
||||
if [[ ${os_version} -lt 9 ]]; then
|
||||
echo -e "${red} Please use Rocky Linux 9 or higher ${plain}\n" && exit 1
|
||||
if [[ ${os_version} -lt 8 ]]; then
|
||||
echo -e "${red} Please use Rocky Linux 8 or higher ${plain}\n" && exit 1
|
||||
fi
|
||||
elif [[ "${release}" == "oracle" ]]; then
|
||||
elif [[ "${release}" == "ol" ]]; then
|
||||
if [[ ${os_version} -lt 8 ]]; then
|
||||
echo -e "${red} Please use Oracle Linux 8 or higher ${plain}\n" && exit 1
|
||||
fi
|
||||
@@ -83,67 +93,109 @@ else
|
||||
echo "- Ubuntu 20.04+"
|
||||
echo "- Debian 11+"
|
||||
echo "- CentOS 8+"
|
||||
echo "- OpenEuler 22.03+"
|
||||
echo "- Fedora 36+"
|
||||
echo "- Arch Linux"
|
||||
echo "- Parch Linux"
|
||||
echo "- Manjaro"
|
||||
echo "- Armbian"
|
||||
echo "- AlmaLinux 9+"
|
||||
echo "- Rocky Linux 9+"
|
||||
echo "- AlmaLinux 8.0+"
|
||||
echo "- Rocky Linux 8+"
|
||||
echo "- Oracle Linux 8+"
|
||||
echo "- OpenSUSE Tumbleweed"
|
||||
echo "- Amazon Linux 2023"
|
||||
exit 1
|
||||
|
||||
fi
|
||||
|
||||
install_base() {
|
||||
case "${release}" in
|
||||
centos | almalinux | rocky | oracle)
|
||||
ubuntu | debian | armbian)
|
||||
apt-get update && apt-get install -y -q wget curl tar tzdata
|
||||
;;
|
||||
centos | almalinux | rocky | ol)
|
||||
yum -y update && yum install -y -q wget curl tar tzdata
|
||||
;;
|
||||
fedora)
|
||||
fedora | amzn)
|
||||
dnf -y update && dnf install -y -q wget curl tar tzdata
|
||||
;;
|
||||
arch | manjaro | parch)
|
||||
pacman -Syu && pacman -Syu --noconfirm wget curl tar tzdata
|
||||
;;
|
||||
opensuse-tumbleweed)
|
||||
zypper refresh && zypper -q install -y wget curl tar timezone
|
||||
;;
|
||||
*)
|
||||
apt-get update && apt install -y -q wget curl tar tzdata
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# This function will be called when user installed x-ui out of security
|
||||
gen_random_string() {
|
||||
local length="$1"
|
||||
local random_string=$(LC_ALL=C tr -dc 'a-zA-Z0-9' </dev/urandom | fold -w "$length" | head -n 1)
|
||||
echo "$random_string"
|
||||
}
|
||||
|
||||
config_after_install() {
|
||||
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
|
||||
echo -e "${yellow}Install/update finished! For security, it's recommended to modify panel settings ${plain}"
|
||||
read -p "Would you like to customize the Panel Port settings? (If not, random settings will be applied) [y/n]: " config_confirm
|
||||
|
||||
local config_webBasePath=$(gen_random_string 15)
|
||||
local config_account=$(gen_random_string 10)
|
||||
local config_password=$(gen_random_string 10)
|
||||
|
||||
if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then
|
||||
read -p "Please set up your username:" config_account
|
||||
echo -e "${yellow}Your username will be:${config_account}${plain}"
|
||||
read -p "Please set up your password:" config_password
|
||||
echo -e "${yellow}Your password will be:${config_password}${plain}"
|
||||
read -p "Please set up the panel port:" config_port
|
||||
echo -e "${yellow}Your panel port is:${config_port}${plain}"
|
||||
|
||||
read -p "Please set up the panel port: " config_port
|
||||
echo -e "${yellow}Your Panel Port is: ${config_port}${plain}"
|
||||
|
||||
echo -e "${yellow}Your Username will be generated randomly: ${config_account}${plain}"
|
||||
echo -e "${yellow}Your Password will be generated randomly: ${config_password}${plain}"
|
||||
echo -e "${yellow}Your Web Base Path will be generated randomly: ${config_webBasePath}${plain}"
|
||||
|
||||
echo -e "${yellow}Initializing, please wait...${plain}"
|
||||
/usr/local/x-ui/x-ui setting -username ${config_account} -password ${config_password}
|
||||
echo -e "${yellow}Account name and password set successfully!${plain}"
|
||||
/usr/local/x-ui/x-ui setting -port ${config_port}
|
||||
echo -e "${yellow}Panel port set successfully!${plain}"
|
||||
|
||||
/usr/local/x-ui/x-ui setting -username "${config_account}" -password "${config_password}" -port "${config_port}" -webBasePath "${config_webBasePath}"
|
||||
echo -e "${green}Settings applied successfully!${plain}"
|
||||
|
||||
echo -e "###############################################"
|
||||
echo -e "${green}Username: ${config_account}${plain}"
|
||||
echo -e "${green}Password: ${config_password}${plain}"
|
||||
echo -e "${green}Port: ${config_port}${plain}"
|
||||
echo -e "${green}WebBasePath: ${config_webBasePath}${plain}"
|
||||
echo -e "###############################################"
|
||||
|
||||
else
|
||||
echo -e "${red}cancel...${plain}"
|
||||
|
||||
echo -e "${red}Cancel...${plain}"
|
||||
|
||||
if [[ ! -f "/etc/x-ui/x-ui.db" ]]; then
|
||||
local usernameTemp=$(head -c 6 /dev/urandom | base64)
|
||||
local passwordTemp=$(head -c 6 /dev/urandom | base64)
|
||||
/usr/local/x-ui/x-ui setting -username ${usernameTemp} -password ${passwordTemp}
|
||||
echo -e "this is a fresh installation,will generate random login info for security concerns:"
|
||||
|
||||
local portTemp=$(shuf -i 1024-62000 -n 1)
|
||||
|
||||
/usr/local/x-ui/x-ui setting -username "${config_account}" -password "${config_password}" -port "${portTemp}" -webBasePath "${config_webBasePath}"
|
||||
echo -e "This is a fresh installation, generating random login info for security concerns:"
|
||||
echo -e "###############################################"
|
||||
echo -e "${green}username:${usernameTemp}${plain}"
|
||||
echo -e "${green}password:${passwordTemp}${plain}"
|
||||
echo -e "${green}Username: ${config_account}${plain}"
|
||||
echo -e "${green}Password: ${config_password}${plain}"
|
||||
echo -e "${green}Port: ${portTemp}${plain}"
|
||||
echo -e "${green}WebBasePath: ${config_webBasePath}${plain}"
|
||||
echo -e "###############################################"
|
||||
echo -e "${red}if you forgot your login info,you can type x-ui and then type 8 to check after installation${plain}"
|
||||
echo -e "${yellow}If you forgot your login info, you can type 'x-ui settings' to check after installation${plain}"
|
||||
else
|
||||
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 8 to check${plain}"
|
||||
echo -e "${yellow}This is your upgrade, keeping old settings. If you forgot your login info, you can type 'x-ui settings' to check${plain}"
|
||||
|
||||
local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}')
|
||||
|
||||
if [[ ${#existing_webBasePath} -lt 4 ]]; then
|
||||
echo -e "${yellow}WebBasePath is empty, generating a random one...${plain}"
|
||||
|
||||
/usr/local/x-ui/x-ui setting -webBasePath "${config_webBasePath}"
|
||||
echo -e "${green}New WebBasePath: ${config_webBasePath}${plain}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
/usr/local/x-ui/x-ui migrate
|
||||
}
|
||||
|
||||
@@ -151,24 +203,32 @@ install_x-ui() {
|
||||
cd /usr/local/
|
||||
|
||||
if [ $# == 0 ]; then
|
||||
last_version=$(curl -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
if [[ ! -n "$last_version" ]]; then
|
||||
echo -e "${red}Failed to fetch x-ui version, it maybe due to Github API restrictions, please try it later${plain}"
|
||||
tag_version=$(curl -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
if [[ ! -n "$tag_version" ]]; then
|
||||
echo -e "${red}Failed to fetch x-ui version, it may be due to GitHub API restrictions, please try it later${plain}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "Got x-ui latest version: ${last_version}, beginning the installation..."
|
||||
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
|
||||
echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..."
|
||||
wget -N --no-check-certificate -O /usr/local/x-ui-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz
|
||||
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
|
||||
fi
|
||||
else
|
||||
last_version=$1
|
||||
url="https://github.com/MHSanaei/3x-ui/releases/download/${last_version}/x-ui-linux-$(arch).tar.gz"
|
||||
tag_version=$1
|
||||
tag_version_numeric=${tag_version#v}
|
||||
min_version="2.3.5"
|
||||
|
||||
if [[ "$(printf '%s\n' "$min_version" "$tag_version_numeric" | sort -V | head -n1)" != "$min_version" ]]; then
|
||||
echo -e "${red}Please use a newer version (at least v2.3.5). Exiting installation.${plain}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
url="https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz"
|
||||
echo -e "Beginning to install x-ui $1"
|
||||
wget -N --no-check-certificate -O /usr/local/x-ui-linux-$(arch).tar.gz ${url}
|
||||
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 if the version exists ${plain}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
@@ -199,22 +259,25 @@ install_x-ui() {
|
||||
systemctl daemon-reload
|
||||
systemctl enable x-ui
|
||||
systemctl start x-ui
|
||||
echo -e "${green}x-ui ${last_version}${plain} installation finished, it is running now..."
|
||||
echo -e "${green}x-ui ${tag_version}${plain} installation finished, it is running now..."
|
||||
echo -e ""
|
||||
echo -e "x-ui control menu usages: "
|
||||
echo -e "----------------------------------------------"
|
||||
echo -e "x-ui - Enter Admin menu"
|
||||
echo -e "x-ui start - Start x-ui"
|
||||
echo -e "x-ui stop - Stop x-ui"
|
||||
echo -e "x-ui restart - Restart x-ui"
|
||||
echo -e "x-ui status - Show x-ui status"
|
||||
echo -e "x-ui enable - Enable x-ui on system startup"
|
||||
echo -e "x-ui disable - Disable x-ui on system startup"
|
||||
echo -e "x-ui log - Check x-ui logs"
|
||||
echo -e "SUBCOMMANDS:"
|
||||
echo -e "x-ui - Admin Management Script"
|
||||
echo -e "x-ui start - Start"
|
||||
echo -e "x-ui stop - Stop"
|
||||
echo -e "x-ui restart - Restart"
|
||||
echo -e "x-ui status - Current Status"
|
||||
echo -e "x-ui settings - Current Settings"
|
||||
echo -e "x-ui enable - Enable Autostart on OS Startup"
|
||||
echo -e "x-ui disable - Disable Autostart on OS Startup"
|
||||
echo -e "x-ui log - Check logs"
|
||||
echo -e "x-ui banlog - Check Fail2ban ban logs"
|
||||
echo -e "x-ui update - Update x-ui"
|
||||
echo -e "x-ui install - Install x-ui"
|
||||
echo -e "x-ui uninstall - Uninstall x-ui"
|
||||
echo -e "x-ui update - Update"
|
||||
echo -e "x-ui custom - custom version"
|
||||
echo -e "x-ui install - Install"
|
||||
echo -e "x-ui uninstall - Uninstall"
|
||||
echo -e "----------------------------------------------"
|
||||
}
|
||||
|
||||
|
||||
182
main.go
@@ -21,7 +21,7 @@ import (
|
||||
)
|
||||
|
||||
func runWebServer() {
|
||||
log.Printf("%v %v", config.GetName(), config.GetVersion())
|
||||
log.Printf("Starting %v %v", config.GetName(), config.GetVersion())
|
||||
|
||||
switch config.GetLogLevel() {
|
||||
case config.Debug:
|
||||
@@ -35,31 +35,29 @@ func runWebServer() {
|
||||
case config.Error:
|
||||
logger.InitLogger(logging.ERROR)
|
||||
default:
|
||||
log.Fatal("unknown log level:", config.GetLogLevel())
|
||||
log.Fatalf("Unknown log level: %v", config.GetLogLevel())
|
||||
}
|
||||
|
||||
err := database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("Error initializing database: %v", err)
|
||||
}
|
||||
|
||||
var server *web.Server
|
||||
|
||||
server = web.NewServer()
|
||||
global.SetWebServer(server)
|
||||
err = server.Start()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
log.Fatalf("Error starting web server: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var subServer *sub.Server
|
||||
subServer = sub.NewServer()
|
||||
global.SetSubServer(subServer)
|
||||
|
||||
err = subServer.Start()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
log.Fatalf("Error starting sub server: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -71,34 +69,39 @@ func runWebServer() {
|
||||
|
||||
switch sig {
|
||||
case syscall.SIGHUP:
|
||||
logger.Info("Received SIGHUP signal. Restarting servers...")
|
||||
|
||||
err := server.Stop()
|
||||
if err != nil {
|
||||
logger.Warning("stop server err:", err)
|
||||
logger.Debug("Error stopping web server:", err)
|
||||
}
|
||||
err = subServer.Stop()
|
||||
if err != nil {
|
||||
logger.Warning("stop server err:", err)
|
||||
logger.Debug("Error stopping sub server:", err)
|
||||
}
|
||||
|
||||
server = web.NewServer()
|
||||
global.SetWebServer(server)
|
||||
err = server.Start()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
log.Fatalf("Error restarting web server: %v", err)
|
||||
return
|
||||
}
|
||||
log.Println("Web server restarted successfully.")
|
||||
|
||||
subServer = sub.NewServer()
|
||||
global.SetSubServer(subServer)
|
||||
|
||||
err = subServer.Start()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
log.Fatalf("Error restarting sub server: %v", err)
|
||||
return
|
||||
}
|
||||
log.Println("Sub server restarted successfully.")
|
||||
|
||||
default:
|
||||
server.Stop()
|
||||
subServer.Stop()
|
||||
log.Println("Shutting down servers.")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -107,16 +110,16 @@ func runWebServer() {
|
||||
func resetSetting() {
|
||||
err := database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Println("Failed to initialize database:", err)
|
||||
return
|
||||
}
|
||||
|
||||
settingService := service.SettingService{}
|
||||
err = settingService.ResetSettings()
|
||||
if err != nil {
|
||||
fmt.Println("reset setting failed:", err)
|
||||
fmt.Println("Failed to reset settings:", err)
|
||||
} else {
|
||||
fmt.Println("reset setting success")
|
||||
fmt.Println("Settings successfully reset.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,40 +128,53 @@ func showSetting(show bool) {
|
||||
settingService := service.SettingService{}
|
||||
port, err := settingService.GetPort()
|
||||
if err != nil {
|
||||
fmt.Println("get current port failed,error info:", err)
|
||||
fmt.Println("get current port failed, error info:", err)
|
||||
}
|
||||
|
||||
webBasePath, err := settingService.GetBasePath()
|
||||
if err != nil {
|
||||
fmt.Println("get webBasePath failed, error info:", err)
|
||||
}
|
||||
|
||||
userService := service.UserService{}
|
||||
userModel, err := userService.GetFirstUser()
|
||||
if err != nil {
|
||||
fmt.Println("get current user info failed,error info:", err)
|
||||
fmt.Println("get current user info failed, error info:", err)
|
||||
}
|
||||
|
||||
username := userModel.Username
|
||||
userpasswd := userModel.Password
|
||||
if (username == "") || (userpasswd == "") {
|
||||
if username == "" || userpasswd == "" {
|
||||
fmt.Println("current username or password is empty")
|
||||
}
|
||||
|
||||
fmt.Println("current panel settings as follows:")
|
||||
fmt.Println("username:", username)
|
||||
fmt.Println("userpasswd:", userpasswd)
|
||||
fmt.Println("password:", userpasswd)
|
||||
fmt.Println("port:", port)
|
||||
if webBasePath != "" {
|
||||
fmt.Println("webBasePath:", webBasePath)
|
||||
} else {
|
||||
fmt.Println("webBasePath is not set")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateTgbotEnableSts(status bool) {
|
||||
settingService := service.SettingService{}
|
||||
currentTgSts, err := settingService.GetTgbotenabled()
|
||||
currentTgSts, err := settingService.GetTgbotEnabled()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
logger.Infof("current enabletgbot status[%v],need update to status[%v]", currentTgSts, status)
|
||||
if currentTgSts != status {
|
||||
err := settingService.SetTgbotenabled(status)
|
||||
err := settingService.SetTgbotEnabled(status)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
} else {
|
||||
logger.Infof("SetTgbotenabled[%v] success", status)
|
||||
logger.Infof("SetTgbotEnabled[%v] success", status)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,7 +182,7 @@ func updateTgbotEnableSts(status bool) {
|
||||
func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) {
|
||||
err := database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Println("Error initializing database:", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -175,59 +191,93 @@ func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime stri
|
||||
if tgBotToken != "" {
|
||||
err := settingService.SetTgBotToken(tgBotToken)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Printf("Error setting Telegram bot token: %v\n", err)
|
||||
return
|
||||
} else {
|
||||
logger.Info("updateTgbotSetting tgBotToken success")
|
||||
}
|
||||
logger.Info("Successfully updated Telegram bot token.")
|
||||
}
|
||||
|
||||
if tgBotRuntime != "" {
|
||||
err := settingService.SetTgbotRuntime(tgBotRuntime)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Printf("Error setting Telegram bot runtime: %v\n", err)
|
||||
return
|
||||
} else {
|
||||
logger.Infof("updateTgbotSetting tgBotRuntime[%s] success", tgBotRuntime)
|
||||
}
|
||||
logger.Infof("Successfully updated Telegram bot runtime to [%s].", tgBotRuntime)
|
||||
}
|
||||
|
||||
if tgBotChatid != "" {
|
||||
err := settingService.SetTgBotChatId(tgBotChatid)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Printf("Error setting Telegram bot chat ID: %v\n", err)
|
||||
return
|
||||
}
|
||||
logger.Info("Successfully updated Telegram bot chat ID.")
|
||||
}
|
||||
}
|
||||
|
||||
func updateSetting(port int, username string, password string, webBasePath string) {
|
||||
err := database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
fmt.Println("Database initialization failed:", err)
|
||||
return
|
||||
}
|
||||
|
||||
settingService := service.SettingService{}
|
||||
userService := service.UserService{}
|
||||
|
||||
if port > 0 {
|
||||
err := settingService.SetPort(port)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to set port:", err)
|
||||
} else {
|
||||
logger.Info("updateTgbotSetting tgBotChatid success")
|
||||
fmt.Printf("Port set successfully: %v\n", port)
|
||||
}
|
||||
}
|
||||
|
||||
if username != "" || password != "" {
|
||||
err := userService.UpdateFirstUser(username, password)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to update username and password:", err)
|
||||
} else {
|
||||
fmt.Println("Username and password updated successfully")
|
||||
}
|
||||
}
|
||||
|
||||
if webBasePath != "" {
|
||||
err := settingService.SetBasePath(webBasePath)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to set base URI path:", err)
|
||||
} else {
|
||||
fmt.Println("Base URI path set successfully")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateSetting(port int, username string, password string) {
|
||||
func updateCert(publicKey string, privateKey string) {
|
||||
err := database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
settingService := service.SettingService{}
|
||||
if (privateKey != "" && publicKey != "") || (privateKey == "" && publicKey == "") {
|
||||
settingService := service.SettingService{}
|
||||
err = settingService.SetCertFile(publicKey)
|
||||
if err != nil {
|
||||
fmt.Println("set certificate public key failed:", err)
|
||||
} else {
|
||||
fmt.Println("set certificate public key success")
|
||||
}
|
||||
|
||||
if port > 0 {
|
||||
err := settingService.SetPort(port)
|
||||
err = settingService.SetKeyFile(privateKey)
|
||||
if err != nil {
|
||||
fmt.Println("set port failed:", err)
|
||||
fmt.Println("set certificate private key failed:", err)
|
||||
} else {
|
||||
fmt.Printf("set port %v success", port)
|
||||
}
|
||||
}
|
||||
if username != "" || password != "" {
|
||||
userService := service.UserService{}
|
||||
err := userService.UpdateFirstUser(username, password)
|
||||
if err != nil {
|
||||
fmt.Println("set username and password failed:", err)
|
||||
} else {
|
||||
fmt.Println("set username and password success")
|
||||
fmt.Println("set certificate private key success")
|
||||
}
|
||||
} else {
|
||||
fmt.Println("both public and private key should be entered.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,6 +338,9 @@ func main() {
|
||||
var port int
|
||||
var username string
|
||||
var password string
|
||||
var webBasePath string
|
||||
var webCertFile string
|
||||
var webKeyFile string
|
||||
var tgbottoken string
|
||||
var tgbotchatid string
|
||||
var enabletgbot bool
|
||||
@@ -295,16 +348,19 @@ func main() {
|
||||
var reset bool
|
||||
var show bool
|
||||
var remove_secret bool
|
||||
settingCmd.BoolVar(&reset, "reset", false, "reset all settings")
|
||||
settingCmd.BoolVar(&show, "show", false, "show current settings")
|
||||
settingCmd.BoolVar(&remove_secret, "remove_secret", false, "remove secret")
|
||||
settingCmd.IntVar(&port, "port", 0, "set panel port")
|
||||
settingCmd.StringVar(&username, "username", "", "set login username")
|
||||
settingCmd.StringVar(&password, "password", "", "set login password")
|
||||
settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "set telegram bot token")
|
||||
settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "set telegram bot cron time")
|
||||
settingCmd.StringVar(&tgbotchatid, "tgbotchatid", "", "set telegram bot chat id")
|
||||
settingCmd.BoolVar(&enabletgbot, "enabletgbot", false, "enable telegram bot notify")
|
||||
settingCmd.BoolVar(&reset, "reset", false, "Reset all settings")
|
||||
settingCmd.BoolVar(&show, "show", false, "Display current settings")
|
||||
settingCmd.BoolVar(&remove_secret, "remove_secret", false, "Remove secret key")
|
||||
settingCmd.IntVar(&port, "port", 0, "Set panel port number")
|
||||
settingCmd.StringVar(&username, "username", "", "Set login username")
|
||||
settingCmd.StringVar(&password, "password", "", "Set login password")
|
||||
settingCmd.StringVar(&webBasePath, "webBasePath", "", "Set base path for Panel")
|
||||
settingCmd.StringVar(&webCertFile, "webCert", "", "Set path to public key file for panel")
|
||||
settingCmd.StringVar(&webKeyFile, "webCertKey", "", "Set path to private key file for panel")
|
||||
settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "Set token for Telegram bot")
|
||||
settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "Set cron time for Telegram bot notifications")
|
||||
settingCmd.StringVar(&tgbotchatid, "tgbotchatid", "", "Set chat ID for Telegram bot notifications")
|
||||
settingCmd.BoolVar(&enabletgbot, "enabletgbot", false, "Enable notifications via Telegram bot")
|
||||
|
||||
oldUsage := flag.Usage
|
||||
flag.Usage = func() {
|
||||
@@ -341,7 +397,7 @@ func main() {
|
||||
if reset {
|
||||
resetSetting()
|
||||
} else {
|
||||
updateSetting(port, username, password)
|
||||
updateSetting(port, username, password, webBasePath)
|
||||
}
|
||||
if show {
|
||||
showSetting(show)
|
||||
@@ -355,6 +411,18 @@ func main() {
|
||||
if enabletgbot {
|
||||
updateTgbotEnableSts(enabletgbot)
|
||||
}
|
||||
case "cert":
|
||||
err := settingCmd.Parse(os.Args[2:])
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
if reset {
|
||||
updateCert("", "")
|
||||
} else {
|
||||
updateCert(webCertFile, webKeyFile)
|
||||
}
|
||||
|
||||
default:
|
||||
fmt.Println("Invalid subcommands")
|
||||
fmt.Println()
|
||||
|
||||
BIN
media/1.png
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 59 KiB |
BIN
media/2.png
|
Before Width: | Height: | Size: 214 KiB After Width: | Height: | Size: 91 KiB |
BIN
media/3.png
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 35 KiB |
BIN
media/4.png
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 26 KiB |
BIN
media/5.png
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 71 KiB |
BIN
media/6.png
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 42 KiB |
BIN
media/7.png
|
Before Width: | Height: | Size: 261 KiB After Width: | Height: | Size: 60 KiB |
BIN
media/buymeacoffe.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
@@ -23,6 +23,7 @@
|
||||
"destOverride": [
|
||||
"http",
|
||||
"tls",
|
||||
"quic",
|
||||
"fakedns"
|
||||
],
|
||||
"enabled": true
|
||||
|
||||
15
sub/sub.go
@@ -92,6 +92,11 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
SubJsonFragment = ""
|
||||
}
|
||||
|
||||
SubJsonNoises, err := s.settingService.GetSubJsonNoises()
|
||||
if err != nil {
|
||||
SubJsonNoises = ""
|
||||
}
|
||||
|
||||
SubJsonMux, err := s.settingService.GetSubJsonMux()
|
||||
if err != nil {
|
||||
SubJsonMux = ""
|
||||
@@ -106,7 +111,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
|
||||
s.sub = NewSUBController(
|
||||
g, LinksPath, JsonPath, Encrypt, ShowInfo, RemarkModel, SubUpdates,
|
||||
SubJsonFragment, SubJsonMux, SubJsonRules)
|
||||
SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules)
|
||||
|
||||
return engine, nil
|
||||
}
|
||||
@@ -163,13 +168,13 @@ func (s *Server) Start() (err error) {
|
||||
}
|
||||
listener = network.NewAutoHttpsListener(listener)
|
||||
listener = tls.NewListener(listener, c)
|
||||
logger.Info("sub server run https on", listener.Addr())
|
||||
logger.Info("Sub server running HTTPS on", listener.Addr())
|
||||
} else {
|
||||
logger.Error("error in loading certificates: ", err)
|
||||
logger.Info("sub server run http on", listener.Addr())
|
||||
logger.Error("Error loading certificates:", err)
|
||||
logger.Info("Sub server running HTTP on", listener.Addr())
|
||||
}
|
||||
} else {
|
||||
logger.Info("sub server run http on", listener.Addr())
|
||||
logger.Info("Sub server running HTTP on", listener.Addr())
|
||||
}
|
||||
s.listener = listener
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package sub
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -26,6 +27,7 @@ func NewSUBController(
|
||||
rModel string,
|
||||
update string,
|
||||
jsonFragment string,
|
||||
jsonNoise string,
|
||||
jsonMux string,
|
||||
jsonRules string,
|
||||
) *SUBController {
|
||||
@@ -37,7 +39,7 @@ func NewSUBController(
|
||||
updateInterval: update,
|
||||
|
||||
subService: sub,
|
||||
subJsonService: NewSubJsonService(jsonFragment, jsonMux, jsonRules, sub),
|
||||
subJsonService: NewSubJsonService(jsonFragment, jsonNoise, jsonMux, jsonRules, sub),
|
||||
}
|
||||
a.initRouter(g)
|
||||
return a
|
||||
@@ -54,7 +56,20 @@ func (a *SUBController) initRouter(g *gin.RouterGroup) {
|
||||
|
||||
func (a *SUBController) subs(c *gin.Context) {
|
||||
subId := c.Param("subid")
|
||||
host := strings.Split(c.Request.Host, ":")[0]
|
||||
var host string
|
||||
if h, err := getHostFromXFH(c.GetHeader("X-Forwarded-Host")); err == nil {
|
||||
host = h
|
||||
}
|
||||
if host == "" {
|
||||
host = c.GetHeader("X-Real-IP")
|
||||
}
|
||||
if host == "" {
|
||||
var err error
|
||||
host, _, err = net.SplitHostPort(c.Request.Host)
|
||||
if err != nil {
|
||||
host = c.Request.Host
|
||||
}
|
||||
}
|
||||
subs, header, err := a.subService.GetSubs(subId, host)
|
||||
if err != nil || len(subs) == 0 {
|
||||
c.String(400, "Error!")
|
||||
@@ -79,7 +94,20 @@ func (a *SUBController) subs(c *gin.Context) {
|
||||
|
||||
func (a *SUBController) subJsons(c *gin.Context) {
|
||||
subId := c.Param("subid")
|
||||
host := strings.Split(c.Request.Host, ":")[0]
|
||||
var host string
|
||||
if h, err := getHostFromXFH(c.GetHeader("X-Forwarded-Host")); err == nil {
|
||||
host = h
|
||||
}
|
||||
if host == "" {
|
||||
host = c.GetHeader("X-Real-IP")
|
||||
}
|
||||
if host == "" {
|
||||
var err error
|
||||
host, _, err = net.SplitHostPort(c.Request.Host)
|
||||
if err != nil {
|
||||
host = c.Request.Host
|
||||
}
|
||||
}
|
||||
jsonSub, header, err := a.subJsonService.GetJson(subId, host)
|
||||
if err != nil || len(jsonSub) == 0 {
|
||||
c.String(400, "Error!")
|
||||
@@ -93,3 +121,14 @@ func (a *SUBController) subJsons(c *gin.Context) {
|
||||
c.String(200, jsonSub)
|
||||
}
|
||||
}
|
||||
|
||||
func getHostFromXFH(s string) (string, error) {
|
||||
if strings.Contains(s, ":") {
|
||||
realHost, _, err := net.SplitHostPort(s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return realHost, nil
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
@@ -21,13 +21,14 @@ type SubJsonService struct {
|
||||
configJson map[string]interface{}
|
||||
defaultOutbounds []json_util.RawMessage
|
||||
fragment string
|
||||
noises string
|
||||
mux string
|
||||
|
||||
inboundService service.InboundService
|
||||
SubService *SubService
|
||||
}
|
||||
|
||||
func NewSubJsonService(fragment string, mux string, rules string, subService *SubService) *SubJsonService {
|
||||
func NewSubJsonService(fragment string, noises string, mux string, rules string, subService *SubService) *SubJsonService {
|
||||
var configJson map[string]interface{}
|
||||
var defaultOutbounds []json_util.RawMessage
|
||||
json.Unmarshal([]byte(defaultJson), &configJson)
|
||||
@@ -52,10 +53,15 @@ func NewSubJsonService(fragment string, mux string, rules string, subService *Su
|
||||
defaultOutbounds = append(defaultOutbounds, json_util.RawMessage(fragment))
|
||||
}
|
||||
|
||||
if noises != "" {
|
||||
defaultOutbounds = append(defaultOutbounds, json_util.RawMessage(noises))
|
||||
}
|
||||
|
||||
return &SubJsonService{
|
||||
configJson: configJson,
|
||||
defaultOutbounds: defaultOutbounds,
|
||||
fragment: fragment,
|
||||
noises: noises,
|
||||
mux: mux,
|
||||
SubService: subService,
|
||||
}
|
||||
@@ -211,7 +217,7 @@ func (s *SubJsonService) streamData(stream string) map[string]interface{} {
|
||||
delete(streamSettings, "sockopt")
|
||||
|
||||
if s.fragment != "" {
|
||||
streamSettings["sockopt"] = json_util.RawMessage(`{"dialerProxy": "fragment", "tcpKeepAliveIdle": 100, "tcpNoDelay": true}`)
|
||||
streamSettings["sockopt"] = json_util.RawMessage(`{"dialerProxy": "fragment", "tcpKeepAliveIdle": 100, "tcpMptcp": true, "tcpNoDelay": true}`)
|
||||
}
|
||||
|
||||
// remove proxy protocol
|
||||
@@ -224,7 +230,6 @@ func (s *SubJsonService) streamData(stream string) map[string]interface{} {
|
||||
case "httpupgrade":
|
||||
streamSettings["httpupgradeSettings"] = s.removeAcceptProxy(streamSettings["httpupgradeSettings"])
|
||||
}
|
||||
|
||||
return streamSettings
|
||||
}
|
||||
|
||||
@@ -283,6 +288,9 @@ func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_ut
|
||||
|
||||
usersData[0].ID = client.ID
|
||||
usersData[0].Level = 8
|
||||
if inbound.Protocol == model.VMESS {
|
||||
usersData[0].Security = client.Security
|
||||
}
|
||||
if inbound.Protocol == model.VLESS {
|
||||
usersData[0].Flow = client.Flow
|
||||
usersData[0].Encryption = "none"
|
||||
@@ -372,6 +380,7 @@ type UserVnext struct {
|
||||
Encryption string `json:"encryption,omitempty"`
|
||||
Flow string `json:"flow,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Security string `json:"security,omitempty"`
|
||||
Level int `json:"level"`
|
||||
}
|
||||
|
||||
|
||||
@@ -168,7 +168,7 @@ func (s *SubService) getLink(inbound *model.Inbound, email string) string {
|
||||
}
|
||||
|
||||
func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||
if inbound.Protocol != model.VMess {
|
||||
if inbound.Protocol != model.VMESS {
|
||||
return ""
|
||||
}
|
||||
obj := map[string]interface{}{
|
||||
@@ -202,24 +202,17 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||
case "ws":
|
||||
ws, _ := stream["wsSettings"].(map[string]interface{})
|
||||
obj["path"] = ws["path"].(string)
|
||||
obj["host"] = ws["host"].(string)
|
||||
if headers, ok := ws["headers"].(map[string]interface{}); ok {
|
||||
hostFromHeaders := searchHost(headers)
|
||||
if hostFromHeaders != "" {
|
||||
obj["host"] = hostFromHeaders
|
||||
}
|
||||
if host, ok := ws["host"].(string); ok && len(host) > 0 {
|
||||
obj["host"] = host
|
||||
} else {
|
||||
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)
|
||||
@@ -230,12 +223,20 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||
case "httpupgrade":
|
||||
httpupgrade, _ := stream["httpupgradeSettings"].(map[string]interface{})
|
||||
obj["path"] = httpupgrade["path"].(string)
|
||||
obj["host"] = httpupgrade["host"].(string)
|
||||
if headers, ok := httpupgrade["headers"].(map[string]interface{}); ok {
|
||||
hostFromHeaders := searchHost(headers)
|
||||
if hostFromHeaders != "" {
|
||||
obj["host"] = hostFromHeaders
|
||||
}
|
||||
if host, ok := httpupgrade["host"].(string); ok && len(host) > 0 {
|
||||
obj["host"] = host
|
||||
} else {
|
||||
headers, _ := httpupgrade["headers"].(map[string]interface{})
|
||||
obj["host"] = searchHost(headers)
|
||||
}
|
||||
case "splithttp":
|
||||
splithttp, _ := stream["splithttpSettings"].(map[string]interface{})
|
||||
obj["path"] = splithttp["path"].(string)
|
||||
if host, ok := splithttp["host"].(string); ok && len(host) > 0 {
|
||||
obj["host"] = host
|
||||
} else {
|
||||
headers, _ := splithttp["headers"].(map[string]interface{})
|
||||
obj["host"] = searchHost(headers)
|
||||
}
|
||||
}
|
||||
security, _ := stream["security"].(string)
|
||||
@@ -274,6 +275,7 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||
}
|
||||
}
|
||||
obj["id"] = clients[clientIndex].ID
|
||||
obj["scy"] = clients[clientIndex].Security
|
||||
|
||||
externalProxies, _ := stream["externalProxy"].([]interface{})
|
||||
|
||||
@@ -352,24 +354,16 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
case "ws":
|
||||
ws, _ := stream["wsSettings"].(map[string]interface{})
|
||||
params["path"] = ws["path"].(string)
|
||||
params["host"] = ws["host"].(string)
|
||||
headers, _ := ws["headers"].(map[string]interface{})
|
||||
if headers != nil {
|
||||
hostFromHeaders := searchHost(headers)
|
||||
if hostFromHeaders != "" {
|
||||
params["host"] = hostFromHeaders
|
||||
}
|
||||
if host, ok := ws["host"].(string); ok && len(host) > 0 {
|
||||
params["host"] = host
|
||||
} else {
|
||||
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)
|
||||
@@ -380,13 +374,20 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
case "httpupgrade":
|
||||
httpupgrade, _ := stream["httpupgradeSettings"].(map[string]interface{})
|
||||
params["path"] = httpupgrade["path"].(string)
|
||||
params["host"] = httpupgrade["host"].(string)
|
||||
headers, _ := httpupgrade["headers"].(map[string]interface{})
|
||||
if headers != nil {
|
||||
hostFromHeaders := searchHost(headers)
|
||||
if hostFromHeaders != "" {
|
||||
params["host"] = hostFromHeaders
|
||||
}
|
||||
if host, ok := httpupgrade["host"].(string); ok && len(host) > 0 {
|
||||
params["host"] = host
|
||||
} else {
|
||||
headers, _ := httpupgrade["headers"].(map[string]interface{})
|
||||
params["host"] = searchHost(headers)
|
||||
}
|
||||
case "splithttp":
|
||||
splithttp, _ := stream["splithttpSettings"].(map[string]interface{})
|
||||
params["path"] = splithttp["path"].(string)
|
||||
if host, ok := splithttp["host"].(string); ok && len(host) > 0 {
|
||||
params["host"] = host
|
||||
} else {
|
||||
headers, _ := splithttp["headers"].(map[string]interface{})
|
||||
params["host"] = searchHost(headers)
|
||||
}
|
||||
}
|
||||
security, _ := stream["security"].(string)
|
||||
@@ -581,24 +582,16 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
||||
case "ws":
|
||||
ws, _ := stream["wsSettings"].(map[string]interface{})
|
||||
params["path"] = ws["path"].(string)
|
||||
params["host"] = ws["host"].(string)
|
||||
headers, _ := ws["headers"].(map[string]interface{})
|
||||
if headers != nil {
|
||||
hostFromHeaders := searchHost(headers)
|
||||
if hostFromHeaders != "" {
|
||||
params["host"] = hostFromHeaders
|
||||
}
|
||||
if host, ok := ws["host"].(string); ok && len(host) > 0 {
|
||||
params["host"] = host
|
||||
} else {
|
||||
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)
|
||||
@@ -609,13 +602,20 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
||||
case "httpupgrade":
|
||||
httpupgrade, _ := stream["httpupgradeSettings"].(map[string]interface{})
|
||||
params["path"] = httpupgrade["path"].(string)
|
||||
params["host"] = httpupgrade["host"].(string)
|
||||
headers, _ := httpupgrade["headers"].(map[string]interface{})
|
||||
if headers != nil {
|
||||
hostFromHeaders := searchHost(headers)
|
||||
if hostFromHeaders != "" {
|
||||
params["host"] = hostFromHeaders
|
||||
}
|
||||
if host, ok := httpupgrade["host"].(string); ok && len(host) > 0 {
|
||||
params["host"] = host
|
||||
} else {
|
||||
headers, _ := httpupgrade["headers"].(map[string]interface{})
|
||||
params["host"] = searchHost(headers)
|
||||
}
|
||||
case "splithttp":
|
||||
splithttp, _ := stream["splithttpSettings"].(map[string]interface{})
|
||||
params["path"] = splithttp["path"].(string)
|
||||
if host, ok := splithttp["host"].(string); ok && len(host) > 0 {
|
||||
params["host"] = host
|
||||
} else {
|
||||
headers, _ := splithttp["headers"].(map[string]interface{})
|
||||
params["host"] = searchHost(headers)
|
||||
}
|
||||
}
|
||||
security, _ := stream["security"].(string)
|
||||
@@ -811,24 +811,16 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
|
||||
case "ws":
|
||||
ws, _ := stream["wsSettings"].(map[string]interface{})
|
||||
params["path"] = ws["path"].(string)
|
||||
params["host"] = ws["host"].(string)
|
||||
headers, _ := ws["headers"].(map[string]interface{})
|
||||
if headers != nil {
|
||||
hostFromHeaders := searchHost(headers)
|
||||
if hostFromHeaders != "" {
|
||||
params["host"] = hostFromHeaders
|
||||
}
|
||||
if host, ok := ws["host"].(string); ok && len(host) > 0 {
|
||||
params["host"] = host
|
||||
} else {
|
||||
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)
|
||||
@@ -839,13 +831,20 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
|
||||
case "httpupgrade":
|
||||
httpupgrade, _ := stream["httpupgradeSettings"].(map[string]interface{})
|
||||
params["path"] = httpupgrade["path"].(string)
|
||||
params["host"] = httpupgrade["host"].(string)
|
||||
headers, _ := httpupgrade["headers"].(map[string]interface{})
|
||||
if headers != nil {
|
||||
hostFromHeaders := searchHost(headers)
|
||||
if hostFromHeaders != "" {
|
||||
params["host"] = hostFromHeaders
|
||||
}
|
||||
if host, ok := httpupgrade["host"].(string); ok && len(host) > 0 {
|
||||
params["host"] = host
|
||||
} else {
|
||||
headers, _ := httpupgrade["headers"].(map[string]interface{})
|
||||
params["host"] = searchHost(headers)
|
||||
}
|
||||
case "splithttp":
|
||||
splithttp, _ := stream["splithttpSettings"].(map[string]interface{})
|
||||
params["path"] = splithttp["path"].(string)
|
||||
if host, ok := splithttp["host"].(string); ok && len(host) > 0 {
|
||||
params["host"] = host
|
||||
} else {
|
||||
headers, _ := splithttp["headers"].(map[string]interface{})
|
||||
params["host"] = searchHost(headers)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -985,9 +984,36 @@ func (s *SubService) genRemark(inbound *model.Inbound, email string, extra strin
|
||||
now := time.Now().Unix()
|
||||
switch exp := stats.ExpiryTime / 1000; {
|
||||
case exp > 0:
|
||||
remark = append(remark, fmt.Sprintf("%d%s⏳", (exp-now)/86400, "Days"))
|
||||
remainingSeconds := exp - now
|
||||
days := remainingSeconds / 86400
|
||||
hours := (remainingSeconds % 86400) / 3600
|
||||
minutes := (remainingSeconds % 3600) / 60
|
||||
if days > 0 {
|
||||
if hours > 0 {
|
||||
remark = append(remark, fmt.Sprintf("%dD,%dH⏳", days, hours))
|
||||
} else {
|
||||
remark = append(remark, fmt.Sprintf("%dD⏳", days))
|
||||
}
|
||||
} else if hours > 0 {
|
||||
remark = append(remark, fmt.Sprintf("%dH⏳", hours))
|
||||
} else {
|
||||
remark = append(remark, fmt.Sprintf("%dM⏳", minutes))
|
||||
}
|
||||
case exp < 0:
|
||||
remark = append(remark, fmt.Sprintf("%d%s⏳", exp/-86400, "Days"))
|
||||
days := exp / -86400
|
||||
hours := (exp % -86400) / 3600
|
||||
minutes := (exp % -3600) / 60
|
||||
if days > 0 {
|
||||
if hours > 0 {
|
||||
remark = append(remark, fmt.Sprintf("%dD,%dH⏳", days, hours))
|
||||
} else {
|
||||
remark = append(remark, fmt.Sprintf("%dD⏳", days))
|
||||
}
|
||||
} else if hours > 0 {
|
||||
remark = append(remark, fmt.Sprintf("%dH⏳", hours))
|
||||
} else {
|
||||
remark = append(remark, fmt.Sprintf("%dM⏳", minutes))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@ import (
|
||||
_ "unsafe"
|
||||
)
|
||||
|
||||
//go:linkname HostProc github.com/shirou/gopsutil/v3/internal/common.HostProc
|
||||
//go:linkname HostProc github.com/shirou/gopsutil/v4/internal/common.HostProc
|
||||
func HostProc(combineWith ...string) string
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
package sys
|
||||
|
||||
import (
|
||||
"github.com/shirou/gopsutil/v3/net"
|
||||
"github.com/shirou/gopsutil/v4/net"
|
||||
)
|
||||
|
||||
func GetTCPCount() (int, error) {
|
||||
|
||||
@@ -6,7 +6,7 @@ package sys
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/shirou/gopsutil/v3/net"
|
||||
"github.com/shirou/gopsutil/v4/net"
|
||||
)
|
||||
|
||||
func GetConnectionCount(proto string) (int, error) {
|
||||
|
||||
3
web/assets/axios/axios.min.js
vendored
1
web/assets/codemirror/codemirror.min.js
vendored
Normal file
@@ -1,86 +0,0 @@
|
||||
/*
|
||||
Copyright (C) 2011 by MarkLogic Corporation
|
||||
Author: Mike Brevoort <mike@brevoort.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
.cm-s-xq.CodeMirror { border-radius: 1.5rem; border: 1px solid #d9d9d9; height: auto; }
|
||||
.cm-s-xq.CodeMirror:hover { background-color: rgb(232 244 242); border-color: #18947b; transition: all .3s; }
|
||||
.cm-s-xq .CodeMirror-gutters { border-right: 1px solid #ddd; background-color: rgb(221 221 221 / 20%); white-space: nowrap; }
|
||||
.cm-s-xq span.cm-keyword { line-height: 1em; font-weight: bold; color: #5A5CAD; }
|
||||
.cm-s-xq span.cm-atom { color: #7A316F; font-weight:bold; }
|
||||
.cm-s-xq span.cm-number { color: #e36209; }
|
||||
.cm-s-xq span.cm-def { text-decoration:underline; }
|
||||
.cm-s-xq span.cm-variable { color: black; }
|
||||
.cm-s-xq span.cm-variable-2 { color:black; }
|
||||
.cm-s-xq span.cm-variable-3, .cm-s-xq span.cm-type { color: black; }
|
||||
.cm-s-xq span.cm-property { color: #008771; }
|
||||
.cm-s-xq span.cm-operator {}
|
||||
.cm-s-xq span.cm-comment { color: #bbbbbb; font-style: italic; }
|
||||
.cm-s-xq span.cm-string {}
|
||||
.cm-s-xq span.cm-meta { color: yellow; }
|
||||
.cm-s-xq span.cm-qualifier { color: grey; }
|
||||
.cm-s-xq span.cm-builtin { color: #7EA656; }
|
||||
.cm-s-xq span.cm-bracket { color: #cc7; }
|
||||
.cm-s-xq span.cm-tag { color: #3F7F7F; }
|
||||
.cm-s-xq span.cm-attribute { color: #7F007F; }
|
||||
.cm-s-xq span.cm-error { color: #e04141; }
|
||||
|
||||
.cm-s-xq .CodeMirror-activeline-background { background: #e8f2ff; }
|
||||
.cm-s-xq .CodeMirror-matchingbracket { outline:1px solid grey;color:black !important;background:yellow; }
|
||||
|
||||
.dark .cm-s-xq.CodeMirror { background-color: var(--dark-color-surface-200); border-color: var(--dark-color-surface-300); color: rgb(255 255 255 / 65%); }
|
||||
.dark .cm-s-xq.CodeMirror:hover { background-color: rgb(0 50 42 / 30%); border-color: #008771; transition: all .3s; }
|
||||
.dark .cm-s-xq div.CodeMirror-selected { background: var(--dark-color-codemirror-line-selection); }
|
||||
.dark .cm-s-xq .CodeMirror-line::selection, .dark .cm-s-xq .CodeMirror-line > span::selection, .dark .cm-s-xq .CodeMirror-line > span > span::selection { background: var(--dark-color-codemirror-line-selection); }
|
||||
.dark .cm-s-xq .CodeMirror-line::-moz-selection, .dark .cm-s-xq .CodeMirror-line > span::-moz-selection, .dark .cm-s-xq .CodeMirror-line > span > span::-moz-selection { background: var(--dark-color-codemirror-line-selection); }
|
||||
.dark .cm-s-xq .CodeMirror-gutters { background: rgb(0 0 0 / 30%); border-right: 1px solid var(--dark-color-surface-300); }
|
||||
.dark .cm-s-xq .CodeMirror-guttermarker { color: #FFBD40; }
|
||||
.dark .cm-s-xq .CodeMirror-guttermarker-subtle { color: rgb(255 255 255 / 70%); }
|
||||
.dark .cm-s-xq .CodeMirror-linenumber { color: rgb(255 255 255 / 50%); }
|
||||
.dark .cm-s-xq .CodeMirror-cursor { border-left: 1px solid white; }
|
||||
|
||||
.dark .cm-s-xq span.cm-keyword { color: #FFBD40; }
|
||||
.dark .cm-s-xq span.cm-atom { color: #c099ff; }
|
||||
.dark .cm-s-xq span.cm-number { color: #9ccfd8; }
|
||||
.dark .cm-s-xq span.cm-def { color: #FFF; text-decoration:underline; }
|
||||
.dark .cm-s-xq span.cm-variable { color: #FFF; }
|
||||
.dark .cm-s-xq span.cm-variable-2 { color: #EEE; }
|
||||
.dark .cm-s-xq span.cm-variable-3, .dark .cm-s-xq span.cm-type { color: #DDD; }
|
||||
.dark .cm-s-xq span.cm-property { color: #f6c177; }
|
||||
.dark .cm-s-xq span.cm-operator {}
|
||||
.dark .cm-s-xq span.cm-comment { color: gray; }
|
||||
.dark .cm-s-xq span.cm-string {}
|
||||
.dark .cm-s-xq span.cm-meta { color: yellow; }
|
||||
.dark .cm-s-xq span.cm-qualifier { color: #FFF700; }
|
||||
.dark .cm-s-xq span.cm-builtin { color: #30a; }
|
||||
.dark .cm-s-xq span.cm-bracket { color: #cc7; }
|
||||
.dark .cm-s-xq span.cm-tag { color: #FFBD40; }
|
||||
.dark .cm-s-xq span.cm-attribute { color: #FFF700; }
|
||||
.dark .cm-s-xq span.cm-error { color: #e04141; }
|
||||
|
||||
.dark .cm-s-xq .CodeMirror-activeline-background { background: #27282E; }
|
||||
.dark .cm-s-xq .CodeMirror-matchingbracket { outline:1px solid grey; color:white !important; }
|
||||
|
||||
.Line-Hover{transition: all .2s;}
|
||||
.Line-Hover:hover{ background-color: rgba(0, 102, 85, 0.05) !important; }
|
||||
.dark .Line-Hover:hover{ background-color: var(--dark-color-codemirror-line-hover) !important; }
|
||||
|
||||
.CodeMirror-foldmarker { color: #fc8800; text-shadow: #ffd8aa 1px 1px 2px, #ffd8aa -1px -1px 2px, #ffd8aa 1px -1px 2px, #ffd8aa -1px 1px 2px; font-family: arial; line-height: .3; cursor: pointer; }
|
||||
.dark .CodeMirror-foldmarker { color: #ffffff; text-shadow: #bbb 1px 1px 2px, #bbb -1px -1px 2px, #bbb 1px -1px 2px, #bbb -1px 1px 2px; font-family: arial; line-height: .3; cursor: pointer; }
|
||||
1
web/assets/codemirror/xq.min.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.CodeMirror{background-color:#f6fbfa;border:1px solid #d9d9d9}.cm-s-xq.CodeMirror{border-radius:1.5rem;height:auto;transition:background-color .3s,border-color 0.3s}.cm-s-xq.CodeMirror:hover{background-color:#e8f4f2;border-color:#18947b}.cm-s-xq div.CodeMirror-selected{background:rgb(0 102 85 / .1)}.cm-s-xq .CodeMirror-line::selection,.cm-s-xq .CodeMirror-line>span::selection,.cm-s-xq .CodeMirror-line>span>span::selection{background:rgb(0 102 85 / .1)}.cm-s-xq .CodeMirror-line::-moz-selection,.cm-s-xq .CodeMirror-line>span::-moz-selection,.cm-s-xq .CodeMirror-line>span>span::-moz-selection{background:rgb(0 102 85 / .1)}.cm-s-xq .CodeMirror-gutters{border-right:1px solid #ddd;background-color:rgb(221 221 221 / 20%);white-space:nowrap}.cm-s-xq span.cm-keyword{line-height:1em;font-weight:700;color:#5A5CAD}.cm-s-xq span.cm-atom{color:#7A316F;font-weight:700}.cm-s-xq span.cm-number{color:#e36209}.cm-s-xq span.cm-def{text-decoration:underline}.cm-s-xq span.cm-variable{color:#000}.cm-s-xq span.cm-variable-2{color:#000}.cm-s-xq span.cm-variable-3,.cm-s-xq span.cm-type{color:#000}.cm-s-xq span.cm-property{color:#008771}.cm-s-xq span.cm-comment{color:#bbb;font-style:italic}.cm-s-xq span.cm-meta{color:#ff0}.cm-s-xq span.cm-qualifier{color:grey}.cm-s-xq span.cm-builtin{color:#7EA656}.cm-s-xq span.cm-bracket{color:#cc7}.cm-s-xq span.cm-tag{color:#3F7F7F}.cm-s-xq span.cm-attribute{color:#7F007F}.cm-s-xq span.cm-error{color:#e04141}.cm-s-xq .CodeMirror-activeline-background{background:#e8f2ff}.cm-s-xq .CodeMirror-matchingbracket{outline:1px solid grey;color:black!important;background:#ff0}.dark .CodeMirror{background-color:var(--dark-color-surface-200);border-color:var(--dark-color-surface-300)}.dark .cm-s-xq.CodeMirror{background-color:var(--dark-color-surface-200);border-color:var(--dark-color-surface-300);color:rgb(255 255 255 / 65%)}.dark .cm-s-xq.CodeMirror:hover{background-color:rgb(0 50 42 / 30%);border-color:#008771}.dark .cm-s-xq div.CodeMirror-selected{background:var(--dark-color-codemirror-line-selection)}.dark .cm-s-xq .CodeMirror-line::selection,.dark .cm-s-xq .CodeMirror-line>span::selection,.dark .cm-s-xq .CodeMirror-line>span>span::selection{background:var(--dark-color-codemirror-line-selection)}.dark .cm-s-xq .CodeMirror-line::-moz-selection,.dark .cm-s-xq .CodeMirror-line>span::-moz-selection,.dark .cm-s-xq .CodeMirror-line>span>span::-moz-selection{background:var(--dark-color-codemirror-line-selection)}.dark .cm-s-xq .CodeMirror-gutters{background:rgb(0 0 0 / 30%);border-right:1px solid var(--dark-color-surface-300)}.dark .cm-s-xq .CodeMirror-guttermarker{color:#FFBD40}.dark .cm-s-xq .CodeMirror-guttermarker-subtle{color:rgb(255 255 255 / 70%)}.dark .cm-s-xq .CodeMirror-linenumber{color:rgb(255 255 255 / 50%)}.dark .cm-s-xq .CodeMirror-cursor{border-left:1px solid #fff}.dark .cm-s-xq span.cm-keyword{color:#FFBD40}.dark .cm-s-xq span.cm-atom{color:#c099ff}.dark .cm-s-xq span.cm-number{color:#9ccfd8}.dark .cm-s-xq span.cm-def{color:#FFF;text-decoration:underline}.dark .cm-s-xq span.cm-variable{color:#FFF}.dark .cm-s-xq span.cm-variable-2{color:#EEE}.dark .cm-s-xq span.cm-variable-3,.dark .cm-s-xq span.cm-type{color:#DDD}.dark .cm-s-xq span.cm-property{color:#f6c177}.dark .cm-s-xq span.cm-comment{color:gray}.dark .cm-s-xq span.cm-meta{color:#ff0}.dark .cm-s-xq span.cm-qualifier{color:#FFF700}.dark .cm-s-xq span.cm-builtin{color:#30a}.dark .cm-s-xq span.cm-bracket{color:#cc7}.dark .cm-s-xq span.cm-tag{color:#FFBD40}.dark .cm-s-xq span.cm-attribute{color:#FFF700}.dark .cm-s-xq span.cm-error{color:#e04141}.dark .cm-s-xq .CodeMirror-activeline-background{background:#27282E}.dark .cm-s-xq .CodeMirror-matchingbracket{outline:1px solid grey;color:white!important}.Line-Hover{transition:all .2s}.CodeMirror pre.CodeMirror-line:hover,.CodeMirror pre.CodeMirror-line-like:hover{background-color:rgb(0 102 85 / .05)}.dark .CodeMirror pre.CodeMirror-line:hover,.CodeMirror pre.CodeMirror-line-like:hover{background-color:var(--dark-color-codemirror-line-hover)}.CodeMirror-foldmarker{color:#fc8800;text-shadow:#ffd8aa 1px 1px 2px,#ffd8aa -1px -1px 2px,#ffd8aa 1px -1px 2px,#ffd8aa -1px 1px 2px;font-family:arial;line-height:.3;cursor:pointer}.dark .CodeMirror-foldmarker{color:#fff;text-shadow:#bbb 1px 1px 2px,#bbb -1px -1px 2px,#bbb 1px -1px 2px,#bbb -1px 1px 2px;font-family:arial;line-height:.3;cursor:pointer}
|
||||
1
web/assets/css/custom.min.css
vendored
Normal file
@@ -1,83 +1,98 @@
|
||||
const supportLangs = [
|
||||
{
|
||||
name: 'English',
|
||||
value: 'en-US',
|
||||
icon: '🇺🇸',
|
||||
},
|
||||
{
|
||||
name: 'فارسی',
|
||||
value: 'fa-IR',
|
||||
icon: '🇮🇷',
|
||||
},
|
||||
{
|
||||
name: '汉语',
|
||||
value: 'zh-Hans',
|
||||
icon: '🇨🇳',
|
||||
},
|
||||
{
|
||||
name: 'Русский',
|
||||
value: 'ru-RU',
|
||||
icon: '🇷🇺',
|
||||
},
|
||||
{
|
||||
name: 'Tiếng Việt',
|
||||
value: 'vi-VN',
|
||||
icon: '🇻🇳',
|
||||
},
|
||||
{
|
||||
name: 'Español',
|
||||
value: 'es-ES',
|
||||
icon: '🇪🇸',
|
||||
},
|
||||
{
|
||||
name: 'Indonesian',
|
||||
value: 'id-ID',
|
||||
icon: '🇮🇩',
|
||||
},
|
||||
{
|
||||
name: 'Український',
|
||||
value: 'uk-UA',
|
||||
icon: '🇺🇦',
|
||||
},
|
||||
{
|
||||
name: "English",
|
||||
value: "en-US",
|
||||
icon: "🇺🇸",
|
||||
},
|
||||
{
|
||||
name: "فارسی",
|
||||
value: "fa-IR",
|
||||
icon: "🇮🇷",
|
||||
},
|
||||
{
|
||||
name: "簡體中文",
|
||||
value: "zh-CN",
|
||||
icon: "🇨🇳",
|
||||
},
|
||||
{
|
||||
name: "繁體中文",
|
||||
value: "zh-TW",
|
||||
icon: "🇹🇼",
|
||||
},
|
||||
{
|
||||
name: "Русский",
|
||||
value: "ru-RU",
|
||||
icon: "🇷🇺",
|
||||
},
|
||||
{
|
||||
name: "Tiếng Việt",
|
||||
value: "vi-VN",
|
||||
icon: "🇻🇳",
|
||||
},
|
||||
{
|
||||
name: "Español",
|
||||
value: "es-ES",
|
||||
icon: "🇪🇸",
|
||||
},
|
||||
{
|
||||
name: "Indonesian",
|
||||
value: "id-ID",
|
||||
icon: "🇮🇩",
|
||||
},
|
||||
{
|
||||
name: "Український",
|
||||
value: "uk-UA",
|
||||
icon: "🇺🇦",
|
||||
},
|
||||
{
|
||||
name: "Türkçe",
|
||||
value: "tr-TR",
|
||||
icon: "🇹🇷",
|
||||
},
|
||||
{
|
||||
name: "Português",
|
||||
value: "pt-BR",
|
||||
icon: "🇧🇷",
|
||||
},
|
||||
];
|
||||
|
||||
function getLang() {
|
||||
let lang = getCookie('lang');
|
||||
let lang = getCookie("lang");
|
||||
|
||||
if (!lang) {
|
||||
if (window.navigator) {
|
||||
lang = window.navigator.language || window.navigator.userLanguage;
|
||||
if (!lang) {
|
||||
if (window.navigator) {
|
||||
lang = window.navigator.language || window.navigator.userLanguage;
|
||||
|
||||
if (isSupportLang(lang)) {
|
||||
setCookie('lang', lang, 150);
|
||||
} else {
|
||||
setCookie('lang', 'en-US', 150);
|
||||
window.location.reload();
|
||||
}
|
||||
} else {
|
||||
setCookie('lang', 'en-US', 150);
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
if (isSupportLang(lang)) {
|
||||
setCookie("lang", lang, 150);
|
||||
} else {
|
||||
setCookie("lang", "en-US", 150);
|
||||
window.location.reload();
|
||||
}
|
||||
} else {
|
||||
setCookie("lang", "en-US", 150);
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
return lang;
|
||||
return lang;
|
||||
}
|
||||
|
||||
function setLang(lang) {
|
||||
if (!isSupportLang(lang)) {
|
||||
lang = 'en-US';
|
||||
}
|
||||
if (!isSupportLang(lang)) {
|
||||
lang = "en-US";
|
||||
}
|
||||
|
||||
setCookie('lang', lang, 150);
|
||||
window.location.reload();
|
||||
setCookie("lang", lang, 150);
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function isSupportLang(lang) {
|
||||
for (l of supportLangs) {
|
||||
if (l.value === lang) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (l of supportLangs) {
|
||||
if (l.value === lang) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -69,12 +69,23 @@ const WireguardDomainStrategy = [
|
||||
"ForceIPv6v4"
|
||||
];
|
||||
|
||||
const USERS_SECURITY = {
|
||||
AES_128_GCM: "aes-128-gcm",
|
||||
CHACHA20_POLY1305: "chacha20-poly1305",
|
||||
AUTO: "auto",
|
||||
NONE: "none",
|
||||
ZERO: "zero",
|
||||
};
|
||||
|
||||
Object.freeze(Protocols);
|
||||
Object.freeze(SSMethods);
|
||||
Object.freeze(TLS_FLOW_CONTROL);
|
||||
Object.freeze(UTLS_FINGERPRINT);
|
||||
Object.freeze(ALPN_OPTION);
|
||||
Object.freeze(OutboundDomainStrategies);
|
||||
Object.freeze(WireguardDomainStrategy);
|
||||
Object.freeze(USERS_SECURITY);
|
||||
|
||||
|
||||
class CommonClass {
|
||||
|
||||
@@ -90,30 +101,30 @@ class CommonClass {
|
||||
return this;
|
||||
}
|
||||
|
||||
toString(format=true) {
|
||||
toString(format = true) {
|
||||
return format ? JSON.stringify(this.toJson(), null, 2) : JSON.stringify(this.toJson());
|
||||
}
|
||||
}
|
||||
|
||||
class TcpStreamSettings extends CommonClass {
|
||||
constructor(type='none', host, path) {
|
||||
constructor(type = 'none', host, path) {
|
||||
super();
|
||||
this.type = type;
|
||||
this.host = host;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
static fromJson(json = {}) {
|
||||
let header = json.header;
|
||||
if (!header) return new TcpStreamSettings();
|
||||
if(header.type == 'http' && header.request){
|
||||
if (header.type == 'http' && header.request) {
|
||||
return new TcpStreamSettings(
|
||||
header.type,
|
||||
header.request.headers.Host.join(','),
|
||||
header.request.path.join(','),
|
||||
);
|
||||
}
|
||||
return new TcpStreamSettings(header.type,'','');
|
||||
return new TcpStreamSettings(header.type, '', '');
|
||||
}
|
||||
|
||||
toJson() {
|
||||
@@ -132,15 +143,17 @@ class TcpStreamSettings extends CommonClass {
|
||||
}
|
||||
|
||||
class KcpStreamSettings extends CommonClass {
|
||||
constructor(mtu=1350, tti=20,
|
||||
uplinkCapacity=5,
|
||||
downlinkCapacity=20,
|
||||
congestion=false,
|
||||
readBufferSize=2,
|
||||
writeBufferSize=2,
|
||||
type='none',
|
||||
seed='',
|
||||
) {
|
||||
constructor(
|
||||
mtu = 1350,
|
||||
tti = 50,
|
||||
uplinkCapacity = 5,
|
||||
downlinkCapacity = 20,
|
||||
congestion = false,
|
||||
readBufferSize = 2,
|
||||
writeBufferSize = 2,
|
||||
type = 'none',
|
||||
seed = '',
|
||||
) {
|
||||
super();
|
||||
this.mtu = mtu;
|
||||
this.tti = tti;
|
||||
@@ -153,7 +166,7 @@ class KcpStreamSettings extends CommonClass {
|
||||
this.seed = seed;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
static fromJson(json = {}) {
|
||||
return new KcpStreamSettings(
|
||||
json.mtu,
|
||||
json.tti,
|
||||
@@ -185,16 +198,16 @@ class KcpStreamSettings extends CommonClass {
|
||||
}
|
||||
|
||||
class WsStreamSettings extends CommonClass {
|
||||
constructor(path='/', host='') {
|
||||
constructor(path = '/', host = '') {
|
||||
super();
|
||||
this.path = path;
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
static fromJson(json = {}) {
|
||||
return new WsStreamSettings(
|
||||
json.path,
|
||||
json.host
|
||||
json.host,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -202,19 +215,18 @@ class WsStreamSettings extends CommonClass {
|
||||
return {
|
||||
path: this.path,
|
||||
host: this.host,
|
||||
headers: ObjectUtil.isEmpty(this.host) ? undefined : {Host: this.host},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class HttpStreamSettings extends CommonClass {
|
||||
constructor(path='/', host='') {
|
||||
constructor(path = '/', host = '') {
|
||||
super();
|
||||
this.path = path;
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
static fromJson(json = {}) {
|
||||
return new HttpStreamSettings(
|
||||
json.path,
|
||||
json.host ? json.host.join(',') : '',
|
||||
@@ -230,15 +242,18 @@ class HttpStreamSettings extends CommonClass {
|
||||
}
|
||||
|
||||
class QuicStreamSettings extends CommonClass {
|
||||
constructor(security='none',
|
||||
key='', type='none') {
|
||||
constructor(
|
||||
security = 'none',
|
||||
key = '',
|
||||
type = 'none'
|
||||
) {
|
||||
super();
|
||||
this.security = security;
|
||||
this.key = key;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
static fromJson(json = {}) {
|
||||
return new QuicStreamSettings(
|
||||
json.security,
|
||||
json.key,
|
||||
@@ -258,37 +273,63 @@ class QuicStreamSettings extends CommonClass {
|
||||
}
|
||||
|
||||
class GrpcStreamSettings extends CommonClass {
|
||||
constructor(serviceName="", multiMode=false, authority="") {
|
||||
constructor(
|
||||
serviceName = "",
|
||||
authority = "",
|
||||
multiMode = false
|
||||
) {
|
||||
super();
|
||||
this.serviceName = serviceName;
|
||||
this.multiMode = multiMode;
|
||||
this.authority = authority;
|
||||
this.multiMode = multiMode;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
return new GrpcStreamSettings(json.serviceName, json.multiMode,json.authority);
|
||||
static fromJson(json = {}) {
|
||||
return new GrpcStreamSettings(json.serviceName, json.authority, json.multiMode);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
serviceName: this.serviceName,
|
||||
multiMode: this.multiMode,
|
||||
authority: this.authority
|
||||
authority: this.authority,
|
||||
multiMode: this.multiMode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class HttpUpgradeStreamSettings extends CommonClass {
|
||||
constructor(path='/', host='') {
|
||||
constructor(path = '/', host = '') {
|
||||
super();
|
||||
this.path = path;
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
static fromJson(json = {}) {
|
||||
return new HttpUpgradeStreamSettings(
|
||||
json.path,
|
||||
json.host
|
||||
json.host,
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
path: this.path,
|
||||
host: this.host,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class SplitHTTPStreamSettings extends CommonClass {
|
||||
constructor(path = '/', host = '') {
|
||||
super();
|
||||
this.path = path;
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
return new SplitHTTPStreamSettings(
|
||||
json.path,
|
||||
json.host,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -296,16 +337,17 @@ class HttpUpgradeStreamSettings extends CommonClass {
|
||||
return {
|
||||
path: this.path,
|
||||
host: this.host,
|
||||
headers: ObjectUtil.isEmpty(this.host) ? undefined : {Host: this.host},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TlsStreamSettings extends CommonClass {
|
||||
constructor(serverName='',
|
||||
alpn=[],
|
||||
fingerprint = '',
|
||||
allowInsecure = false) {
|
||||
constructor(
|
||||
serverName = '',
|
||||
alpn = [],
|
||||
fingerprint = '',
|
||||
allowInsecure = false
|
||||
) {
|
||||
super();
|
||||
this.serverName = serverName;
|
||||
this.alpn = alpn;
|
||||
@@ -313,7 +355,7 @@ class TlsStreamSettings extends CommonClass {
|
||||
this.allowInsecure = allowInsecure;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
static fromJson(json = {}) {
|
||||
return new TlsStreamSettings(
|
||||
json.serverName,
|
||||
json.alpn,
|
||||
@@ -333,7 +375,13 @@ class TlsStreamSettings extends CommonClass {
|
||||
}
|
||||
|
||||
class RealityStreamSettings extends CommonClass {
|
||||
constructor(publicKey = '', fingerprint = '', serverName = '', shortId = '', spiderX = '/') {
|
||||
constructor(
|
||||
publicKey = '',
|
||||
fingerprint = '',
|
||||
serverName = '',
|
||||
shortId = '',
|
||||
spiderX = '/'
|
||||
) {
|
||||
super();
|
||||
this.publicKey = publicKey;
|
||||
this.fingerprint = fingerprint;
|
||||
@@ -361,11 +409,18 @@ class RealityStreamSettings extends CommonClass {
|
||||
}
|
||||
};
|
||||
class SockoptStreamSettings extends CommonClass {
|
||||
constructor(dialerProxy = "", tcpFastOpen = false, tcpKeepAliveInterval = 0, tcpNoDelay = false) {
|
||||
constructor(
|
||||
dialerProxy = "",
|
||||
tcpFastOpen = false,
|
||||
tcpKeepAliveInterval = 0,
|
||||
tcpMptcp = false,
|
||||
tcpNoDelay = false
|
||||
) {
|
||||
super();
|
||||
this.dialerProxy = dialerProxy;
|
||||
this.tcpFastOpen = tcpFastOpen;
|
||||
this.tcpKeepAliveInterval = tcpKeepAliveInterval;
|
||||
this.tcpMptcp = tcpMptcp;
|
||||
this.tcpNoDelay = tcpNoDelay;
|
||||
}
|
||||
|
||||
@@ -375,6 +430,7 @@ class SockoptStreamSettings extends CommonClass {
|
||||
json.dialerProxy,
|
||||
json.tcpFastOpen,
|
||||
json.tcpKeepAliveInterval,
|
||||
json.tcpMptcp,
|
||||
json.tcpNoDelay,
|
||||
);
|
||||
}
|
||||
@@ -384,25 +440,28 @@ class SockoptStreamSettings extends CommonClass {
|
||||
dialerProxy: this.dialerProxy,
|
||||
tcpFastOpen: this.tcpFastOpen,
|
||||
tcpKeepAliveInterval: this.tcpKeepAliveInterval,
|
||||
tcpMptcp: this.tcpMptcp,
|
||||
tcpNoDelay: this.tcpNoDelay,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class StreamSettings extends CommonClass {
|
||||
constructor(network='tcp',
|
||||
security='none',
|
||||
tlsSettings=new TlsStreamSettings(),
|
||||
realitySettings = new RealityStreamSettings(),
|
||||
tcpSettings=new TcpStreamSettings(),
|
||||
kcpSettings=new KcpStreamSettings(),
|
||||
wsSettings=new WsStreamSettings(),
|
||||
httpSettings=new HttpStreamSettings(),
|
||||
quicSettings=new QuicStreamSettings(),
|
||||
grpcSettings=new GrpcStreamSettings(),
|
||||
httpupgradeSettings=new HttpUpgradeStreamSettings(),
|
||||
sockopt = undefined,
|
||||
) {
|
||||
constructor(
|
||||
network = 'tcp',
|
||||
security = 'none',
|
||||
tlsSettings = new TlsStreamSettings(),
|
||||
realitySettings = new RealityStreamSettings(),
|
||||
tcpSettings = new TcpStreamSettings(),
|
||||
kcpSettings = new KcpStreamSettings(),
|
||||
wsSettings = new WsStreamSettings(),
|
||||
httpSettings = new HttpStreamSettings(),
|
||||
quicSettings = new QuicStreamSettings(),
|
||||
grpcSettings = new GrpcStreamSettings(),
|
||||
httpupgradeSettings = new HttpUpgradeStreamSettings(),
|
||||
splithttpSettings = new SplitHTTPStreamSettings(),
|
||||
sockopt = undefined,
|
||||
) {
|
||||
super();
|
||||
this.network = network;
|
||||
this.security = security;
|
||||
@@ -412,12 +471,12 @@ class StreamSettings extends CommonClass {
|
||||
this.kcp = kcpSettings;
|
||||
this.ws = wsSettings;
|
||||
this.http = httpSettings;
|
||||
this.quic = quicSettings;
|
||||
this.grpc = grpcSettings;
|
||||
this.httpupgrade = httpupgradeSettings;
|
||||
this.splithttp = splithttpSettings;
|
||||
this.sockopt = sockopt;
|
||||
}
|
||||
|
||||
|
||||
get isTls() {
|
||||
return this.security === 'tls';
|
||||
}
|
||||
@@ -434,7 +493,7 @@ class StreamSettings extends CommonClass {
|
||||
this.sockopt = value ? new SockoptStreamSettings() : undefined;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
static fromJson(json = {}) {
|
||||
return new StreamSettings(
|
||||
json.network,
|
||||
json.security,
|
||||
@@ -447,6 +506,7 @@ class StreamSettings extends CommonClass {
|
||||
QuicStreamSettings.fromJson(json.quicSettings),
|
||||
GrpcStreamSettings.fromJson(json.grpcSettings),
|
||||
HttpUpgradeStreamSettings.fromJson(json.httpupgradeSettings),
|
||||
SplitHTTPStreamSettings.fromJson(json.splithttpSettings),
|
||||
SockoptStreamSettings.fromJson(json.sockopt),
|
||||
);
|
||||
}
|
||||
@@ -462,9 +522,9 @@ class StreamSettings extends CommonClass {
|
||||
kcpSettings: network === 'kcp' ? this.kcp.toJson() : undefined,
|
||||
wsSettings: network === 'ws' ? this.ws.toJson() : undefined,
|
||||
httpSettings: network === 'http' ? this.http.toJson() : undefined,
|
||||
quicSettings: network === 'quic' ? this.quic.toJson() : undefined,
|
||||
grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined,
|
||||
httpupgradeSettings: network === 'httpupgrade' ? this.httpupgrade.toJson() : undefined,
|
||||
splithttpSettings: network === 'splithttp' ? this.splithttp.toJson() : undefined,
|
||||
sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined,
|
||||
};
|
||||
}
|
||||
@@ -501,9 +561,9 @@ class Mux extends CommonClass {
|
||||
|
||||
class Outbound extends CommonClass {
|
||||
constructor(
|
||||
tag='',
|
||||
protocol=Protocols.VMess,
|
||||
settings=null,
|
||||
tag = '',
|
||||
protocol = Protocols.VMess,
|
||||
settings = null,
|
||||
streamSettings = new StreamSettings(),
|
||||
sendThrough,
|
||||
mux = new Mux(),
|
||||
@@ -529,7 +589,7 @@ class Outbound extends CommonClass {
|
||||
|
||||
canEnableTls() {
|
||||
if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(this.protocol)) return false;
|
||||
return ["tcp", "ws", "http", "quic", "grpc", "httpupgrade"].includes(this.stream.network);
|
||||
return ["tcp", "ws", "http", "grpc", "httpupgrade", "splithttp"].includes(this.stream.network);
|
||||
}
|
||||
|
||||
//this is used for xtls-rprx-vision
|
||||
@@ -550,6 +610,10 @@ class Outbound extends CommonClass {
|
||||
}
|
||||
|
||||
canEnableMux() {
|
||||
if (this.settings.flow && this.settings.flow != '') {
|
||||
this.mux.enabled = false;
|
||||
return false;
|
||||
}
|
||||
return [Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks, Protocols.HTTP, Protocols.Socks].includes(this.protocol);
|
||||
}
|
||||
|
||||
@@ -577,7 +641,7 @@ class Outbound extends CommonClass {
|
||||
return [Protocols.Socks, Protocols.HTTP].includes(this.protocol);
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
static fromJson(json = {}) {
|
||||
return new Outbound(
|
||||
json.tag,
|
||||
json.protocol,
|
||||
@@ -608,8 +672,8 @@ class Outbound extends CommonClass {
|
||||
|
||||
static fromLink(link) {
|
||||
data = link.split('://');
|
||||
if(data.length !=2) return null;
|
||||
switch(data[0].toLowerCase()){
|
||||
if (data.length != 2) return null;
|
||||
switch (data[0].toLowerCase()) {
|
||||
case Protocols.VMess:
|
||||
return this.fromVmessLink(JSON.parse(Base64.decode(data[1])));
|
||||
case Protocols.VLESS:
|
||||
@@ -621,7 +685,7 @@ class Outbound extends CommonClass {
|
||||
}
|
||||
}
|
||||
|
||||
static fromVmessLink(json={}){
|
||||
static fromVmessLink(json = {}) {
|
||||
let stream = new StreamSettings(json.net, json.tls);
|
||||
|
||||
let network = json.net;
|
||||
@@ -635,24 +699,21 @@ class Outbound extends CommonClass {
|
||||
stream.type = json.type;
|
||||
stream.seed = json.path;
|
||||
} else if (network === 'ws') {
|
||||
stream.ws = new WsStreamSettings(json.path,json.host);
|
||||
stream.ws = new WsStreamSettings(json.path, json.host);
|
||||
} else if (network === 'http' || network == 'h2') {
|
||||
stream.network = 'http'
|
||||
stream.http = new HttpStreamSettings(
|
||||
json.path,
|
||||
json.host);
|
||||
} else if (network === 'quic') {
|
||||
stream.quic = new QuicStreamSettings(
|
||||
json.host ? json.host : 'none',
|
||||
json.path,
|
||||
json.type ? json.type : 'none');
|
||||
} else if (network === 'grpc') {
|
||||
stream.grpc = new GrpcStreamSettings(json.path, json.authority, json.type == 'multi');
|
||||
} else if (network === 'httpupgrade') {
|
||||
stream.httpupgrade = new HttpUpgradeStreamSettings(json.path,json.host);
|
||||
stream.httpupgrade = new HttpUpgradeStreamSettings(json.path, json.host);
|
||||
} else if (network === 'splithttp') {
|
||||
stream.splithttp = new SplitHTTPStreamSettings(json.path, json.host);
|
||||
}
|
||||
|
||||
if(json.tls && json.tls == 'tls'){
|
||||
if (json.tls && json.tls == 'tls') {
|
||||
stream.tls = new TlsStreamSettings(
|
||||
json.sni,
|
||||
json.alpn ? json.alpn.split(',') : [],
|
||||
@@ -662,10 +723,10 @@ class Outbound extends CommonClass {
|
||||
|
||||
const port = json.port * 1;
|
||||
|
||||
return new Outbound(json.ps, Protocols.VMess, new Outbound.VmessSettings(json.add, port, json.id), stream);
|
||||
return new Outbound(json.ps, Protocols.VMess, new Outbound.VmessSettings(json.add, port, json.id, json.scy), stream);
|
||||
}
|
||||
|
||||
static fromParamLink(link){
|
||||
static fromParamLink(link) {
|
||||
const url = new URL(link);
|
||||
let type = url.searchParams.get('type') ?? 'tcp';
|
||||
let security = url.searchParams.get('security') ?? 'none';
|
||||
@@ -682,37 +743,34 @@ class Outbound extends CommonClass {
|
||||
stream.kcp.type = headerType ?? 'none';
|
||||
stream.kcp.seed = path;
|
||||
} else if (type === 'ws') {
|
||||
stream.ws = new WsStreamSettings(path,host);
|
||||
stream.ws = new WsStreamSettings(path, host);
|
||||
} else if (type === 'http' || type == 'h2') {
|
||||
stream.http = new HttpStreamSettings(path,host);
|
||||
} else if (type === 'quic') {
|
||||
stream.quic = new QuicStreamSettings(
|
||||
url.searchParams.get('quicSecurity') ?? 'none',
|
||||
url.searchParams.get('key') ?? '',
|
||||
headerType ?? 'none');
|
||||
} else if (type === 'grpc') {
|
||||
stream.grpc = new GrpcStreamSettings(
|
||||
url.searchParams.get('serviceName') ?? '',
|
||||
url.searchParams.get('authority') ?? '',
|
||||
url.searchParams.get('mode') == 'multi');
|
||||
} else if (type === 'httpupgrade') {
|
||||
stream.httpupgrade = new HttpUpgradeStreamSettings(path,host);
|
||||
stream.http = new HttpStreamSettings(path, host);
|
||||
} else if (type === 'grpc') {
|
||||
stream.grpc = new GrpcStreamSettings(
|
||||
url.searchParams.get('serviceName') ?? '',
|
||||
url.searchParams.get('authority') ?? '',
|
||||
url.searchParams.get('mode') == 'multi');
|
||||
} else if (type === 'httpupgrade') {
|
||||
stream.httpupgrade = new HttpUpgradeStreamSettings(path, host);
|
||||
} else if (type === 'splithttp') {
|
||||
stream.splithttp = new SplitHTTPStreamSettings(path, host);
|
||||
}
|
||||
|
||||
if(security == 'tls'){
|
||||
let fp=url.searchParams.get('fp') ?? 'none';
|
||||
let alpn=url.searchParams.get('alpn');
|
||||
let allowInsecure=url.searchParams.get('allowInsecure');
|
||||
let sni=url.searchParams.get('sni') ?? '';
|
||||
if (security == 'tls') {
|
||||
let fp = url.searchParams.get('fp') ?? 'none';
|
||||
let alpn = url.searchParams.get('alpn');
|
||||
let allowInsecure = url.searchParams.get('allowInsecure');
|
||||
let sni = url.searchParams.get('sni') ?? '';
|
||||
stream.tls = new TlsStreamSettings(sni, alpn ? alpn.split(',') : [], fp, allowInsecure == 1);
|
||||
}
|
||||
|
||||
if(security == 'reality'){
|
||||
let pbk=url.searchParams.get('pbk');
|
||||
let fp=url.searchParams.get('fp');
|
||||
let sni=url.searchParams.get('sni') ?? '';
|
||||
let sid=url.searchParams.get('sid') ?? '';
|
||||
let spx=url.searchParams.get('spx') ?? '';
|
||||
if (security == 'reality') {
|
||||
let pbk = url.searchParams.get('pbk');
|
||||
let fp = url.searchParams.get('fp');
|
||||
let sni = url.searchParams.get('sni') ?? '';
|
||||
let sid = url.searchParams.get('sid') ?? '';
|
||||
let spx = url.searchParams.get('spx') ?? '';
|
||||
stream.reality = new RealityStreamSettings(pbk, fp, sni, sid, spx);
|
||||
}
|
||||
|
||||
@@ -720,14 +778,14 @@ class Outbound extends CommonClass {
|
||||
const match = link.match(regex);
|
||||
|
||||
if (!match) return null;
|
||||
let [, protocol, userData, address, port, ] = match;
|
||||
let [, protocol, userData, address, port,] = match;
|
||||
port *= 1;
|
||||
if(protocol == 'ss') {
|
||||
if (protocol == 'ss') {
|
||||
protocol = 'shadowsocks';
|
||||
userData = atob(userData).split(':');
|
||||
}
|
||||
var settings;
|
||||
switch(protocol){
|
||||
switch (protocol) {
|
||||
case Protocols.VLESS:
|
||||
settings = new Outbound.VLESSSettings(address, port, userData, url.searchParams.get('flow') ?? '');
|
||||
break;
|
||||
@@ -735,7 +793,7 @@ class Outbound extends CommonClass {
|
||||
settings = new Outbound.TrojanSettings(address, port, userData);
|
||||
break;
|
||||
case Protocols.Shadowsocks:
|
||||
let method = userData.splice(0,1)[0];
|
||||
let method = userData.splice(0, 1)[0];
|
||||
settings = new Outbound.ShadowsocksSettings(address, port, userData.join(":"), method, true);
|
||||
break;
|
||||
default:
|
||||
@@ -791,35 +849,55 @@ Outbound.Settings = class extends CommonClass {
|
||||
}
|
||||
};
|
||||
Outbound.FreedomSettings = class extends CommonClass {
|
||||
constructor(domainStrategy='', fragment={}) {
|
||||
constructor(
|
||||
domainStrategy = '',
|
||||
redirect = '',
|
||||
fragment = {},
|
||||
noises = []
|
||||
) {
|
||||
super();
|
||||
this.domainStrategy = domainStrategy;
|
||||
this.redirect = redirect;
|
||||
this.fragment = fragment;
|
||||
this.noises = noises;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
addNoise() {
|
||||
this.noises.push(new Outbound.FreedomSettings.Noise());
|
||||
}
|
||||
|
||||
delNoise(index) {
|
||||
this.noises.splice(index, 1);
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
return new Outbound.FreedomSettings(
|
||||
json.domainStrategy,
|
||||
json.redirect,
|
||||
json.fragment ? Outbound.FreedomSettings.Fragment.fromJson(json.fragment) : undefined,
|
||||
json.noises ? json.noises.map(noise => Outbound.FreedomSettings.Noise.fromJson(noise)) : [new Outbound.FreedomSettings.Noise()],
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
domainStrategy: ObjectUtil.isEmpty(this.domainStrategy) ? undefined : this.domainStrategy,
|
||||
redirect: this.redirect,
|
||||
fragment: Object.keys(this.fragment).length === 0 ? undefined : this.fragment,
|
||||
noises: Outbound.FreedomSettings.Noise.toJsonArray(this.noises),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
Outbound.FreedomSettings.Fragment = class extends CommonClass {
|
||||
constructor(packets='1-3',length='',interval=''){
|
||||
constructor(packets = '1-3', length = '', interval = '') {
|
||||
super();
|
||||
this.packets = packets;
|
||||
this.length = length;
|
||||
this.interval = interval;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
static fromJson(json = {}) {
|
||||
return new Outbound.FreedomSettings.Fragment(
|
||||
json.packets,
|
||||
json.length,
|
||||
@@ -827,13 +905,47 @@ Outbound.FreedomSettings.Fragment = class extends CommonClass {
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Outbound.FreedomSettings.Noise = class extends CommonClass {
|
||||
constructor(
|
||||
type = 'rand',
|
||||
packet = '10-20',
|
||||
delay = '10-16'
|
||||
) {
|
||||
super();
|
||||
this.type = type;
|
||||
this.packet = packet;
|
||||
this.delay = delay;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
return new Outbound.FreedomSettings.Noise(
|
||||
json.type,
|
||||
json.packet,
|
||||
json.delay,
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
type: this.type,
|
||||
packet: this.packet,
|
||||
delay: this.delay,
|
||||
};
|
||||
}
|
||||
|
||||
static toJsonArray(noises) {
|
||||
return noises.map(noise => noise.toJson());
|
||||
}
|
||||
};
|
||||
|
||||
Outbound.BlackholeSettings = class extends CommonClass {
|
||||
constructor(type) {
|
||||
super();
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
static fromJson(json = {}) {
|
||||
return new Outbound.BlackholeSettings(
|
||||
json.response ? json.response.type : undefined,
|
||||
);
|
||||
@@ -841,40 +953,52 @@ Outbound.BlackholeSettings = class extends CommonClass {
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
response: ObjectUtil.isEmpty(this.type) ? undefined : {type: this.type},
|
||||
response: ObjectUtil.isEmpty(this.type) ? undefined : { type: this.type },
|
||||
};
|
||||
}
|
||||
};
|
||||
Outbound.DNSSettings = class extends CommonClass {
|
||||
constructor(network='udp', address='1.1.1.1', port=53) {
|
||||
constructor(
|
||||
network = 'udp',
|
||||
address = '1.1.1.1',
|
||||
port = 53,
|
||||
nonIPQuery = 'drop',
|
||||
blockTypes = []
|
||||
) {
|
||||
super();
|
||||
this.network = network;
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
this.nonIPQuery = nonIPQuery;
|
||||
this.blockTypes = blockTypes;
|
||||
}
|
||||
|
||||
static fromJson(json={}){
|
||||
static fromJson(json = {}) {
|
||||
return new Outbound.DNSSettings(
|
||||
json.network,
|
||||
json.address,
|
||||
json.port,
|
||||
json.nonIPQuery,
|
||||
json.blockTypes,
|
||||
);
|
||||
}
|
||||
};
|
||||
Outbound.VmessSettings = class extends CommonClass {
|
||||
constructor(address, port, id) {
|
||||
constructor(address, port, id, security) {
|
||||
super();
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
this.id = id;
|
||||
this.security = security;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
if(ObjectUtil.isArrEmpty(json.vnext)) return new Outbound.VmessSettings();
|
||||
static fromJson(json = {}) {
|
||||
if (ObjectUtil.isArrEmpty(json.vnext)) return new Outbound.VmessSettings();
|
||||
return new Outbound.VmessSettings(
|
||||
json.vnext[0].address,
|
||||
json.vnext[0].port,
|
||||
json.vnext[0].users[0].id,
|
||||
json.vnext[0].users[0].security,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -883,13 +1007,13 @@ Outbound.VmessSettings = class extends CommonClass {
|
||||
vnext: [{
|
||||
address: this.address,
|
||||
port: this.port,
|
||||
users: [{id: this.id}],
|
||||
users: [{ id: this.id, security: this.security }],
|
||||
}],
|
||||
};
|
||||
}
|
||||
};
|
||||
Outbound.VLESSSettings = class extends CommonClass {
|
||||
constructor(address, port, id, flow, encryption='none') {
|
||||
constructor(address, port, id, flow, encryption = 'none') {
|
||||
super();
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
@@ -898,8 +1022,8 @@ Outbound.VLESSSettings = class extends CommonClass {
|
||||
this.encryption = encryption
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
if(ObjectUtil.isArrEmpty(json.vnext)) return new Outbound.VLESSSettings();
|
||||
static fromJson(json = {}) {
|
||||
if (ObjectUtil.isArrEmpty(json.vnext)) return new Outbound.VLESSSettings();
|
||||
return new Outbound.VLESSSettings(
|
||||
json.vnext[0].address,
|
||||
json.vnext[0].port,
|
||||
@@ -914,7 +1038,7 @@ Outbound.VLESSSettings = class extends CommonClass {
|
||||
vnext: [{
|
||||
address: this.address,
|
||||
port: this.port,
|
||||
users: [{id: this.id, flow: this.flow, encryption: 'none',}],
|
||||
users: [{ id: this.id, flow: this.flow, encryption: 'none', }],
|
||||
}],
|
||||
};
|
||||
}
|
||||
@@ -927,8 +1051,8 @@ Outbound.TrojanSettings = class extends CommonClass {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
if(ObjectUtil.isArrEmpty(json.servers)) return new Outbound.TrojanSettings();
|
||||
static fromJson(json = {}) {
|
||||
if (ObjectUtil.isArrEmpty(json.servers)) return new Outbound.TrojanSettings();
|
||||
return new Outbound.TrojanSettings(
|
||||
json.servers[0].address,
|
||||
json.servers[0].port,
|
||||
@@ -947,24 +1071,26 @@ Outbound.TrojanSettings = class extends CommonClass {
|
||||
}
|
||||
};
|
||||
Outbound.ShadowsocksSettings = class extends CommonClass {
|
||||
constructor(address, port, password, method, uot) {
|
||||
constructor(address, port, password, method, uot, UoTVersion) {
|
||||
super();
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
this.password = password;
|
||||
this.method = method;
|
||||
this.uot = uot;
|
||||
this.UoTVersion = UoTVersion;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
static fromJson(json = {}) {
|
||||
let servers = json.servers;
|
||||
if(ObjectUtil.isArrEmpty(servers)) servers=[{}];
|
||||
if (ObjectUtil.isArrEmpty(servers)) servers = [{}];
|
||||
return new Outbound.ShadowsocksSettings(
|
||||
servers[0].address,
|
||||
servers[0].port,
|
||||
servers[0].password,
|
||||
servers[0].method,
|
||||
servers[0].uot,
|
||||
servers[0].UoTVersion,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -976,6 +1102,7 @@ Outbound.ShadowsocksSettings = class extends CommonClass {
|
||||
password: this.password,
|
||||
method: this.method,
|
||||
uot: this.uot,
|
||||
UoTVersion: this.UoTVersion,
|
||||
}],
|
||||
};
|
||||
}
|
||||
@@ -990,9 +1117,9 @@ Outbound.SocksSettings = class extends CommonClass {
|
||||
this.pass = pass;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
static fromJson(json = {}) {
|
||||
let servers = json.servers;
|
||||
if(ObjectUtil.isArrEmpty(servers)) servers=[{users: [{}]}];
|
||||
if (ObjectUtil.isArrEmpty(servers)) servers = [{ users: [{}] }];
|
||||
return new Outbound.SocksSettings(
|
||||
servers[0].address,
|
||||
servers[0].port,
|
||||
@@ -1006,7 +1133,7 @@ Outbound.SocksSettings = class extends CommonClass {
|
||||
servers: [{
|
||||
address: this.address,
|
||||
port: this.port,
|
||||
users: ObjectUtil.isEmpty(this.user) ? [] : [{user: this.user, pass: this.pass}],
|
||||
users: ObjectUtil.isEmpty(this.user) ? [] : [{ user: this.user, pass: this.pass }],
|
||||
}],
|
||||
};
|
||||
}
|
||||
@@ -1020,9 +1147,9 @@ Outbound.HttpSettings = class extends CommonClass {
|
||||
this.pass = pass;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
static fromJson(json = {}) {
|
||||
let servers = json.servers;
|
||||
if(ObjectUtil.isArrEmpty(servers)) servers=[{users: [{}]}];
|
||||
if (ObjectUtil.isArrEmpty(servers)) servers = [{ users: [{}] }];
|
||||
return new Outbound.HttpSettings(
|
||||
servers[0].address,
|
||||
servers[0].port,
|
||||
@@ -1036,7 +1163,7 @@ Outbound.HttpSettings = class extends CommonClass {
|
||||
servers: [{
|
||||
address: this.address,
|
||||
port: this.port,
|
||||
users: ObjectUtil.isEmpty(this.user) ? [] : [{user: this.user, pass: this.pass}],
|
||||
users: ObjectUtil.isEmpty(this.user) ? [] : [{ user: this.user, pass: this.pass }],
|
||||
}],
|
||||
};
|
||||
}
|
||||
@@ -1044,17 +1171,23 @@ Outbound.HttpSettings = class extends CommonClass {
|
||||
|
||||
Outbound.WireguardSettings = class extends CommonClass {
|
||||
constructor(
|
||||
mtu=1420, secretKey='',
|
||||
address=[''], workers=2, domainStrategy='', reserved='',
|
||||
peers=[new Outbound.WireguardSettings.Peer()], kernelMode=false) {
|
||||
mtu = 1420,
|
||||
secretKey = '',
|
||||
address = [''],
|
||||
workers = 2,
|
||||
domainStrategy = '',
|
||||
reserved = '',
|
||||
peers = [new Outbound.WireguardSettings.Peer()],
|
||||
kernelMode = false
|
||||
) {
|
||||
super();
|
||||
this.mtu = mtu;
|
||||
this.secretKey = secretKey;
|
||||
this.pubKey = secretKey.length>0 ? Wireguard.generateKeypair(secretKey).publicKey : '';
|
||||
this.address = address instanceof Array ? address.join(',') : address;
|
||||
this.pubKey = secretKey.length > 0 ? Wireguard.generateKeypair(secretKey).publicKey : '';
|
||||
this.address = Array.isArray(address) ? address.join(',') : address;
|
||||
this.workers = workers;
|
||||
this.domainStrategy = domainStrategy;
|
||||
this.reserved = reserved instanceof Array ? reserved.join(',') : reserved;
|
||||
this.reserved = Array.isArray(reserved) ? reserved.join(',') : reserved;
|
||||
this.peers = peers;
|
||||
this.kernelMode = kernelMode;
|
||||
}
|
||||
@@ -1067,7 +1200,7 @@ Outbound.WireguardSettings = class extends CommonClass {
|
||||
this.peers.splice(index, 1);
|
||||
}
|
||||
|
||||
static fromJson(json={}){
|
||||
static fromJson(json = {}) {
|
||||
return new Outbound.WireguardSettings(
|
||||
json.mtu,
|
||||
json.secretKey,
|
||||
@@ -1082,10 +1215,10 @@ Outbound.WireguardSettings = class extends CommonClass {
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
mtu: this.mtu?? undefined,
|
||||
mtu: this.mtu ?? undefined,
|
||||
secretKey: this.secretKey,
|
||||
address: this.address ? this.address.split(",") : [],
|
||||
workers: this.workers?? undefined,
|
||||
workers: this.workers ?? undefined,
|
||||
domainStrategy: WireguardDomainStrategy.includes(this.domainStrategy) ? this.domainStrategy : undefined,
|
||||
reserved: this.reserved ? this.reserved.split(",").map(Number) : undefined,
|
||||
peers: Outbound.WireguardSettings.Peer.toJsonArray(this.peers),
|
||||
@@ -1095,7 +1228,13 @@ Outbound.WireguardSettings = class extends CommonClass {
|
||||
};
|
||||
|
||||
Outbound.WireguardSettings.Peer = class extends CommonClass {
|
||||
constructor(publicKey='', psk='', allowedIPs=['0.0.0.0/0','::/0'], endpoint='', keepAlive=0) {
|
||||
constructor(
|
||||
publicKey = '',
|
||||
psk = '',
|
||||
allowedIPs = ['0.0.0.0/0', '::/0'],
|
||||
endpoint = '',
|
||||
keepAlive = 0
|
||||
) {
|
||||
super();
|
||||
this.publicKey = publicKey;
|
||||
this.psk = psk;
|
||||
@@ -1104,7 +1243,7 @@ Outbound.WireguardSettings.Peer = class extends CommonClass {
|
||||
this.keepAlive = keepAlive;
|
||||
}
|
||||
|
||||
static fromJson(json={}){
|
||||
static fromJson(json = {}) {
|
||||
return new Outbound.WireguardSettings.Peer(
|
||||
json.publicKey,
|
||||
json.preSharedKey,
|
||||
@@ -1117,10 +1256,10 @@ Outbound.WireguardSettings.Peer = class extends CommonClass {
|
||||
toJson() {
|
||||
return {
|
||||
publicKey: this.publicKey,
|
||||
preSharedKey: this.psk.length>0 ? this.psk : undefined,
|
||||
preSharedKey: this.psk.length > 0 ? this.psk : undefined,
|
||||
allowedIPs: this.allowedIPs ? this.allowedIPs : undefined,
|
||||
endpoint: this.endpoint,
|
||||
keepAlive: this.keepAlive?? undefined,
|
||||
keepAlive: this.keepAlive ?? undefined,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -7,10 +7,10 @@ class AllSetting {
|
||||
this.webCertFile = "";
|
||||
this.webKeyFile = "";
|
||||
this.webBasePath = "/";
|
||||
this.sessionMaxAge = "";
|
||||
this.pageSize = 0;
|
||||
this.expireDiff = "";
|
||||
this.trafficDiff = "";
|
||||
this.sessionMaxAge = 60;
|
||||
this.pageSize = 50;
|
||||
this.expireDiff = 0;
|
||||
this.trafficDiff = 0;
|
||||
this.remarkModel = "-ieo";
|
||||
this.datepicker = "gregorian";
|
||||
this.tgBotEnable = false;
|
||||
@@ -19,25 +19,26 @@ class AllSetting {
|
||||
this.tgBotChatId = "";
|
||||
this.tgRunTime = "@daily";
|
||||
this.tgBotBackup = false;
|
||||
this.tgBotLoginNotify = false;
|
||||
this.tgCpu = "";
|
||||
this.tgBotLoginNotify = true;
|
||||
this.tgCpu = 80;
|
||||
this.tgLang = "en-US";
|
||||
this.xrayTemplateConfig = "";
|
||||
this.secretEnable = false;
|
||||
this.subEnable = false;
|
||||
this.subListen = "";
|
||||
this.subPort = "2096";
|
||||
this.subPort = 2096;
|
||||
this.subPath = "/sub/";
|
||||
this.subJsonPath = "/json/";
|
||||
this.subDomain = "";
|
||||
this.subCertFile = "";
|
||||
this.subKeyFile = "";
|
||||
this.subUpdates = 0;
|
||||
this.subUpdates = 12;
|
||||
this.subEncrypt = true;
|
||||
this.subShowInfo = false;
|
||||
this.subShowInfo = true;
|
||||
this.subURI = "";
|
||||
this.subJsonURI = "";
|
||||
this.subJsonFragment = "";
|
||||
this.subJsonNoises = "";
|
||||
this.subJsonMux = "";
|
||||
this.subJsonRules = "";
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ Date.prototype.formatDateTime = function (split = ' ') {
|
||||
};
|
||||
|
||||
class DateUtil {
|
||||
// String string to date object
|
||||
// String to date object
|
||||
static parseDate(str) {
|
||||
return new Date(str.replace(/-/g, '/'));
|
||||
}
|
||||
@@ -143,4 +143,9 @@ class DateUtil {
|
||||
date.setMinTime();
|
||||
return date;
|
||||
}
|
||||
|
||||
static convertToJalalian(date) {
|
||||
return date && moment.isMoment(date) ? date.format('jYYYY/jMM/jDD HH:mm:ss') : null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,73 +1,60 @@
|
||||
class Msg {
|
||||
constructor(success, msg, obj) {
|
||||
this.success = false;
|
||||
this.msg = "";
|
||||
this.obj = null;
|
||||
|
||||
if (success != null) {
|
||||
this.success = success;
|
||||
}
|
||||
if (msg != null) {
|
||||
this.msg = msg;
|
||||
}
|
||||
if (obj != null) {
|
||||
this.obj = obj;
|
||||
}
|
||||
constructor(success = false, msg = "", obj = null) {
|
||||
this.success = success;
|
||||
this.msg = msg;
|
||||
this.obj = obj;
|
||||
}
|
||||
}
|
||||
|
||||
class HttpUtil {
|
||||
static _handleMsg(msg) {
|
||||
if (!(msg instanceof Msg)) {
|
||||
if (!(msg instanceof Msg) || msg.msg === "") {
|
||||
return;
|
||||
}
|
||||
if (msg.msg === "") {
|
||||
return;
|
||||
}
|
||||
if (msg.success) {
|
||||
Vue.prototype.$message.success(msg.msg);
|
||||
} else {
|
||||
Vue.prototype.$message.error(msg.msg);
|
||||
}
|
||||
const messageType = msg.success ? 'success' : 'error';
|
||||
Vue.prototype.$message[messageType](msg.msg);
|
||||
}
|
||||
|
||||
static _respToMsg(resp) {
|
||||
const data = resp.data;
|
||||
if (!resp || !resp.data) {
|
||||
return new Msg(false, 'No response data');
|
||||
}
|
||||
const { data } = resp;
|
||||
if (data == null) {
|
||||
return new Msg(true);
|
||||
} else if (typeof data === 'object') {
|
||||
if (data.hasOwnProperty('success')) {
|
||||
return new Msg(data.success, data.msg, data.obj);
|
||||
} else {
|
||||
return data;
|
||||
}
|
||||
} else {
|
||||
return new Msg(false, 'unknown data:', data);
|
||||
}
|
||||
if (typeof data === 'object' && 'success' in data) {
|
||||
return new Msg(data.success, data.msg, data.obj);
|
||||
}
|
||||
return typeof data === 'object' ? data : new Msg(false, 'unknown data:', data);
|
||||
}
|
||||
|
||||
static async get(url, data, options) {
|
||||
let msg;
|
||||
static async get(url, params, options = {}) {
|
||||
try {
|
||||
const resp = await axios.get(url, data, options);
|
||||
msg = this._respToMsg(resp);
|
||||
} catch (e) {
|
||||
msg = new Msg(false, e.toString());
|
||||
const resp = await axios.get(url, { params, ...options });
|
||||
const msg = this._respToMsg(resp);
|
||||
this._handleMsg(msg);
|
||||
return msg;
|
||||
} catch (error) {
|
||||
console.error('GET request failed:', error);
|
||||
const errorMsg = new Msg(false, error.response?.data?.message || error.message || 'Request failed');
|
||||
this._handleMsg(errorMsg);
|
||||
return errorMsg;
|
||||
}
|
||||
this._handleMsg(msg);
|
||||
return msg;
|
||||
}
|
||||
|
||||
static async post(url, data, options) {
|
||||
let msg;
|
||||
static async post(url, data, options = {}) {
|
||||
try {
|
||||
const resp = await axios.post(url, data, options);
|
||||
msg = this._respToMsg(resp);
|
||||
} catch (e) {
|
||||
msg = new Msg(false, e.toString());
|
||||
const msg = this._respToMsg(resp);
|
||||
this._handleMsg(msg);
|
||||
return msg;
|
||||
} catch (error) {
|
||||
console.error('POST request failed:', error);
|
||||
const errorMsg = new Msg(false, error.response?.data?.message || error.message || 'Request failed');
|
||||
this._handleMsg(errorMsg);
|
||||
return errorMsg;
|
||||
}
|
||||
this._handleMsg(msg);
|
||||
return msg;
|
||||
}
|
||||
|
||||
static async postWithModal(url, data, modal) {
|
||||
@@ -112,12 +99,22 @@ class RandomUtil {
|
||||
return str;
|
||||
}
|
||||
|
||||
static randomShortId() {
|
||||
let str = '';
|
||||
for (let i = 0; i < 8; ++i) {
|
||||
str += seq[this.randomInt(16)];
|
||||
static randomShortIds() {
|
||||
const lengths = [2, 4, 6, 8, 10, 12, 14, 16];
|
||||
for (let i = lengths.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[lengths[i], lengths[j]] = [lengths[j], lengths[i]];
|
||||
}
|
||||
return str;
|
||||
|
||||
let shortIds = [];
|
||||
for (let length of lengths) {
|
||||
let shortId = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
shortId += seq[this.randomInt(16)];
|
||||
}
|
||||
shortIds.push(shortId);
|
||||
}
|
||||
return shortIds.join(',');
|
||||
}
|
||||
|
||||
static randomLowerAndNum(len) {
|
||||
@@ -297,172 +294,172 @@ class ObjectUtil {
|
||||
}
|
||||
|
||||
class Wireguard {
|
||||
static gf(init) {
|
||||
var r = new Float64Array(16);
|
||||
if (init) {
|
||||
for (var i = 0; i < init.length; ++i)
|
||||
r[i] = init[i];
|
||||
}
|
||||
return r;
|
||||
}
|
||||
static gf(init) {
|
||||
var r = new Float64Array(16);
|
||||
if (init) {
|
||||
for (var i = 0; i < init.length; ++i)
|
||||
r[i] = init[i];
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
static pack(o, n) {
|
||||
var b, m = this.gf(), t = this.gf();
|
||||
for (var i = 0; i < 16; ++i)
|
||||
t[i] = n[i];
|
||||
this.carry(t);
|
||||
this.carry(t);
|
||||
this.carry(t);
|
||||
for (var j = 0; j < 2; ++j) {
|
||||
m[0] = t[0] - 0xffed;
|
||||
for (var i = 1; i < 15; ++i) {
|
||||
m[i] = t[i] - 0xffff - ((m[i - 1] >> 16) & 1);
|
||||
m[i - 1] &= 0xffff;
|
||||
}
|
||||
m[15] = t[15] - 0x7fff - ((m[14] >> 16) & 1);
|
||||
b = (m[15] >> 16) & 1;
|
||||
m[14] &= 0xffff;
|
||||
this.cswap(t, m, 1 - b);
|
||||
}
|
||||
for (var i = 0; i < 16; ++i) {
|
||||
o[2 * i] = t[i] & 0xff;
|
||||
o[2 * i + 1] = t[i] >> 8;
|
||||
}
|
||||
}
|
||||
static pack(o, n) {
|
||||
var b, m = this.gf(), t = this.gf();
|
||||
for (var i = 0; i < 16; ++i)
|
||||
t[i] = n[i];
|
||||
this.carry(t);
|
||||
this.carry(t);
|
||||
this.carry(t);
|
||||
for (var j = 0; j < 2; ++j) {
|
||||
m[0] = t[0] - 0xffed;
|
||||
for (var i = 1; i < 15; ++i) {
|
||||
m[i] = t[i] - 0xffff - ((m[i - 1] >> 16) & 1);
|
||||
m[i - 1] &= 0xffff;
|
||||
}
|
||||
m[15] = t[15] - 0x7fff - ((m[14] >> 16) & 1);
|
||||
b = (m[15] >> 16) & 1;
|
||||
m[14] &= 0xffff;
|
||||
this.cswap(t, m, 1 - b);
|
||||
}
|
||||
for (var i = 0; i < 16; ++i) {
|
||||
o[2 * i] = t[i] & 0xff;
|
||||
o[2 * i + 1] = t[i] >> 8;
|
||||
}
|
||||
}
|
||||
|
||||
static carry(o) {
|
||||
var c;
|
||||
for (var i = 0; i < 16; ++i) {
|
||||
o[(i + 1) % 16] += (i < 15 ? 1 : 38) * Math.floor(o[i] / 65536);
|
||||
o[i] &= 0xffff;
|
||||
}
|
||||
}
|
||||
static carry(o) {
|
||||
var c;
|
||||
for (var i = 0; i < 16; ++i) {
|
||||
o[(i + 1) % 16] += (i < 15 ? 1 : 38) * Math.floor(o[i] / 65536);
|
||||
o[i] &= 0xffff;
|
||||
}
|
||||
}
|
||||
|
||||
static cswap(p, q, b) {
|
||||
var t, c = ~(b - 1);
|
||||
for (var i = 0; i < 16; ++i) {
|
||||
t = c & (p[i] ^ q[i]);
|
||||
p[i] ^= t;
|
||||
q[i] ^= t;
|
||||
}
|
||||
}
|
||||
static cswap(p, q, b) {
|
||||
var t, c = ~(b - 1);
|
||||
for (var i = 0; i < 16; ++i) {
|
||||
t = c & (p[i] ^ q[i]);
|
||||
p[i] ^= t;
|
||||
q[i] ^= t;
|
||||
}
|
||||
}
|
||||
|
||||
static add(o, a, b) {
|
||||
for (var i = 0; i < 16; ++i)
|
||||
o[i] = (a[i] + b[i]) | 0;
|
||||
}
|
||||
static add(o, a, b) {
|
||||
for (var i = 0; i < 16; ++i)
|
||||
o[i] = (a[i] + b[i]) | 0;
|
||||
}
|
||||
|
||||
static subtract(o, a, b) {
|
||||
for (var i = 0; i < 16; ++i)
|
||||
o[i] = (a[i] - b[i]) | 0;
|
||||
}
|
||||
static subtract(o, a, b) {
|
||||
for (var i = 0; i < 16; ++i)
|
||||
o[i] = (a[i] - b[i]) | 0;
|
||||
}
|
||||
|
||||
static multmod(o, a, b) {
|
||||
var t = new Float64Array(31);
|
||||
for (var i = 0; i < 16; ++i) {
|
||||
for (var j = 0; j < 16; ++j)
|
||||
t[i + j] += a[i] * b[j];
|
||||
}
|
||||
for (var i = 0; i < 15; ++i)
|
||||
t[i] += 38 * t[i + 16];
|
||||
for (var i = 0; i < 16; ++i)
|
||||
o[i] = t[i];
|
||||
this.carry(o);
|
||||
this.carry(o);
|
||||
}
|
||||
static multmod(o, a, b) {
|
||||
var t = new Float64Array(31);
|
||||
for (var i = 0; i < 16; ++i) {
|
||||
for (var j = 0; j < 16; ++j)
|
||||
t[i + j] += a[i] * b[j];
|
||||
}
|
||||
for (var i = 0; i < 15; ++i)
|
||||
t[i] += 38 * t[i + 16];
|
||||
for (var i = 0; i < 16; ++i)
|
||||
o[i] = t[i];
|
||||
this.carry(o);
|
||||
this.carry(o);
|
||||
}
|
||||
|
||||
static invert(o, i) {
|
||||
var c = this.gf();
|
||||
for (var a = 0; a < 16; ++a)
|
||||
c[a] = i[a];
|
||||
for (var a = 253; a >= 0; --a) {
|
||||
this.multmod(c, c, c);
|
||||
if (a !== 2 && a !== 4)
|
||||
static invert(o, i) {
|
||||
var c = this.gf();
|
||||
for (var a = 0; a < 16; ++a)
|
||||
c[a] = i[a];
|
||||
for (var a = 253; a >= 0; --a) {
|
||||
this.multmod(c, c, c);
|
||||
if (a !== 2 && a !== 4)
|
||||
this.multmod(c, c, i);
|
||||
}
|
||||
for (var a = 0; a < 16; ++a)
|
||||
o[a] = c[a];
|
||||
}
|
||||
}
|
||||
for (var a = 0; a < 16; ++a)
|
||||
o[a] = c[a];
|
||||
}
|
||||
|
||||
static clamp(z) {
|
||||
z[31] = (z[31] & 127) | 64;
|
||||
z[0] &= 248;
|
||||
}
|
||||
static clamp(z) {
|
||||
z[31] = (z[31] & 127) | 64;
|
||||
z[0] &= 248;
|
||||
}
|
||||
|
||||
static generatePublicKey(privateKey) {
|
||||
var r, z = new Uint8Array(32);
|
||||
var a = this.gf([1]),
|
||||
b = this.gf([9]),
|
||||
c = this.gf(),
|
||||
d = this.gf([1]),
|
||||
e = this.gf(),
|
||||
f = this.gf(),
|
||||
_121665 = this.gf([0xdb41, 1]),
|
||||
_9 = this.gf([9]);
|
||||
for (var i = 0; i < 32; ++i)
|
||||
z[i] = privateKey[i];
|
||||
this.clamp(z);
|
||||
for (var i = 254; i >= 0; --i) {
|
||||
r = (z[i >>> 3] >>> (i & 7)) & 1;
|
||||
this.cswap(a, b, r);
|
||||
this.cswap(c, d, r);
|
||||
this.add(e, a, c);
|
||||
this.subtract(a, a, c);
|
||||
this.add(c, b, d);
|
||||
this.subtract(b, b, d);
|
||||
this.multmod(d, e, e);
|
||||
this.multmod(f, a, a);
|
||||
this.multmod(a, c, a);
|
||||
this.multmod(c, b, e);
|
||||
this.add(e, a, c);
|
||||
this.subtract(a, a, c);
|
||||
this.multmod(b, a, a);
|
||||
this.subtract(c, d, f);
|
||||
this.multmod(a, c, _121665);
|
||||
this.add(a, a, d);
|
||||
this.multmod(c, c, a);
|
||||
this.multmod(a, d, f);
|
||||
this.multmod(d, b, _9);
|
||||
this.multmod(b, e, e);
|
||||
this.cswap(a, b, r);
|
||||
this.cswap(c, d, r);
|
||||
}
|
||||
this.invert(c, c);
|
||||
this.multmod(a, a, c);
|
||||
this.pack(z, a);
|
||||
return z;
|
||||
}
|
||||
static generatePublicKey(privateKey) {
|
||||
var r, z = new Uint8Array(32);
|
||||
var a = this.gf([1]),
|
||||
b = this.gf([9]),
|
||||
c = this.gf(),
|
||||
d = this.gf([1]),
|
||||
e = this.gf(),
|
||||
f = this.gf(),
|
||||
_121665 = this.gf([0xdb41, 1]),
|
||||
_9 = this.gf([9]);
|
||||
for (var i = 0; i < 32; ++i)
|
||||
z[i] = privateKey[i];
|
||||
this.clamp(z);
|
||||
for (var i = 254; i >= 0; --i) {
|
||||
r = (z[i >>> 3] >>> (i & 7)) & 1;
|
||||
this.cswap(a, b, r);
|
||||
this.cswap(c, d, r);
|
||||
this.add(e, a, c);
|
||||
this.subtract(a, a, c);
|
||||
this.add(c, b, d);
|
||||
this.subtract(b, b, d);
|
||||
this.multmod(d, e, e);
|
||||
this.multmod(f, a, a);
|
||||
this.multmod(a, c, a);
|
||||
this.multmod(c, b, e);
|
||||
this.add(e, a, c);
|
||||
this.subtract(a, a, c);
|
||||
this.multmod(b, a, a);
|
||||
this.subtract(c, d, f);
|
||||
this.multmod(a, c, _121665);
|
||||
this.add(a, a, d);
|
||||
this.multmod(c, c, a);
|
||||
this.multmod(a, d, f);
|
||||
this.multmod(d, b, _9);
|
||||
this.multmod(b, e, e);
|
||||
this.cswap(a, b, r);
|
||||
this.cswap(c, d, r);
|
||||
}
|
||||
this.invert(c, c);
|
||||
this.multmod(a, a, c);
|
||||
this.pack(z, a);
|
||||
return z;
|
||||
}
|
||||
|
||||
static generatePresharedKey() {
|
||||
var privateKey = new Uint8Array(32);
|
||||
window.crypto.getRandomValues(privateKey);
|
||||
return privateKey;
|
||||
}
|
||||
static generatePresharedKey() {
|
||||
var privateKey = new Uint8Array(32);
|
||||
window.crypto.getRandomValues(privateKey);
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
static generatePrivateKey() {
|
||||
var privateKey = this.generatePresharedKey();
|
||||
this.clamp(privateKey);
|
||||
return privateKey;
|
||||
}
|
||||
static generatePrivateKey() {
|
||||
var privateKey = this.generatePresharedKey();
|
||||
this.clamp(privateKey);
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
static encodeBase64(dest, src) {
|
||||
var input = Uint8Array.from([(src[0] >> 2) & 63, ((src[0] << 4) | (src[1] >> 4)) & 63, ((src[1] << 2) | (src[2] >> 6)) & 63, src[2] & 63]);
|
||||
for (var i = 0; i < 4; ++i)
|
||||
dest[i] = input[i] + 65 +
|
||||
(((25 - input[i]) >> 8) & 6) -
|
||||
(((51 - input[i]) >> 8) & 75) -
|
||||
(((61 - input[i]) >> 8) & 15) +
|
||||
(((62 - input[i]) >> 8) & 3);
|
||||
}
|
||||
static encodeBase64(dest, src) {
|
||||
var input = Uint8Array.from([(src[0] >> 2) & 63, ((src[0] << 4) | (src[1] >> 4)) & 63, ((src[1] << 2) | (src[2] >> 6)) & 63, src[2] & 63]);
|
||||
for (var i = 0; i < 4; ++i)
|
||||
dest[i] = input[i] + 65 +
|
||||
(((25 - input[i]) >> 8) & 6) -
|
||||
(((51 - input[i]) >> 8) & 75) -
|
||||
(((61 - input[i]) >> 8) & 15) +
|
||||
(((62 - input[i]) >> 8) & 3);
|
||||
}
|
||||
|
||||
static keyToBase64(key) {
|
||||
var i, base64 = new Uint8Array(44);
|
||||
for (i = 0; i < 32 / 3; ++i)
|
||||
static keyToBase64(key) {
|
||||
var i, base64 = new Uint8Array(44);
|
||||
for (i = 0; i < 32 / 3; ++i)
|
||||
this.encodeBase64(base64.subarray(i * 4), key.subarray(i * 3));
|
||||
this.encodeBase64(base64.subarray(i * 4), Uint8Array.from([key[i * 3 + 0], key[i * 3 + 1], 0]));
|
||||
base64[43] = 61;
|
||||
return String.fromCharCode.apply(null, base64);
|
||||
}
|
||||
base64[43] = 61;
|
||||
return String.fromCharCode.apply(null, base64);
|
||||
}
|
||||
|
||||
static keyFromBase64(encoded) {
|
||||
const binaryStr = atob(encoded);
|
||||
@@ -473,12 +470,12 @@ class Wireguard {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
static generateKeypair(secretKey='') {
|
||||
var privateKey = secretKey.length>0 ? this.keyFromBase64(secretKey) : this.generatePrivateKey();
|
||||
static generateKeypair(secretKey = '') {
|
||||
var privateKey = secretKey.length > 0 ? this.keyFromBase64(secretKey) : this.generatePrivateKey();
|
||||
var publicKey = this.generatePublicKey(privateKey);
|
||||
return {
|
||||
publicKey: this.keyToBase64(publicKey),
|
||||
privateKey: secretKey.length>0 ? secretKey : this.keyToBase64(privateKey)
|
||||
privateKey: secretKey.length > 0 ? secretKey : this.keyToBase64(privateKey)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
|
||||
{"GET", "/list", a.inboundController.getInbounds},
|
||||
{"GET", "/get/:id", a.inboundController.getInbound},
|
||||
{"GET", "/getClientTraffics/:email", a.inboundController.getClientTraffics},
|
||||
{"GET", "/getClientTrafficsById/:id", a.inboundController.getClientTrafficsById},
|
||||
{"POST", "/add", a.inboundController.addInbound},
|
||||
{"POST", "/del/:id", a.inboundController.delInbound},
|
||||
{"POST", "/update/:id", a.inboundController.updateInbound},
|
||||
|
||||
@@ -77,6 +77,16 @@ func (a *InboundController) getClientTraffics(c *gin.Context) {
|
||||
jsonObj(c, clientTraffics, nil)
|
||||
}
|
||||
|
||||
func (a *InboundController) getClientTrafficsById(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
clientTraffics, err := a.inboundService.GetClientTrafficByID(id)
|
||||
if err != nil {
|
||||
jsonMsg(c, "Error getting traffics", err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, clientTraffics, nil)
|
||||
}
|
||||
|
||||
func (a *InboundController) addInbound(c *gin.Context) {
|
||||
inbound := &model.Inbound{}
|
||||
err := c.ShouldBind(inbound)
|
||||
@@ -232,14 +242,12 @@ func (a *InboundController) resetClientTraffic(c *gin.Context) {
|
||||
}
|
||||
email := c.Param("email")
|
||||
|
||||
needRestart := true
|
||||
|
||||
needRestart, err = a.inboundService.ResetClientTraffic(id, email)
|
||||
needRestart, err := a.inboundService.ResetClientTraffic(id, email)
|
||||
if err != nil {
|
||||
jsonMsg(c, "Something went wrong!", err)
|
||||
return
|
||||
}
|
||||
jsonMsg(c, "traffic reseted", nil)
|
||||
jsonMsg(c, "Traffic has been reset", nil)
|
||||
if needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
@@ -253,7 +261,7 @@ func (a *InboundController) resetAllTraffics(c *gin.Context) {
|
||||
} else {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
jsonMsg(c, "All traffics reseted", nil)
|
||||
jsonMsg(c, "all traffic has been reset", nil)
|
||||
}
|
||||
|
||||
func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
|
||||
@@ -270,7 +278,7 @@ func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
|
||||
} else {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
jsonMsg(c, "All traffics of client reseted", nil)
|
||||
jsonMsg(c, "All traffic from the client has been reset.", nil)
|
||||
}
|
||||
|
||||
func (a *InboundController) importInbound(c *gin.Context) {
|
||||
@@ -313,9 +321,9 @@ func (a *InboundController) delDepletedClients(c *gin.Context) {
|
||||
jsonMsg(c, "Something went wrong!", err)
|
||||
return
|
||||
}
|
||||
jsonMsg(c, "All delpeted clients are deleted", nil)
|
||||
jsonMsg(c, "All depleted clients are deleted", nil)
|
||||
}
|
||||
|
||||
func (a *InboundController) onlines(c *gin.Context) {
|
||||
jsonObj(c, a.inboundService.GetOnlineClinets(), nil)
|
||||
jsonObj(c, a.inboundService.GetOnlineClients(), nil)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"x-ui/logger"
|
||||
@@ -64,37 +65,38 @@ func (a *IndexController) login(c *gin.Context) {
|
||||
|
||||
user := a.userService.CheckUser(form.Username, form.Password, form.LoginSecret)
|
||||
timeStr := time.Now().Format("2006-01-02 15:04:05")
|
||||
safeUser := template.HTMLEscapeString(form.Username)
|
||||
safePass := template.HTMLEscapeString(form.Password)
|
||||
safeSecret := template.HTMLEscapeString(form.LoginSecret)
|
||||
if user == nil {
|
||||
logger.Warningf("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password)
|
||||
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
|
||||
logger.Warningf("wrong username or password or secret: \"%s\" \"%s\" \"%s\"", safeUser, safePass, safeSecret)
|
||||
a.tgbot.UserLoginNotify(safeUser, safePass, getRemoteIp(c), timeStr, 0)
|
||||
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
|
||||
return
|
||||
} else {
|
||||
logger.Infof("%s login success, Ip Address: %s\n", form.Username, getRemoteIp(c))
|
||||
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 1)
|
||||
logger.Infof("%s logged in successfully, Ip Address: %s\n", safeUser, getRemoteIp(c))
|
||||
a.tgbot.UserLoginNotify(safeUser, ``, getRemoteIp(c), timeStr, 1)
|
||||
}
|
||||
|
||||
sessionMaxAge, err := a.settingService.GetSessionMaxAge()
|
||||
if err != nil {
|
||||
logger.Warningf("Unable to get session's max age from DB")
|
||||
logger.Warning("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.SetMaxAge(c, sessionMaxAge*60)
|
||||
if err != nil {
|
||||
logger.Warning("Unable to set session's max age")
|
||||
}
|
||||
|
||||
err = session.SetLoginUser(c, user)
|
||||
logger.Info("user", user.Id, "login success")
|
||||
logger.Infof("%s logged in successfully", user.Username)
|
||||
jsonMsg(c, I18nWeb(c, "pages.login.toasts.successLogin"), err)
|
||||
}
|
||||
|
||||
func (a *IndexController) logout(c *gin.Context) {
|
||||
user := session.GetLoginUser(c)
|
||||
if user != nil {
|
||||
logger.Info("user", user.Id, "logout")
|
||||
logger.Infof("%s logged out successfully", user.Username)
|
||||
}
|
||||
session.ClearSession(c)
|
||||
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
|
||||
|
||||
@@ -105,7 +105,7 @@ func (a *ServerController) stopXrayService(c *gin.Context) {
|
||||
jsonMsg(c, "", err)
|
||||
return
|
||||
}
|
||||
jsonMsg(c, "Xray stoped", err)
|
||||
jsonMsg(c, "Xray stopped", err)
|
||||
}
|
||||
|
||||
func (a *ServerController) restartXrayService(c *gin.Context) {
|
||||
|
||||
@@ -13,15 +13,18 @@ import (
|
||||
)
|
||||
|
||||
func getRemoteIp(c *gin.Context) string {
|
||||
value := c.GetHeader("X-Forwarded-For")
|
||||
value := c.GetHeader("X-Real-IP")
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
value = c.GetHeader("X-Forwarded-For")
|
||||
if value != "" {
|
||||
ips := strings.Split(value, ",")
|
||||
return ips[0]
|
||||
} else {
|
||||
addr := c.Request.RemoteAddr
|
||||
ip, _, _ := net.SplitHostPort(addr)
|
||||
return ip
|
||||
}
|
||||
addr := c.Request.RemoteAddr
|
||||
ip, _, _ := net.SplitHostPort(addr)
|
||||
return ip
|
||||
}
|
||||
|
||||
func jsonMsg(c *gin.Context, msg string, err error) {
|
||||
@@ -39,12 +42,12 @@ func jsonMsgObj(c *gin.Context, msg string, obj interface{}, err error) {
|
||||
if err == nil {
|
||||
m.Success = true
|
||||
if msg != "" {
|
||||
m.Msg = msg + I18nWeb(c, "success")
|
||||
m.Msg = msg + " " + I18nWeb(c, "success")
|
||||
}
|
||||
} else {
|
||||
m.Success = false
|
||||
m.Msg = msg + I18nWeb(c, "fail") + ": " + err.Error()
|
||||
logger.Warning(msg+I18nWeb(c, "fail")+": ", err)
|
||||
m.Msg = msg + " " + I18nWeb(c, "fail") + ": " + err.Error()
|
||||
logger.Warning(msg+" "+I18nWeb(c, "fail")+": ", err)
|
||||
}
|
||||
c.JSON(http.StatusOK, m)
|
||||
}
|
||||
@@ -61,7 +64,18 @@ func html(c *gin.Context, name string, title string, data gin.H) {
|
||||
data = gin.H{}
|
||||
}
|
||||
data["title"] = title
|
||||
data["host"] = strings.Split(c.Request.Host, ":")[0]
|
||||
host := c.GetHeader("X-Forwarded-Host")
|
||||
if host == "" {
|
||||
host = c.GetHeader("X-Real-IP")
|
||||
}
|
||||
if host == "" {
|
||||
var err error
|
||||
host, _, err = net.SplitHostPort(c.Request.Host)
|
||||
if err != nil {
|
||||
host = c.Request.Host
|
||||
}
|
||||
}
|
||||
data["host"] = host
|
||||
data["request_uri"] = c.Request.RequestURI
|
||||
data["base_path"] = c.GetString("base_path")
|
||||
c.HTML(http.StatusOK, name, getContext(data))
|
||||
|
||||
@@ -12,6 +12,7 @@ type XraySettingController struct {
|
||||
InboundService service.InboundService
|
||||
OutboundService service.OutboundService
|
||||
XrayService service.XrayService
|
||||
WarpService service.WarpService
|
||||
}
|
||||
|
||||
func NewXraySettingController(g *gin.RouterGroup) *XraySettingController {
|
||||
@@ -72,16 +73,18 @@ func (a *XraySettingController) warp(c *gin.Context) {
|
||||
var err error
|
||||
switch action {
|
||||
case "data":
|
||||
resp, err = a.XraySettingService.GetWarp()
|
||||
resp, err = a.WarpService.GetWarpData()
|
||||
case "del":
|
||||
err = a.WarpService.DelWarpData()
|
||||
case "config":
|
||||
resp, err = a.XraySettingService.GetWarpConfig()
|
||||
resp, err = a.WarpService.GetWarpConfig()
|
||||
case "reg":
|
||||
skey := c.PostForm("privateKey")
|
||||
pkey := c.PostForm("publicKey")
|
||||
resp, err = a.XraySettingService.RegWarp(skey, pkey)
|
||||
resp, err = a.WarpService.RegWarp(skey, pkey)
|
||||
case "license":
|
||||
license := c.PostForm("license")
|
||||
resp, err = a.XraySettingService.SetWarpLicence(license)
|
||||
resp, err = a.WarpService.SetWarpLicense(license)
|
||||
}
|
||||
|
||||
jsonObj(c, resp, err)
|
||||
|
||||
@@ -52,6 +52,7 @@ type AllSetting struct {
|
||||
SubJsonPath string `json:"subJsonPath" form:"subJsonPath"`
|
||||
SubJsonURI string `json:"subJsonURI" form:"subJsonURI"`
|
||||
SubJsonFragment string `json:"subJsonFragment" form:"subJsonFragment"`
|
||||
SubJsonNoises string `json:"subJsonNoises" form:"subJsonNoises"`
|
||||
SubJsonMux string `json:"subJsonMux" form:"subJsonMux"`
|
||||
SubJsonRules string `json:"subJsonRules" form:"subJsonRules"`
|
||||
Datepicker string `json:"datepicker" form:"datepicker"`
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
{{define "head"}}
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="renderer" content="webkit">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue@1.7.8/antd.min.css">
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/element-ui@2.15.0/theme-chalk/display.css">
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/css/custom.css?{{ .cur_ver }}">
|
||||
<style>
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
}
|
||||
/* vazirmatn-regular - arabic_latin_latin-ext */
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
font-family: 'Vazirmatn';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('{{ .base_path }}assets/Vazirmatn-UI-NL-Regular.woff2') format('woff2');
|
||||
unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC, U+0030-0039;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Vazirmatn', 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
|
||||
'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji',
|
||||
'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
}
|
||||
</style>
|
||||
<title>{{ .host }}-{{ i18n .title}}</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="renderer" content="webkit">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue/antd.min.css">
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/element-ui/theme-chalk/display.css">
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/css/custom.min.css?{{ .cur_ver }}">
|
||||
<style>
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
}
|
||||
/* vazirmatn-regular - arabic_latin_latin-ext */
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
font-family: 'Vazirmatn';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('{{ .base_path }}assets/Vazirmatn-UI-NL-Regular.woff2') format('woff2');
|
||||
unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC, U+0030-0039;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Vazirmatn', 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
|
||||
'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji',
|
||||
'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
}
|
||||
</style>
|
||||
<title>{{ .host }}-{{ i18n .title}}</title>
|
||||
</head>
|
||||
<div id="message"></div>
|
||||
{{end}}
|
||||
@@ -1,7 +1,7 @@
|
||||
{{define "js"}}
|
||||
<script src="{{ .base_path }}assets/vue/vue.min.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/moment/moment.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/ant-design-vue@1.7.8/antd.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/ant-design-vue/antd.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/axios/axios.min.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/qs/qs.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/js/axios-init.js?{{ .cur_ver }}"></script>
|
||||
|
||||
@@ -1,110 +1,165 @@
|
||||
{{define "qrcodeModal"}}
|
||||
<a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title"
|
||||
:dialog-style="{ top: '20px' }"
|
||||
:dialog-style="isMobileQr ? { top: '18px' } : {}"
|
||||
:closable="true"
|
||||
:class="themeSwitcher.currentTheme"
|
||||
:footer="null" width="300px">
|
||||
<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>{{ i18n "pages.settings.subSettings"}}</a-divider>
|
||||
<div class="qr-bg"><canvas @click="copyToClipboard('qrCode-sub',genSubLink(qrModal.client.subId))" id="qrCode-sub" class="qr-cv"></canvas></div>
|
||||
<a-divider>{{ i18n "pages.settings.subSettings"}} Json</a-divider>
|
||||
<div class="qr-bg"><canvas @click="copyToClipboard('qrCode-subJson',genSubJsonLink(qrModal.client.subId))" id="qrCode-subJson" class="qr-cv"></canvas></div>
|
||||
</template>
|
||||
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
|
||||
<template v-for="(row, index) in qrModal.qrcodes">
|
||||
<a-tag color="green" style="margin: 10px 0; display: block; text-align: center;">[[ row.remark ]]</a-tag>
|
||||
<div class="qr-bg"><canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" class="qr-cv"></canvas></div>
|
||||
</template>
|
||||
:footer="null" width="fit-content">
|
||||
<tr-qr-modal class="qr-modal">
|
||||
<template v-if="app.subSettings.enable && qrModal.subId">
|
||||
<tr-qr-box class="qr-box">
|
||||
<a-tag color="purple" class="qr-tag"><span>{{ i18n "pages.settings.subSettings"}}</span></a-tag>
|
||||
<tr-qr-bg class="qr-bg-sub">
|
||||
<tr-qr-bg-inner class="qr-bg-sub-inner">
|
||||
<canvas @click="copyToClipboard('qrCode-sub',genSubLink(qrModal.client.subId))" id="qrCode-sub" class="qr-cv"></canvas>
|
||||
</tr-qr-bg-inner>
|
||||
</tr-qr-bg>
|
||||
</tr-qr-box>
|
||||
<tr-qr-box class="qr-box">
|
||||
<a-tag color="purple" class="qr-tag"><span>{{ i18n "pages.settings.subSettings"}} Json</span></a-tag>
|
||||
<tr-qr-bg class="qr-bg-sub">
|
||||
<tr-qr-bg-inner class="qr-bg-sub-inner">
|
||||
<canvas @click="copyToClipboard('qrCode-subJson',genSubJsonLink(qrModal.client.subId))" id="qrCode-subJson" class="qr-cv"></canvas>
|
||||
</tr-qr-bg-inner>
|
||||
</tr-qr-bg>
|
||||
</tr-qr-box>
|
||||
</template>
|
||||
<template v-for="(row, index) in qrModal.qrcodes">
|
||||
<tr-qr-box class="qr-box">
|
||||
<a-tag color="green" class="qr-tag"><span>[[ row.remark ]]</span></a-tag>
|
||||
<tr-qr-bg class="qr-bg">
|
||||
<canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" class="qr-cv"></canvas>
|
||||
</tr-qr-bg>
|
||||
</tr-qr-box>
|
||||
</template>
|
||||
</tr-qr-modal>
|
||||
</a-modal>
|
||||
|
||||
<script>
|
||||
const isMobileQr = window.innerWidth <= 768;
|
||||
const qrModal = {
|
||||
title: '',
|
||||
dbInbound: new DBInbound(),
|
||||
client: null,
|
||||
qrcodes: [],
|
||||
clipboard: null,
|
||||
visible: false,
|
||||
subId: '',
|
||||
show: function(title = '', dbInbound, client) {
|
||||
this.title = title;
|
||||
this.dbInbound = dbInbound;
|
||||
this.inbound = dbInbound.toInbound();
|
||||
this.client = client;
|
||||
this.subId = '';
|
||||
this.qrcodes = [];
|
||||
if (this.inbound.protocol == Protocols.WIREGUARD) {
|
||||
this.inbound.genInboundLinks(dbInbound.remark).split('\r\n').forEach((l, index) => {
|
||||
this.qrcodes.push({
|
||||
remark: "Peer " + (index + 1),
|
||||
link: l
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.inbound.genAllLinks(this.dbInbound.remark, app.remarkModel, client).forEach(l => {
|
||||
this.qrcodes.push({
|
||||
remark: l.remark,
|
||||
link: l.link
|
||||
});
|
||||
});
|
||||
}
|
||||
this.visible = true;
|
||||
},
|
||||
close: function() {
|
||||
this.visible = false;
|
||||
},
|
||||
};
|
||||
const qrModalApp = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#qrcode-modal',
|
||||
data: {
|
||||
qrModal: qrModal,
|
||||
},
|
||||
methods: {
|
||||
copyToClipboard(elementId, content) {
|
||||
this.qrModal.clipboard = new ClipboardJS('#' + elementId, {
|
||||
text: () => content,
|
||||
});
|
||||
this.qrModal.clipboard.on('success', () => {
|
||||
app.$message.success('{{ i18n "copied" }}')
|
||||
this.qrModal.clipboard.destroy();
|
||||
});
|
||||
},
|
||||
setQrCode(elementId, content) {
|
||||
new QRious({
|
||||
element: document.querySelector('#' + elementId),
|
||||
size: 400,
|
||||
value: content,
|
||||
background: 'white',
|
||||
backgroundAlpha: 0,
|
||||
foreground: 'black',
|
||||
padding: 2,
|
||||
level: 'L'
|
||||
});
|
||||
},
|
||||
genSubLink(subID) {
|
||||
return app.subSettings.subURI + subID;
|
||||
},
|
||||
genSubJsonLink(subID) {
|
||||
return app.subSettings.subJsonURI + subID;
|
||||
},
|
||||
revertOverflow() {
|
||||
const elements = document.querySelectorAll(".qr-tag");
|
||||
elements.forEach((element) => {
|
||||
element.classList.remove("tr-marquee");
|
||||
element.children[0].style.animation = '';
|
||||
while (element.children.length > 1) {
|
||||
element.removeChild(element.lastChild);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
updated() {
|
||||
if (this.qrModal.visible) {
|
||||
fixOverflow();
|
||||
} else {
|
||||
this.revertOverflow();
|
||||
}
|
||||
if (qrModal.client && qrModal.client.subId) {
|
||||
qrModal.subId = qrModal.client.subId;
|
||||
this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId));
|
||||
this.setQrCode("qrCode-subJson", this.genSubJsonLink(qrModal.subId));
|
||||
}
|
||||
qrModal.qrcodes.forEach((element, index) => {
|
||||
this.setQrCode("qrCode-" + index, element.link);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const qrModal = {
|
||||
title: '',
|
||||
dbInbound: new DBInbound(),
|
||||
client: null,
|
||||
qrcodes: [],
|
||||
clipboard: null,
|
||||
visible: false,
|
||||
subId: '',
|
||||
show: function (title = '', dbInbound, client) {
|
||||
this.title = title;
|
||||
this.dbInbound = dbInbound;
|
||||
this.inbound = dbInbound.toInbound();
|
||||
this.client = client;
|
||||
this.subId = '';
|
||||
this.qrcodes = [];
|
||||
if (this.inbound.protocol == Protocols.WIREGUARD){
|
||||
this.inbound.genInboundLinks(dbInbound.remark).split('\r\n').forEach((l,index) =>{
|
||||
this.qrcodes.push({
|
||||
remark: "Peer " + (index+1),
|
||||
link: l
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.inbound.genAllLinks(this.dbInbound.remark, app.remarkModel, client).forEach(l => {
|
||||
this.qrcodes.push({
|
||||
remark: l.remark,
|
||||
link: l.link
|
||||
});
|
||||
});
|
||||
}
|
||||
this.visible = true;
|
||||
},
|
||||
close: function () {
|
||||
this.visible = false;
|
||||
},
|
||||
};
|
||||
function fixOverflow() {
|
||||
const elements = document.querySelectorAll(".qr-tag");
|
||||
elements.forEach((element) => {
|
||||
function isElementOverflowing(element) {
|
||||
const overflowX = element.offsetWidth < element.scrollWidth,
|
||||
overflowY = element.offsetHeight < element.scrollHeight;
|
||||
return overflowX || overflowY;
|
||||
}
|
||||
|
||||
const qrModalApp = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#qrcode-modal',
|
||||
data: {
|
||||
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: 400,
|
||||
value: content,
|
||||
background: 'white',
|
||||
backgroundAlpha: 0,
|
||||
foreground: 'black',
|
||||
padding: 2,
|
||||
level: 'L'
|
||||
});
|
||||
},
|
||||
genSubLink(subID) {
|
||||
return app.subSettings.subURI+subID;
|
||||
},
|
||||
genSubJsonLink(subID) {
|
||||
return app.subSettings.subJsonURI+subID;
|
||||
}
|
||||
},
|
||||
updated() {
|
||||
if (qrModal.client && qrModal.client.subId) {
|
||||
qrModal.subId = qrModal.client.subId;
|
||||
this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId));
|
||||
this.setQrCode("qrCode-subJson", this.genSubJsonLink(qrModal.subId));
|
||||
}
|
||||
qrModal.qrcodes.forEach((element, index) => {
|
||||
this.setQrCode("qrCode-" + index, element.link);
|
||||
});
|
||||
function wrapContentsInMarquee(element) {
|
||||
element.classList.add("tr-marquee");
|
||||
element.children[0].style.animation = `move-ltr ${
|
||||
(element.children[0].clientWidth / element.clientWidth) * 5
|
||||
}s ease-in-out infinite`;
|
||||
const marqueeText = element.children[0];
|
||||
if (element.children.length < 2) {
|
||||
for (let i = 0; i < 1; i++) {
|
||||
const marqueeText = element.children[0].cloneNode(true);
|
||||
element.children[0].after(marqueeText);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isElementOverflowing(element)) {
|
||||
wrapContentsInMarquee(element);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<a-row type="flex" justify="center" align="middle" style="height: 100%; overflow: auto;">
|
||||
<a-row type="flex" justify="center" align="middle" style="height: 100%; overflow: auto; overflow-x: hidden;">
|
||||
<a-col :xs="22" :sm="20" :md="14" :lg="10" :xl="8" :xxl="6" id="login" style="margin: 3rem 0;">
|
||||
<a-row type="flex" justify="center">
|
||||
<a-col style="width: 100%;">
|
||||
@@ -416,19 +416,19 @@
|
||||
<a-col span="24">
|
||||
<a-form>
|
||||
<a-form-item>
|
||||
<a-input autocomplete="username" v-model.trim="user.username" placeholder='{{ i18n "username" }}'
|
||||
<a-input autocomplete="username" name="username" v-model.trim="user.username" placeholder='{{ i18n "username" }}'
|
||||
@keydown.enter.native="login" autofocus>
|
||||
<a-icon slot="prefix" type="user" style="font-size: 16px;"></a-icon>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<password-input autocomplete="current-password" icon="lock" v-model.trim="user.password"
|
||||
<password-input autocomplete="password" name="password" icon="lock" v-model.trim="user.password"
|
||||
placeholder='{{ i18n "password" }}'
|
||||
@keydown.enter.native="login">
|
||||
</password-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="secretEnable">
|
||||
<password-input autocomplete="secret" icon="key" v-model.trim="user.loginSecret"
|
||||
<password-input autocomplete="secret" name="secret" icon="key" v-model.trim="user.loginSecret"
|
||||
placeholder='{{ i18n "secretToken" }}'
|
||||
@keydown.enter.native="login">
|
||||
</password-input>
|
||||
@@ -449,7 +449,7 @@
|
||||
<a-row justify="center" class="centered">
|
||||
<a-col :span="24">
|
||||
<a-select ref="selectLang" v-model="lang"
|
||||
@change="setLang(lang)" style="width: 150px;"
|
||||
@change="setLang(lang)" style="width: 200px;"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option :value="l.value" label="English" v-for="l in supportLangs">
|
||||
<span role="img" aria-label="l.name" v-text="l.icon"></span>
|
||||
@@ -461,7 +461,7 @@
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-row justify="center" class="centered">
|
||||
<theme-switch></theme-switch>
|
||||
<theme-switch-login></theme-switch-login>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
@@ -476,83 +476,82 @@
|
||||
{{template "component/themeSwitcher" .}}
|
||||
{{template "component/password" .}}
|
||||
<script>
|
||||
class User {
|
||||
constructor() {
|
||||
this.username = "";
|
||||
this.password = "";
|
||||
class User {
|
||||
constructor() {
|
||||
this.username = "";
|
||||
this.password = "";
|
||||
}
|
||||
}
|
||||
const app = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#app',
|
||||
data: {
|
||||
themeSwitcher,
|
||||
loading: false,
|
||||
user: new User(),
|
||||
secretEnable: false,
|
||||
lang: ""
|
||||
},
|
||||
async created() {
|
||||
this.lang = getLang();
|
||||
this.secretEnable = await this.getSecretStatus();
|
||||
},
|
||||
methods: {
|
||||
async login() {
|
||||
this.loading = true;
|
||||
const msg = await HttpUtil.post('/login', this.user);
|
||||
this.loading = false;
|
||||
if (msg.success) {
|
||||
location.href = basePath + 'panel/';
|
||||
}
|
||||
},
|
||||
async getSecretStatus() {
|
||||
this.loading = true;
|
||||
const msg = await HttpUtil.post('/getSecretStatus');
|
||||
this.loading = false;
|
||||
if (msg.success) {
|
||||
this.secretEnable = msg.obj;
|
||||
return msg.obj;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
var animationDelay = 2000;
|
||||
initHeadline();
|
||||
|
||||
function initHeadline() {
|
||||
animateHeadline(document.querySelectorAll('.headline'));
|
||||
}
|
||||
|
||||
const app = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#app',
|
||||
data: {
|
||||
themeSwitcher,
|
||||
loading: false,
|
||||
user: new User(),
|
||||
secretEnable: false,
|
||||
lang: ""
|
||||
},
|
||||
async created() {
|
||||
this.lang = getLang();
|
||||
this.secretEnable = await this.getSecretStatus();
|
||||
},
|
||||
methods: {
|
||||
async login() {
|
||||
this.loading = true;
|
||||
const msg = await HttpUtil.post('/login', this.user);
|
||||
this.loading = false;
|
||||
if (msg.success) {
|
||||
location.href = basePath + 'panel/';
|
||||
}
|
||||
},
|
||||
async getSecretStatus() {
|
||||
this.loading = true;
|
||||
const msg = await HttpUtil.post('/getSecretStatus');
|
||||
this.loading = false;
|
||||
if (msg.success) {
|
||||
this.secretEnable = msg.obj;
|
||||
return msg.obj;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
var animationDelay = 2000;
|
||||
initHeadline();
|
||||
function animateHeadline(headlines) {
|
||||
var duration = animationDelay;
|
||||
headlines.forEach(function(headline) {
|
||||
setTimeout(function() {
|
||||
hideWord(headline.querySelector('.is-visible'));
|
||||
}, duration);
|
||||
});
|
||||
}
|
||||
|
||||
function initHeadline() {
|
||||
animateHeadline(document.querySelectorAll('.headline'));
|
||||
}
|
||||
function hideWord(word) {
|
||||
var nextWord = takeNext(word);
|
||||
switchWord(word, nextWord);
|
||||
setTimeout(function() {
|
||||
hideWord(nextWord);
|
||||
}, animationDelay);
|
||||
}
|
||||
|
||||
function animateHeadline(headlines) {
|
||||
var duration = animationDelay;
|
||||
headlines.forEach(function(headline) {
|
||||
setTimeout(function() {
|
||||
hideWord(headline.querySelector('.is-visible'));
|
||||
}, duration);
|
||||
});
|
||||
}
|
||||
function takeNext(word) {
|
||||
return (word.nextElementSibling) ? word.nextElementSibling : word.parentElement.firstElementChild;
|
||||
}
|
||||
|
||||
function hideWord(word) {
|
||||
var nextWord = takeNext(word);
|
||||
switchWord(word, nextWord);
|
||||
setTimeout(function() {
|
||||
hideWord(nextWord);
|
||||
}, animationDelay);
|
||||
}
|
||||
|
||||
function takeNext(word) {
|
||||
return (word.nextElementSibling) ? word.nextElementSibling : word.parentElement.firstElementChild;
|
||||
}
|
||||
|
||||
function switchWord(oldWord, newWord) {
|
||||
oldWord.classList.remove('is-visible');
|
||||
oldWord.classList.add('is-hidden');
|
||||
newWord.classList.remove('is-hidden');
|
||||
newWord.classList.add('is-visible');
|
||||
}
|
||||
});
|
||||
function switchWord(oldWord, newWord) {
|
||||
oldWord.classList.remove('is-visible');
|
||||
oldWord.classList.add('is-hidden');
|
||||
newWord.classList.remove('is-hidden');
|
||||
newWord.classList.add('is-visible');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -20,14 +20,19 @@
|
||||
<a-input-number v-model="clientsBulkModal.lastNum" :min="clientsBulkModal.firstNum"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.client.prefix" }}' v-if="clientsBulkModal.emailMethod>0">
|
||||
<a-input v-model="clientsBulkModal.emailPrefix"></a-input>
|
||||
<a-input v-model.trim="clientsBulkModal.emailPrefix"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.client.postfix" }}' v-if="clientsBulkModal.emailMethod>2">
|
||||
<a-input v-model="clientsBulkModal.emailPostfix"></a-input>
|
||||
<a-input v-model.trim="clientsBulkModal.emailPostfix"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.client.clientCount" }}' v-if="clientsBulkModal.emailMethod < 2">
|
||||
<a-input-number v-model="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "security" }}' v-if="inbound.protocol === Protocols.VMESS">
|
||||
<a-select v-model="clientsBulkModal.security" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Flow' v-if="clientsBulkModal.inbound.canEnableTlsFlow()">
|
||||
<a-select v-model="clientsBulkModal.flow" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
|
||||
@@ -58,13 +63,13 @@
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
|
||||
</template>
|
||||
Telegram ID
|
||||
Telegram ChatID
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input-number style="width: 50%" v-model.trim="clientsBulkModal.tgId" min="0"></a-input-number>
|
||||
<a-input-number style="width: 50%" v-model="clientsBulkModal.tgId" min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-form-item v-if="app.ipLimitEnable">
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
@@ -104,18 +109,20 @@
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-date-picker v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme" v-model="clientsBulkModal.expiryTime"></a-date-picker>
|
||||
<a-date-picker v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }"
|
||||
format="YYYY-MM-DD HH:mm:ss" :dropdown-class-name="themeSwitcher.currentTheme"
|
||||
v-model="clientsBulkModal.expiryTime"></a-date-picker>
|
||||
<persian-datepicker v-else placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
|
||||
value="clientsBulkModal.expiryTime" v-model="clientsBulkModal.expiryTime"></persian-datepicker>
|
||||
value="clientsBulkModal.expiryTime" v-model="clientsBulkModal.expiryTime">
|
||||
</persian-datepicker>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="clientsBulkModal.expiryTime != 0">
|
||||
<template slot="label">
|
||||
<span>{{ i18n "pages.client.renew" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.client.renewDesc" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.client.renew" }}
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
@@ -143,7 +150,8 @@
|
||||
emailPrefix: "",
|
||||
emailPostfix: "",
|
||||
subId: "",
|
||||
tgId: 0,
|
||||
tgId: '',
|
||||
security: "auto",
|
||||
flow: "",
|
||||
delayedStart: false,
|
||||
reset: 0,
|
||||
@@ -165,7 +173,8 @@
|
||||
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.tgId = clientsBulkModal.tgId;
|
||||
newClient.security = clientsBulkModal.security;
|
||||
newClient.limitIp = clientsBulkModal.limitIp;
|
||||
newClient._totalGB = clientsBulkModal.totalGB;
|
||||
newClient._expiryTime = clientsBulkModal.expiryTime;
|
||||
@@ -200,7 +209,8 @@
|
||||
this.emailPrefix = "";
|
||||
this.emailPostfix = "";
|
||||
this.subId = "";
|
||||
this.tgId = 0;
|
||||
this.tgId = '';
|
||||
this.security = "auto";
|
||||
this.flow = "";
|
||||
this.dbInbound = new DBInbound(dbInbound);
|
||||
this.inbound = dbInbound.toInbound();
|
||||
@@ -209,7 +219,7 @@
|
||||
},
|
||||
newClient(protocol) {
|
||||
switch (protocol) {
|
||||
case Protocols.VMESS: return new Inbound.VmessSettings.Vmess();
|
||||
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(clientsBulkModal.inbound.settings.shadowsockses[0].method);
|
||||
@@ -220,7 +230,7 @@
|
||||
clientsBulkModal.visible = false;
|
||||
clientsBulkModal.loading(false);
|
||||
},
|
||||
loading(loading=true) {
|
||||
loading(loading = true) {
|
||||
clientsBulkModal.confirmLoading = loading;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
},
|
||||
addClient(protocol, clients) {
|
||||
switch (protocol) {
|
||||
case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.Vmess());
|
||||
case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.VMESS());
|
||||
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
|
||||
case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
|
||||
case Protocols.SHADOWSOCKS: return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks(clients[0].method));
|
||||
|
||||
@@ -1,61 +1,65 @@
|
||||
{{define "menuItems"}}
|
||||
<a-menu-item key="{{ .base_path }}panel/">
|
||||
<a-icon type="dashboard"></a-icon>
|
||||
<span><b>{{ i18n "menu.dashboard"}}</b></span>
|
||||
<a-icon type="dashboard"></a-icon>
|
||||
<span>
|
||||
<b>{{ i18n "menu.dashboard"}}</b>
|
||||
</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="{{ .base_path }}panel/inbounds">
|
||||
<a-icon type="user"></a-icon>
|
||||
<span><b>{{ i18n "menu.inbounds"}}</b></span>
|
||||
<a-icon type="user"></a-icon>
|
||||
<span>
|
||||
<b>{{ i18n "menu.inbounds"}}</b>
|
||||
</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="{{ .base_path }}panel/settings">
|
||||
<a-icon type="setting"></a-icon>
|
||||
<span><b>{{ i18n "menu.settings"}}</b></span>
|
||||
<a-icon type="setting"></a-icon>
|
||||
<span>
|
||||
<b>{{ i18n "menu.settings"}}</b>
|
||||
</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="{{ .base_path }}panel/xray">
|
||||
<a-icon type="tool"></a-icon>
|
||||
<span><b>{{ i18n "menu.xray"}}</b></span>
|
||||
<a-icon type="tool"></a-icon>
|
||||
<span>
|
||||
<b>{{ i18n "menu.xray"}}</b>
|
||||
</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="{{ .base_path }}logout">
|
||||
<a-icon type="logout"></a-icon>
|
||||
<span><b>{{ i18n "menu.logout"}}</b></span>
|
||||
<a-icon type="logout"></a-icon>
|
||||
<span>
|
||||
<b>{{ i18n "menu.logout"}}</b>
|
||||
</span>
|
||||
</a-menu-item>
|
||||
{{end}}
|
||||
|
||||
|
||||
{{define "commonSider"}}
|
||||
<a-layout-sider :theme="themeSwitcher.currentTheme" id="sider" collapsible breakpoint="md" collapsed-width="0">
|
||||
<theme-switch></theme-switch>
|
||||
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="['{{ .request_uri }}']"
|
||||
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
|
||||
{{template "menuItems" .}}
|
||||
</a-menu>
|
||||
<a-layout-sider :theme="themeSwitcher.currentTheme" id="sider" collapsible breakpoint="md">
|
||||
<theme-switch></theme-switch>
|
||||
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="['{{ .request_uri }}']" @click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
|
||||
{{template "menuItems" .}}
|
||||
</a-menu>
|
||||
</a-layout-sider>
|
||||
<a-drawer id="sider-drawer" placement="left" :closable="false"
|
||||
@close="siderDrawer.close()"
|
||||
:visible="siderDrawer.visible"
|
||||
:wrap-class-name="themeSwitcher.currentTheme"
|
||||
:wrap-style="{ padding: 0 }">
|
||||
<div class="drawer-handle" @click="siderDrawer.change()" slot="handle">
|
||||
<a-icon :type="siderDrawer.visible ? 'close' : 'menu-fold'"></a-icon>
|
||||
</div>
|
||||
<theme-switch></theme-switch>
|
||||
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="['{{ .request_uri }}']"
|
||||
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
|
||||
{{template "menuItems" .}}
|
||||
</a-menu>
|
||||
<a-drawer id="sider-drawer" placement="left" :closable="false" @close="siderDrawer.close()" :visible="siderDrawer.visible" :wrap-class-name="themeSwitcher.currentTheme" :wrap-style="{ padding: 0 }">
|
||||
<div class="drawer-handle" @click="siderDrawer.change()" slot="handle">
|
||||
<a-icon :type="siderDrawer.visible ? 'close' : 'menu-fold'"></a-icon>
|
||||
</div>
|
||||
<theme-switch></theme-switch>
|
||||
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="['{{ .request_uri }}']" @click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
|
||||
{{template "menuItems" .}}
|
||||
</a-menu>
|
||||
</a-drawer>
|
||||
<script>
|
||||
const siderDrawer = {
|
||||
visible: false,
|
||||
show() {
|
||||
this.visible = true;
|
||||
},
|
||||
close() {
|
||||
this.visible = false;
|
||||
},
|
||||
change() {
|
||||
this.visible = !this.visible;
|
||||
},
|
||||
};
|
||||
const siderDrawer = {
|
||||
visible: false,
|
||||
show() {
|
||||
this.visible = true;
|
||||
},
|
||||
close() {
|
||||
this.visible = false;
|
||||
},
|
||||
change() {
|
||||
this.visible = !this.visible;
|
||||
},
|
||||
};
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{{define "component/passwordInput"}}
|
||||
<template>
|
||||
<a-input :value="value" :type="showPassword ? 'text' : 'password'"
|
||||
:placeholder="placeholder"
|
||||
@input="$emit('input', $event.target.value)">
|
||||
:placeholder="placeholder"
|
||||
:autocomplete="autocomplete"
|
||||
:name="name"
|
||||
@input="$emit('input', $event.target.value)">
|
||||
<template v-if="icon" #prefix>
|
||||
<a-icon :type="icon" style="font-size: 16px;" />
|
||||
</template>
|
||||
@@ -18,7 +20,7 @@
|
||||
{{define "component/password"}}
|
||||
<script>
|
||||
Vue.component('password-input', {
|
||||
props: ["title", "value", "placeholder", "icon"],
|
||||
props: ["title", "value", "placeholder", "icon", "autocomplete", "name"],
|
||||
template: `{{template "component/passwordInput"}}`,
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -1,236 +1,216 @@
|
||||
{{define "component/sortableTableTrigger"}}
|
||||
<a-icon type="drag"
|
||||
class="sortable-icon"
|
||||
style="cursor: move;"
|
||||
@mouseup="mouseUpHandler"
|
||||
@mousedown="mouseDownHandler"
|
||||
@click="clickHandler" />
|
||||
<a-icon type="drag"
|
||||
class="sortable-icon"
|
||||
style="cursor: move;"
|
||||
@mouseup="mouseUpHandler"
|
||||
@mousedown="mouseDownHandler"
|
||||
@click="clickHandler" />
|
||||
{{end}}
|
||||
|
||||
{{define "component/sortableTable"}}
|
||||
<script>
|
||||
const DRAGGABLE_ROW_CLASS = 'draggable-row';
|
||||
|
||||
const findParentRowElement = (el) => {
|
||||
if (!el || !el.tagName) {
|
||||
return null;
|
||||
} else if (el.classList.contains(DRAGGABLE_ROW_CLASS)) {
|
||||
return el;
|
||||
} else if (el.parentNode) {
|
||||
return findParentRowElement(el.parentNode);
|
||||
const DRAGGABLE_ROW_CLASS = 'draggable-row';
|
||||
const findParentRowElement = (el) => {
|
||||
if (!el || !el.tagName) {
|
||||
return null;
|
||||
} else if (el.classList.contains(DRAGGABLE_ROW_CLASS)) {
|
||||
return el;
|
||||
} else if (el.parentNode) {
|
||||
return findParentRowElement(el.parentNode);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Vue.component('a-table-sortable', {
|
||||
data() {
|
||||
return {
|
||||
sortingElementIndex: null,
|
||||
newElementIndex: null,
|
||||
};
|
||||
},
|
||||
props: ['data-source', 'customRow'],
|
||||
inheritAttrs: false,
|
||||
provide() {
|
||||
const sortable = {}
|
||||
Object.defineProperty(sortable, "setSortableIndex", {
|
||||
enumerable: true,
|
||||
get: () => this.setCurrentSortableIndex,
|
||||
});
|
||||
Object.defineProperty(sortable, "resetSortableIndex", {
|
||||
enumerable: true,
|
||||
get: () => this.resetSortableIndex,
|
||||
});
|
||||
return {
|
||||
sortable,
|
||||
}
|
||||
},
|
||||
render: function(createElement) {
|
||||
return createElement('a-table', {
|
||||
class: {
|
||||
'ant-table-is-sorting': this.isDragging(),
|
||||
},
|
||||
props: {
|
||||
...this.$attrs,
|
||||
'data-source': this.records,
|
||||
customRow: (record, index) => this.customRowRender(record, index),
|
||||
},
|
||||
on: this.$listeners,
|
||||
nativeOn: {
|
||||
drop: (e) => this.dropHandler(e),
|
||||
},
|
||||
scopedSlots: this.$scopedSlots,
|
||||
}, this.$slots.default, )
|
||||
},
|
||||
created() {
|
||||
this.$memoSort = {};
|
||||
},
|
||||
methods: {
|
||||
isDragging() {
|
||||
const currentIndex = this.sortingElementIndex;
|
||||
return currentIndex !== null && currentIndex !== undefined;
|
||||
},
|
||||
resetSortableIndex(e, index) {
|
||||
this.sortingElementIndex = null;
|
||||
this.newElementIndex = null;
|
||||
this.$memoSort = {};
|
||||
},
|
||||
setCurrentSortableIndex(e, index) {
|
||||
this.sortingElementIndex = index;
|
||||
},
|
||||
dragStartHandler(e, index) {
|
||||
if (!this.isDragging()) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
const hideDragImage = this.$el.cloneNode(true);
|
||||
hideDragImage.id = "hideDragImage-hide";
|
||||
hideDragImage.style.opacity = 0;
|
||||
e.dataTransfer.setDragImage(hideDragImage, 0, 0);
|
||||
},
|
||||
dragStopHandler(e, index) {
|
||||
const hideDragImage = document.getElementById('hideDragImage-hide');
|
||||
if (hideDragImage) hideDragImage.remove();
|
||||
this.resetSortableIndex(e, index);
|
||||
},
|
||||
dragOverHandler(e, index) {
|
||||
if (!this.isDragging()) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
const currentIndex = this.sortingElementIndex;
|
||||
if (index === currentIndex) {
|
||||
this.newElementIndex = null;
|
||||
return;
|
||||
}
|
||||
const row = findParentRowElement(e.target);
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
const rect = row.getBoundingClientRect();
|
||||
const offsetTop = e.pageY - rect.top;
|
||||
if (offsetTop < rect.height / 2) {
|
||||
this.newElementIndex = Math.max(index - 1, 0);
|
||||
} else {
|
||||
return null;
|
||||
this.newElementIndex = index;
|
||||
}
|
||||
},
|
||||
dropHandler(e) {
|
||||
if (this.isDragging()) {
|
||||
this.$emit('onsort', this.sortingElementIndex, this.newElementIndex);
|
||||
}
|
||||
},
|
||||
customRowRender(record, index) {
|
||||
const parentMethodResult = this.customRow?.(record, index) || {};
|
||||
const newIndex = this.newElementIndex;
|
||||
const currentIndex = this.sortingElementIndex;
|
||||
return {
|
||||
...parentMethodResult,
|
||||
attrs: {
|
||||
...(parentMethodResult?.attrs || {}),
|
||||
draggable: true,
|
||||
},
|
||||
on: {
|
||||
...(parentMethodResult?.on || {}),
|
||||
dragstart: (e) => this.dragStartHandler(e, index),
|
||||
dragend: (e) => this.dragStopHandler(e, index),
|
||||
dragover: (e) => this.dragOverHandler(e, index),
|
||||
},
|
||||
class: {
|
||||
...(parentMethodResult?.class || {}),
|
||||
[DRAGGABLE_ROW_CLASS]: true,
|
||||
['dragging']: this.isDragging() ? (newIndex === null ? index === currentIndex : index === newIndex) : false,
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
records() {
|
||||
const newIndex = this.newElementIndex;
|
||||
const currentIndex = this.sortingElementIndex;
|
||||
if (!this.isDragging() || newIndex === null || currentIndex === newIndex) {
|
||||
return this.dataSource;
|
||||
}
|
||||
if (this.$memoSort.newIndex === newIndex) {
|
||||
return this.$memoSort.list;
|
||||
}
|
||||
let list = [...this.dataSource];
|
||||
list.splice(newIndex, 0, list.splice(currentIndex, 1)[0]);
|
||||
this.$memoSort = {
|
||||
newIndex,
|
||||
list,
|
||||
};
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
Vue.component('a-table-sortable', {
|
||||
data() {
|
||||
return {
|
||||
sortingElementIndex: null,
|
||||
newElementIndex: null,
|
||||
};
|
||||
},
|
||||
props: ['data-source', 'customRow'],
|
||||
inheritAttrs: false,
|
||||
provide() {
|
||||
const sortable = {}
|
||||
|
||||
Object.defineProperty(sortable, "setSortableIndex", {
|
||||
enumerable: true,
|
||||
get: () => this.setCurrentSortableIndex,
|
||||
});
|
||||
|
||||
Object.defineProperty(sortable, "resetSortableIndex", {
|
||||
enumerable: true,
|
||||
get: () => this.resetSortableIndex,
|
||||
});
|
||||
|
||||
return {
|
||||
sortable,
|
||||
}
|
||||
},
|
||||
render: function (createElement) {
|
||||
return createElement('a-table', {
|
||||
class: {
|
||||
'ant-table-is-sorting': this.isDragging(),
|
||||
},
|
||||
props: {
|
||||
...this.$attrs,
|
||||
'data-source': this.records,
|
||||
customRow: (record, index) => this.customRowRender(record, index),
|
||||
},
|
||||
on: this.$listeners,
|
||||
nativeOn: {
|
||||
drop: (e) => this.dropHandler(e),
|
||||
},
|
||||
scopedSlots: this.$scopedSlots,
|
||||
}, this.$slots.default, )
|
||||
},
|
||||
created() {
|
||||
this.$memoSort = {};
|
||||
},
|
||||
methods: {
|
||||
isDragging() {
|
||||
const currentIndex = this.sortingElementIndex;
|
||||
return currentIndex !== null && currentIndex !== undefined;
|
||||
},
|
||||
resetSortableIndex(e, index) {
|
||||
this.sortingElementIndex = null;
|
||||
this.newElementIndex = null;
|
||||
this.$memoSort = {};
|
||||
},
|
||||
setCurrentSortableIndex(e, index) {
|
||||
this.sortingElementIndex = index;
|
||||
},
|
||||
dragStartHandler(e, index) {
|
||||
if (!this.isDragging()) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
const hideDragImage = this.$el.cloneNode(true);
|
||||
hideDragImage.id = "hideDragImage-hide";
|
||||
hideDragImage.style.opacity = 0;
|
||||
e.dataTransfer.setDragImage(hideDragImage, 0, 0);
|
||||
},
|
||||
dragStopHandler(e, index) {
|
||||
const hideDragImage = document.getElementById('hideDragImage-hide');
|
||||
if (hideDragImage) hideDragImage.remove();
|
||||
this.resetSortableIndex(e, index);
|
||||
},
|
||||
dragOverHandler(e, index) {
|
||||
if (!this.isDragging()) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const currentIndex = this.sortingElementIndex;
|
||||
if (index === currentIndex) {
|
||||
this.newElementIndex = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const row = findParentRowElement(e.target);
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = row.getBoundingClientRect();
|
||||
const offsetTop = e.pageY - rect.top;
|
||||
|
||||
if (offsetTop < rect.height / 2) {
|
||||
this.newElementIndex = Math.max(index - 1, 0);
|
||||
} else {
|
||||
this.newElementIndex = index;
|
||||
}
|
||||
},
|
||||
dropHandler(e) {
|
||||
if (this.isDragging()) {
|
||||
this.$emit('onsort', this.sortingElementIndex, this.newElementIndex);
|
||||
}
|
||||
},
|
||||
customRowRender(record, index) {
|
||||
const parentMethodResult = this.customRow?.(record, index) || {};
|
||||
const newIndex = this.newElementIndex;
|
||||
const currentIndex = this.sortingElementIndex;
|
||||
|
||||
return {
|
||||
...parentMethodResult,
|
||||
attrs: {
|
||||
...(parentMethodResult?.attrs || {}),
|
||||
draggable: true,
|
||||
},
|
||||
on: {
|
||||
...(parentMethodResult?.on || {}),
|
||||
dragstart: (e) => this.dragStartHandler(e, index),
|
||||
dragend: (e) => this.dragStopHandler(e, index),
|
||||
dragover: (e) => this.dragOverHandler(e, index),
|
||||
},
|
||||
class: {
|
||||
...(parentMethodResult?.class || {}),
|
||||
[DRAGGABLE_ROW_CLASS]: true,
|
||||
['dragging']: this.isDragging()
|
||||
? (newIndex === null ? index === currentIndex : index === newIndex)
|
||||
: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
records() {
|
||||
const newIndex = this.newElementIndex;
|
||||
const currentIndex = this.sortingElementIndex;
|
||||
|
||||
if (!this.isDragging() || newIndex === null || currentIndex === newIndex) {
|
||||
return this.dataSource;
|
||||
}
|
||||
|
||||
if (this.$memoSort.newIndex === newIndex) {
|
||||
return this.$memoSort.list;
|
||||
}
|
||||
|
||||
let list = [...this.dataSource];
|
||||
list.splice(newIndex, 0, list.splice(currentIndex, 1)[0]);
|
||||
|
||||
this.$memoSort = {
|
||||
newIndex,
|
||||
list,
|
||||
};
|
||||
|
||||
return list;
|
||||
}
|
||||
});
|
||||
Vue.component('table-sort-trigger', {
|
||||
template: `{{template "component/sortableTableTrigger"}}`,
|
||||
props: ['item-index'],
|
||||
inject: ['sortable'],
|
||||
methods: {
|
||||
mouseDownHandler(e) {
|
||||
if (this.sortable) {
|
||||
this.sortable.setSortableIndex(e, this.itemIndex);
|
||||
}
|
||||
});
|
||||
|
||||
Vue.component('table-sort-trigger', {
|
||||
template: `{{template "component/sortableTableTrigger"}}`,
|
||||
props: ['item-index'],
|
||||
inject: ['sortable'],
|
||||
methods: {
|
||||
mouseDownHandler(e) {
|
||||
if (this.sortable) {
|
||||
this.sortable.setSortableIndex(e, this.itemIndex);
|
||||
}
|
||||
},
|
||||
mouseUpHandler(e) {
|
||||
if (this.sortable) {
|
||||
this.sortable.resetSortableIndex(e, this.itemIndex);
|
||||
}
|
||||
},
|
||||
clickHandler(e) {
|
||||
e.preventDefault();
|
||||
},
|
||||
},
|
||||
mouseUpHandler(e) {
|
||||
if (this.sortable) {
|
||||
this.sortable.resetSortableIndex(e, this.itemIndex);
|
||||
}
|
||||
})
|
||||
},
|
||||
clickHandler(e) {
|
||||
e.preventDefault();
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@media only screen and (max-width: 767px) {
|
||||
.sortable-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.ant-table-is-sorting .draggable-row td {
|
||||
background-color: #ffffff !important;
|
||||
}
|
||||
.dark .ant-table-is-sorting .draggable-row td {
|
||||
background-color: var(--dark-color-surface-100) !important;
|
||||
}
|
||||
.ant-table-is-sorting .dragging td {
|
||||
background-color: rgb(232 244 242) !important;
|
||||
color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.dark .ant-table-is-sorting .dragging td {
|
||||
background-color: var(--dark-color-table-hover) !important;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
.ant-table-is-sorting .dragging {
|
||||
opacity: 1;
|
||||
box-shadow: 1px -2px 2px #008771;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.ant-table-is-sorting .dragging .ant-table-row-index {
|
||||
opacity: 0.3;
|
||||
@media only screen and (max-width: 767px) {
|
||||
.sortable-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.ant-table-is-sorting .draggable-row td {
|
||||
background-color: #ffffff !important;
|
||||
}
|
||||
.dark .ant-table-is-sorting .draggable-row td {
|
||||
background-color: var(--dark-color-surface-100) !important;
|
||||
}
|
||||
.ant-table-is-sorting .dragging td {
|
||||
background-color: rgb(232 244 242) !important;
|
||||
color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.dark .ant-table-is-sorting .dragging td {
|
||||
background-color: var(--dark-color-table-hover) !important;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
.ant-table-is-sorting .dragging {
|
||||
opacity: 1;
|
||||
box-shadow: 1px -2px 2px #008771;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.ant-table-is-sorting .dragging .ant-table-row-index {
|
||||
opacity: 0.3;
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
{{define "component/themeSwitchTemplate"}}
|
||||
<template>
|
||||
<a-menu class="change-theme" :theme="themeSwitcher.currentTheme" mode="inline" selected-keys="">
|
||||
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" selected-keys="">
|
||||
<a-sub-menu>
|
||||
<span slot="title">
|
||||
<a-icon type="bulb" :theme="themeSwitcher.isDarkTheme ? 'filled' : 'outlined'"></a-icon>
|
||||
<span>Theme</span>
|
||||
</span>
|
||||
<a-menu-item id="change-theme" class="ant-menu-theme-switch" @mousedown="themeSwitcher.animationsOff()"> Dark <a-switch style="margin-left: 2px;" size="small" :default-checked="themeSwitcher.isDarkTheme" @change="themeSwitcher.toggleTheme()"></a-switch>
|
||||
</a-menu-item>
|
||||
<a-menu-item id="change-theme-ultra" v-if="themeSwitcher.isDarkTheme" class="ant-menu-theme-switch" @mousedown="themeSwitcher.animationsOffUltra()"> Ultra <a-checkbox style="margin-left: 2px;" :checked="themeSwitcher.isUltra" @click="themeSwitcher.toggleUltra()"></a-checkbox>
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
</a-menu>
|
||||
</template>
|
||||
{{end}}
|
||||
|
||||
{{define "component/themeSwitchTemplateLogin"}}
|
||||
<template>
|
||||
<a-menu @mousedown="themeSwitcher.animationsOff()" id="change-theme" :theme="themeSwitcher.currentTheme" mode="inline" selected-keys="">
|
||||
<a-menu-item mode="inline" class="ant-menu-theme-switch">
|
||||
<a-icon type="bulb" :theme="themeSwitcher.isDarkTheme ? 'filled' : 'outlined'"></a-icon>
|
||||
<a-switch size="small" :default-checked="themeSwitcher.isDarkTheme" @change="themeSwitcher.toggleTheme()"></a-switch>
|
||||
@@ -23,6 +40,26 @@
|
||||
const theme = isDarkTheme ? 'dark' : 'light';
|
||||
document.querySelector('body').setAttribute('class', theme);
|
||||
return {
|
||||
animationsOff() {
|
||||
document.documentElement.setAttribute('data-theme-animations', 'off');
|
||||
const themeAnimations = document.querySelector('#change-theme');
|
||||
themeAnimations.addEventListener('mouseleave', () => {
|
||||
document.documentElement.removeAttribute('data-theme-animations');
|
||||
});
|
||||
themeAnimations.addEventListener('touchend', () => {
|
||||
document.documentElement.removeAttribute('data-theme-animations');
|
||||
});
|
||||
},
|
||||
animationsOffUltra() {
|
||||
document.documentElement.setAttribute('data-theme-animations', 'off');
|
||||
const themeAnimationsUltra = document.querySelector('#change-theme-ultra');
|
||||
themeAnimationsUltra.addEventListener('mouseleave', () => {
|
||||
document.documentElement.removeAttribute('data-theme-animations');
|
||||
});
|
||||
themeAnimationsUltra.addEventListener('touchend', () => {
|
||||
document.documentElement.removeAttribute('data-theme-animations');
|
||||
});
|
||||
},
|
||||
isDarkTheme,
|
||||
isUltra,
|
||||
get currentTheme() {
|
||||
@@ -57,13 +94,19 @@
|
||||
getContainer: () => document.getElementById('message')
|
||||
});
|
||||
document.getElementById('message').className = themeSwitcher.currentTheme;
|
||||
const themeAnimations = document.querySelector('.change-theme');
|
||||
themeAnimations.addEventListener('mousedown', () => {
|
||||
document.documentElement.setAttribute('data-theme-animations', 'off');
|
||||
});
|
||||
themeAnimations.addEventListener('mouseleave', () => {
|
||||
document.documentElement.removeAttribute('data-theme-animations');
|
||||
}
|
||||
});
|
||||
Vue.component('theme-switch-login', {
|
||||
props: [],
|
||||
template: `{{template "component/themeSwitchTemplateLogin"}}`,
|
||||
data: () => ({
|
||||
themeSwitcher
|
||||
}),
|
||||
mounted() {
|
||||
this.$message.config({
|
||||
getContainer: () => document.getElementById('message')
|
||||
});
|
||||
document.getElementById('message').className = themeSwitcher.currentTheme;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,86 +1,114 @@
|
||||
{{define "dnsModal"}}
|
||||
<a-modal id="dns-modal" v-model="dnsModal.visible" :title="dnsModal.title" @ok="dnsModal.ok"
|
||||
:closable="true" :mask-closable="false"
|
||||
:ok-text="dnsModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "pages.xray.outbound.address" }}'>
|
||||
<a-input v-model.trim="dnsModal.dnsServer.address"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.dns.domains" }}'>
|
||||
<a-button size="small" type="primary" @click="dnsModal.dnsServer.domains.push('')">+</a-button>
|
||||
<template v-for="(domain, index) in dnsModal.dnsServer.domains">
|
||||
<a-input v-model.trim="dnsModal.dnsServer.domains[index]">
|
||||
<a-button size="small" slot="addonAfter" @click="dnsModal.dnsServer.domains.splice(index,1)">-</a-button>
|
||||
</a-input>
|
||||
</template>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.dns.strategy" }}' v-if="isAdvanced">
|
||||
<a-select
|
||||
v-model="dnsModal.dnsServer.queryStrategy"
|
||||
style="width: 100%"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option :value="l" :label="l" v-for="l in ['UseIP', 'UseIPv4', 'UseIPv6']">
|
||||
[[ l ]]
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-modal id="dns-modal" v-model="dnsModal.visible" :title="dnsModal.title" @ok="dnsModal.ok" :closable="true"
|
||||
:mask-closable="false" :ok-text="dnsModal.okText" cancel-text='{{ i18n "close" }}'
|
||||
:class="themeSwitcher.currentTheme">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "pages.xray.outbound.address" }}'>
|
||||
<a-input v-model.trim="dnsModal.dnsServer.address"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.dns.domains" }}'>
|
||||
<a-button icon="plus" size="small" type="primary" @click="dnsModal.dnsServer.domains.push('')"></a-button>
|
||||
<template v-for="(domain, index) in dnsModal.dnsServer.domains">
|
||||
<a-input v-model.trim="dnsModal.dnsServer.domains[index]">
|
||||
<a-button icon="minus" size="small" slot="addonAfter"
|
||||
@click="dnsModal.dnsServer.domains.splice(index,1)"></a-button>
|
||||
</a-input>
|
||||
</template>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.dns.strategy" }}' v-if="isAdvanced">
|
||||
<a-select v-model="dnsModal.dnsServer.queryStrategy" style="width: 100%"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option :value="l" :label="l" v-for="l in ['UseIP', 'UseIPv4', 'UseIPv6']"> [[ l ]] </a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Skip Fallback' v-if="isAdvanced">
|
||||
<a-switch v-model="dnsModal.dnsServer.skipFallback"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.dns.expectIPs"}}'>
|
||||
<a-button icon="plus" size="small" type="primary" @click="dnsModal.dnsServer.expectIPs.push('')"></a-button>
|
||||
<template v-for="(domain, index) in dnsModal.dnsServer.expectIPs">
|
||||
<a-input v-model.trim="dnsModal.dnsServer.expectIPs[index]">
|
||||
<a-button icon="minus" size="small" slot="addonAfter"
|
||||
@click="dnsModal.dnsServer.expectIPs.splice(index,1)"></a-button>
|
||||
</a-input>
|
||||
</template>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
<script>
|
||||
const dnsModal = {
|
||||
title: '',
|
||||
visible: false,
|
||||
okText: '{{ i18n "confirm" }}',
|
||||
isEdit: false,
|
||||
confirm: null,
|
||||
dnsServer: {
|
||||
address: "localhost",
|
||||
domains: [],
|
||||
queryStrategy: 'UseIP',
|
||||
},
|
||||
ok() {
|
||||
domains = dnsModal.dnsServer.domains.filter(d => d.length>0);
|
||||
dnsModal.dnsServer.domains = domains;
|
||||
newDnsServer = domains.length > 0 ? dnsModal.dnsServer : dnsModal.dnsServer.address;
|
||||
ObjectUtil.execute(dnsModal.confirm, newDnsServer);
|
||||
},
|
||||
show({ title='', okText='{{ i18n "confirm" }}', dnsServer, confirm=(dnsServer)=>{}, isEdit=false }) {
|
||||
this.title = title;
|
||||
this.okText = okText;
|
||||
this.confirm = confirm;
|
||||
this.visible = true;
|
||||
if(isEdit) {
|
||||
if (typeof dnsServer == 'object'){
|
||||
this.dnsServer = dnsServer;
|
||||
} else {
|
||||
this.dnsServer.address = dnsServer?? '';
|
||||
}
|
||||
} else {
|
||||
this.dnsServer = {
|
||||
address: "localhost",
|
||||
domains: [],
|
||||
queryStrategy: 'UseIP',
|
||||
}
|
||||
}
|
||||
this.isEdit = isEdit;
|
||||
},
|
||||
close() {
|
||||
dnsModal.visible = false;
|
||||
},
|
||||
};
|
||||
const dnsModal = {
|
||||
title: '',
|
||||
visible: false,
|
||||
okText: '{{ i18n "confirm" }}',
|
||||
isEdit: false,
|
||||
confirm: null,
|
||||
dnsServer: {
|
||||
address: "localhost",
|
||||
domains: [],
|
||||
expectIPs: [],
|
||||
queryStrategy: 'UseIP',
|
||||
skipFallback: true,
|
||||
},
|
||||
ok() {
|
||||
domains = dnsModal.dnsServer.domains.filter(d => d.length > 0);
|
||||
expectIPs = dnsModal.dnsServer.expectIPs.filter(ip => ip.length > 0);
|
||||
dnsModal.dnsServer.domains = domains;
|
||||
dnsModal.dnsServer.expectIPs = expectIPs;
|
||||
newDnsServer = (domains.length > 0 || expectIPs.length > 0) ? dnsModal.dnsServer : dnsModal.dnsServer.address;
|
||||
ObjectUtil.execute(dnsModal.confirm, newDnsServer);
|
||||
},
|
||||
|
||||
new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#dns-modal',
|
||||
data: {
|
||||
dnsModal: dnsModal,
|
||||
},
|
||||
computed: {
|
||||
isAdvanced: {
|
||||
get: function () { return dnsModal.dnsServer.domains.length>0 }
|
||||
show({
|
||||
title = '',
|
||||
okText = '{{ i18n "confirm" }}',
|
||||
dnsServer,
|
||||
confirm = (dnsServer) => { },
|
||||
isEdit = false
|
||||
}) {
|
||||
this.title = title;
|
||||
this.okText = okText;
|
||||
this.confirm = confirm;
|
||||
this.visible = true;
|
||||
if (isEdit) {
|
||||
if (typeof dnsServer == 'object') {
|
||||
this.dnsServer = dnsServer;
|
||||
} else {
|
||||
this.dnsServer = {
|
||||
address: dnsServer ?? "",
|
||||
domains: [],
|
||||
expectIPs: [],
|
||||
queryStrategy: 'UseIP',
|
||||
skipFallback: true,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
this.dnsServer = {
|
||||
address: "localhost",
|
||||
domains: [],
|
||||
expectIPs: [],
|
||||
queryStrategy: 'UseIP',
|
||||
skipFallback: true,
|
||||
}
|
||||
}
|
||||
this.isEdit = isEdit;
|
||||
},
|
||||
close() {
|
||||
dnsModal.visible = false;
|
||||
},
|
||||
};
|
||||
new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#dns-modal',
|
||||
data: {
|
||||
dnsModal: dnsModal,
|
||||
},
|
||||
computed: {
|
||||
isAdvanced: {
|
||||
get: function () {
|
||||
return dnsModal.dnsServer.domains.length > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
{{end}}
|
||||
15
web/html/xui/form/allocate.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{{define "form/allocate"}}
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='Strategy'>
|
||||
<a-select v-model="inbound.allocate.strategy" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['always','random']" :value="s">[[ s ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Refresh'>
|
||||
<a-input-number v-model.number="inbound.allocate.refresh" min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Concurrency'>
|
||||
<a-input-number v-model.number="inbound.allocate.concurrency" min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
@@ -39,6 +39,11 @@
|
||||
</template>
|
||||
<a-input v-model.trim="client.id"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="inbound.protocol === Protocols.VMESS" label='{{ i18n "security" }}'>
|
||||
<a-select v-model="client.security" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="client.email && app.subSettings.enable">
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
@@ -57,7 +62,7 @@
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
|
||||
</template>
|
||||
Telegram ID
|
||||
Telegram ChatID
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
@@ -75,7 +80,7 @@
|
||||
</template>
|
||||
<a-input-number v-model="client.limitIp" min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="client.limitIp > 0 && client.email && isEdit">
|
||||
<a-form-item v-if="app.ipLimitEnable && client.limitIp > 0 && client.email && isEdit">
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
@@ -99,7 +104,7 @@
|
||||
</a-textarea>
|
||||
</a-form>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="inbound.xtls" label='Flow'>
|
||||
<a-form-item v-if="inbound.stream.isXtls" label='Flow'>
|
||||
<a-select v-model="client.flow" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<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>
|
||||
|
||||
@@ -54,12 +54,13 @@
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-date-picker style="width: 100%;" v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
v-model="dbInbound._expiryTime"></a-date-picker>
|
||||
<persian-datepicker v-else placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
|
||||
value="dbInbound._expiryTime" v-model="dbInbound._expiryTime"></persian-datepicker>
|
||||
</a-form-item>
|
||||
<a-date-picker style="width: 100%;" v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }"
|
||||
format="YYYY-MM-DD HH:mm:ss" :dropdown-class-name="themeSwitcher.currentTheme"
|
||||
v-model="dbInbound._expiryTime"></a-date-picker>
|
||||
<persian-datepicker v-else placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
|
||||
value="dbInbound._expiryTime" v-model="dbInbound._expiryTime">
|
||||
</persian-datepicker>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- vmess settings -->
|
||||
@@ -114,7 +115,19 @@
|
||||
</template>
|
||||
|
||||
<!-- sniffing -->
|
||||
<template>
|
||||
{{template "form/sniffing"}}
|
||||
</template>
|
||||
<a-collapse>
|
||||
<a-collapse-panel header='Sniffing'>
|
||||
{{template "form/sniffing"}}
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
|
||||
<!-- allocate -->
|
||||
<!-- Temporarily disabled until we accepts range for port allocation
|
||||
<a-collapse>
|
||||
<a-collapse-panel header='Allocate'>
|
||||
{{template "form/allocate"}}
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
-->
|
||||
|
||||
{{end}}
|
||||
|
||||
@@ -1,461 +1,491 @@
|
||||
{{define "form/outbound"}}
|
||||
<!-- base -->
|
||||
<a-tabs :active-key="outModal.activeKey" style="padding: 0; background-color: transparent;" @change="(activeKey) => {outModal.toggleJson(activeKey == '2'); }">
|
||||
<a-tab-pane key="1" tab="Form">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "protocol" }}'>
|
||||
<a-select v-model="outbound.protocol" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="x,y in Protocols" :value="x">[[ y ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.outbound.tag" }}' has-feedback :validate-status="outModal.duplicateTag? 'warning' : 'success'">
|
||||
<a-input v-model.trim="outbound.tag" @change="outModal.check()" placeholder='{{ i18n "pages.xray.outbound.tagDesc" }}'></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.outbound.sendThrough" }}'>
|
||||
<a-input v-model="outbound.sendThrough"></a-input>
|
||||
</a-form-item>
|
||||
<a-tabs :active-key="outModal.activeKey" style="padding: 0; background-color: transparent;" @change="(activeKey) => {outModal.toggleJson(activeKey == '2'); }">
|
||||
<a-tab-pane key="1" tab="Form">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "protocol" }}'>
|
||||
<a-select v-model="outbound.protocol" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="x,y in Protocols" :value="x">[[ y ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.outbound.tag" }}' has-feedback :validate-status="outModal.duplicateTag? 'warning' : 'success'">
|
||||
<a-input v-model.trim="outbound.tag" @change="outModal.check()" placeholder='{{ i18n "pages.xray.outbound.tagDesc" }}'></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.outbound.sendThrough" }}'>
|
||||
<a-input v-model="outbound.sendThrough"></a-input>
|
||||
</a-form-item>
|
||||
|
||||
<!-- freedom settings-->
|
||||
<template v-if="outbound.protocol === Protocols.Freedom">
|
||||
<!-- freedom settings-->
|
||||
<template v-if="outbound.protocol === Protocols.Freedom">
|
||||
<a-form-item label='Strategy'>
|
||||
<a-select
|
||||
v-model="outbound.settings.domainStrategy"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in OutboundDomainStrategies" :value="s">[[ s ]]</a-select-option>
|
||||
</a-select>
|
||||
<a-select v-model="outbound.settings.domainStrategy" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in OutboundDomainStrategies" :value="s">[[ s ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Redirect'>
|
||||
<a-input v-model="outbound.settings.redirect"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Fragment'>
|
||||
<a-switch
|
||||
:checked="Object.keys(outbound.settings.fragment).length >0"
|
||||
@change="checked => outbound.settings.fragment = checked ? new Outbound.FreedomSettings.Fragment() : {}">
|
||||
</a-switch>
|
||||
<a-switch :checked="Object.keys(outbound.settings.fragment).length >0"
|
||||
@change="checked => outbound.settings.fragment = checked ? new Outbound.FreedomSettings.Fragment() : {}">
|
||||
</a-switch>
|
||||
</a-form-item>
|
||||
<template v-if="Object.keys(outbound.settings.fragment).length >0">
|
||||
<a-form-item label='Packets'>
|
||||
<a-select
|
||||
v-model="outbound.settings.fragment.packets"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['1-3','tlshello']" :value="s">[[ s ]]</a-select-option>
|
||||
<template v-if="Object.keys(outbound.settings.fragment).length >0">
|
||||
<a-form-item label='Packets'>
|
||||
<a-select v-model="outbound.settings.fragment.packets" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['1-3','tlshello']" :value="s">[[ s ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Length'>
|
||||
</a-form-item>
|
||||
<a-form-item label='Length'>
|
||||
<a-input v-model.trim="outbound.settings.fragment.length"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Interval'>
|
||||
</a-form-item>
|
||||
<a-form-item label='Interval'>
|
||||
<a-input v-model.trim="outbound.settings.fragment.interval"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- blackhole settings -->
|
||||
<template v-if="outbound.protocol === Protocols.Blackhole">
|
||||
<a-form-item label='Response Type'>
|
||||
<a-select
|
||||
v-model="outbound.settings.type"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<!-- Switch for Noises -->
|
||||
<a-form-item label='Noises'>
|
||||
<a-switch :checked="outbound.settings.noises.length > 0"
|
||||
@change="checked => outbound.settings.noises = checked ? [new Outbound.FreedomSettings.Noise()] : []">
|
||||
</a-switch>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Add Noise Button -->
|
||||
<template v-if="outbound.settings.noises.length > 0">
|
||||
<a-form-item label="Noises">
|
||||
<a-button icon="plus" type="primary" size="small" @click="outbound.settings.addNoise()"></a-button>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Noise Configurations -->
|
||||
<a-form v-for="(noise, index) in outbound.settings.noises" :key="index" :colon="false" :label-col="{ md: {span:8} }"
|
||||
:wrapper-col="{ md: {span:14} }">
|
||||
<a-divider style="margin:0;"> Noise [[ index + 1 ]]
|
||||
<a-icon v-if="outbound.settings.noises.length > 1" type="delete" @click="() => outbound.settings.delNoise(index)"
|
||||
style="color: rgb(255, 77, 79); cursor: pointer;"></a-icon>
|
||||
</a-divider>
|
||||
<a-form-item label='Type'>
|
||||
<a-select v-model="noise.type" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['rand','base64','str']" :value="s">[[ s ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Packet'>
|
||||
<a-input v-model.trim="noise.packet"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Delay'>
|
||||
<a-input v-model.trim="noise.delay"></a-input>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- blackhole settings -->
|
||||
<template v-if="outbound.protocol === Protocols.Blackhole">
|
||||
<a-form-item label='Response Type'>
|
||||
<a-select v-model="outbound.settings.type" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['', 'none','http']" :value="s">[[ s ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- dns settings -->
|
||||
<template v-if="outbound.protocol === Protocols.DNS">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.network" }}'>
|
||||
<a-select
|
||||
v-model="outbound.settings.network"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<!-- dns settings -->
|
||||
<template v-if="outbound.protocol === Protocols.DNS">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.network" }}'>
|
||||
<a-select v-model="outbound.settings.network" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['udp','tcp']" :value="s">[[ s ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- wireguard settings -->
|
||||
<template v-if="outbound.protocol === Protocols.Wireguard">
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.xray.outbound.address" }} <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="outbound.settings.address"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "reset" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.xray.wireguard.secretKey" }}
|
||||
<a-icon type="sync"
|
||||
@click="[outbound.settings.pubKey, outbound.settings.secretKey] = Object.values(Wireguard.generateKeypair())">
|
||||
</a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="outbound.settings.secretKey"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.wireguard.publicKey" }}'>
|
||||
<a-input disabled v-model="outbound.settings.pubKey"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.wireguard.domainStrategy" }}'>
|
||||
<a-select v-model="outbound.settings.domainStrategy" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="wds in ['', ...WireguardDomainStrategy]" :value="wds">[[ wds ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='MTU'>
|
||||
<a-input-number v-model.number="outbound.settings.mtu"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Workers'>
|
||||
<a-input-number min="0" v-model.number="outbound.settings.workers"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Kernel Mode'>
|
||||
<a-switch v-model="outbound.settings.kernelMode"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
|
||||
</template>
|
||||
Reserved <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model="outbound.settings.reserved"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Peers">
|
||||
<a-button type="primary" size="small" @click="outbound.settings.addPeer()">+</a-button>
|
||||
</a-form-item>
|
||||
<a-form v-for="(peer, index) in outbound.settings.peers" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-divider style="margin:0;">
|
||||
Peer [[ index + 1 ]]
|
||||
<a-icon v-if="outbound.settings.peers.length>1" type="delete" @click="() => outbound.settings.delPeer(index)"
|
||||
style="color: rgb(255, 77, 79);cursor: pointer;"/>
|
||||
</a-divider>
|
||||
|
||||
<a-form-item label='{{ i18n "pages.xray.wireguard.endpoint" }}'>
|
||||
<a-input v-model.trim="peer.endpoint"></a-input>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.wireguard.publicKey" }}'>
|
||||
<a-input v-model.trim="peer.publicKey"></a-input>
|
||||
<a-form-item label='non-IP queries'>
|
||||
<a-select v-model="outbound.settings.nonIPQuery" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['drop','skip']" :value="s">[[ s ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.wireguard.psk" }}'>
|
||||
<a-input v-model.trim="peer.psk"></a-input>
|
||||
<a-form-item v-if="outbound.settings.nonIPQuery === 'skip'" label='Block Types' >
|
||||
<a-input v-model.number="outbound.settings.blockTypes"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- wireguard settings -->
|
||||
<template v-if="outbound.protocol === Protocols.Wireguard">
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.xray.outbound.address" }}
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="outbound.settings.address"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "reset" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.xray.wireguard.secretKey" }}
|
||||
<a-icon type="sync"
|
||||
@click="[outbound.settings.pubKey, outbound.settings.secretKey] = Object.values(Wireguard.generateKeypair())">
|
||||
</a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="outbound.settings.secretKey"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.wireguard.publicKey" }}'>
|
||||
<a-input disabled v-model="outbound.settings.pubKey"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.wireguard.domainStrategy" }}'>
|
||||
<a-select v-model="outbound.settings.domainStrategy" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="wds in ['', ...WireguardDomainStrategy]" :value="wds">[[ wds ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='MTU'>
|
||||
<a-input-number v-model.number="outbound.settings.mtu" min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Workers'>
|
||||
<a-input-number v-model.number="outbound.settings.workers" min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Kernel Mode'>
|
||||
<a-switch v-model="outbound.settings.kernelMode"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
|
||||
</template> Reserved <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model="outbound.settings.reserved"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Peers">
|
||||
<a-button icon="plus" type="primary" size="small" @click="outbound.settings.addPeer()"></a-button>
|
||||
</a-form-item>
|
||||
<a-form v-for="(peer, index) in outbound.settings.peers" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-divider style="margin:0;"> Peer [[ index + 1 ]] <a-icon v-if="outbound.settings.peers.length>1" type="delete" @click="() => outbound.settings.delPeer(index)" style="color: rgb(255, 77, 79);cursor: pointer;"></a-icon>
|
||||
</a-divider>
|
||||
<a-form-item label='{{ i18n "pages.xray.wireguard.endpoint" }}'>
|
||||
<a-input v-model.trim="peer.endpoint"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.wireguard.publicKey" }}'>
|
||||
<a-input v-model.trim="peer.publicKey"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.wireguard.psk" }}'>
|
||||
<a-input v-model.trim="peer.psk"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
{{ i18n "pages.xray.wireguard.allowedIPs" }} <a-button type="primary" size="small" @click="peer.allowedIPs.push('')">+</a-button>
|
||||
{{ i18n "pages.xray.wireguard.allowedIPs" }}
|
||||
<a-button icon="plus" type="primary" size="small" @click="peer.allowedIPs.push('')"></a-button>
|
||||
</template>
|
||||
<template v-for="(aip, index) in peer.allowedIPs" style="margin-bottom: 10px;">
|
||||
<a-input v-model.trim="peer.allowedIPs[index]">
|
||||
<a-button v-if="peer.allowedIPs.length>1" slot="addonAfter" size="small" @click="peer.allowedIPs.splice(index, 1)">-</a-button>
|
||||
</a-input>
|
||||
<a-input v-model.trim="peer.allowedIPs[index]">
|
||||
<a-button icon="minus" v-if="peer.allowedIPs.length>1" slot="addonAfter" size="small" @click="peer.allowedIPs.splice(index, 1)"></a-button>
|
||||
</a-input>
|
||||
</template>
|
||||
</a-form-item>
|
||||
<a-form-item label='Keep Alive'>
|
||||
<a-input-number v-model.number="peer.keepAlive" :min="0"></a-input>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
</a-form-item>
|
||||
<a-form-item label='Keep Alive'>
|
||||
<a-input-number v-model.number="peer.keepAlive" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<!-- Address + Port -->
|
||||
<template v-if="outbound.hasAddressPort()">
|
||||
<!-- Address + Port -->
|
||||
<template v-if="outbound.hasAddressPort()">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.address" }}'>
|
||||
<a-input v-model.trim="outbound.settings.address"></a-input>
|
||||
<a-input v-model.trim="outbound.settings.address"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.port" }}'>
|
||||
<a-input-number v-model.number="outbound.settings.port" :min="1" :max="65532"></a-input-number>
|
||||
<a-input-number v-model.number="outbound.settings.port" :min="1" :max="65532"></a-input-number>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- Vnext (vless/vmess) settings -->
|
||||
<template v-if="[Protocols.VMess, Protocols.VLESS].includes(outbound.protocol)">
|
||||
<!-- Vnext (vless/vmess) settings -->
|
||||
<template v-if="[Protocols.VMess, Protocols.VLESS].includes(outbound.protocol)">
|
||||
<a-form-item label='ID'>
|
||||
<a-input v-model.trim="outbound.settings.id"></a-input>
|
||||
<a-input v-model.trim="outbound.settings.id"></a-input>
|
||||
</a-form-item>
|
||||
<!-- vless settings -->
|
||||
<template v-if="outbound.canEnableTlsFlow()">
|
||||
<a-form-item label='Flow'>
|
||||
|
||||
<!-- vmess settings -->
|
||||
<template v-if="outbound.protocol === Protocols.VMess">
|
||||
<a-form-item label='Security'>
|
||||
<a-select v-model="outbound.settings.security" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- vless settings -->
|
||||
<template v-if="outbound.canEnableTlsFlow()">
|
||||
<a-form-item label='Flow'>
|
||||
<a-select v-model="outbound.settings.flow" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<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 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>
|
||||
</template>
|
||||
</template>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- Servers (trojan/shadowsocks/socks/http) settings -->
|
||||
<template v-if="outbound.hasServers()">
|
||||
<!-- http / socks -->
|
||||
<template v-if="outbound.hasUsername()">
|
||||
<a-form-item label='{{ i18n "username" }}'>
|
||||
<!-- Servers (trojan/shadowsocks/socks/http) settings -->
|
||||
<template v-if="outbound.hasServers()">
|
||||
<!-- http / socks -->
|
||||
<template v-if="outbound.hasUsername()">
|
||||
<a-form-item label='{{ i18n "username" }}'>
|
||||
<a-input v-model.trim="outbound.settings.user"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-input v-model.trim="outbound.settings.pass"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<!-- trojan/shadowsocks -->
|
||||
<template v-if="[Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)">
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-input v-model.trim="outbound.settings.password"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<!-- shadowsocks -->
|
||||
<template v-if="outbound.protocol === Protocols.Shadowsocks">
|
||||
<a-form-item label='{{ i18n "encryption" }}'>
|
||||
<a-select v-model="outbound.settings.method" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="(method, method_name) in SSMethods" :value="method">[[ method_name ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='UDP over TCP'>
|
||||
<a-switch v-model="outbound.settings.uot"></a-switch>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- stream settings -->
|
||||
<template v-if="outbound.canEnableStream()">
|
||||
<a-form-item label='{{ i18n "transmission" }}'>
|
||||
<a-select v-model="outbound.stream.network" @change="streamNetworkChange"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<!-- trojan/shadowsocks -->
|
||||
<template v-if="[Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)">
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-input v-model.trim="outbound.settings.password"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- shadowsocks -->
|
||||
<template v-if="outbound.protocol === Protocols.Shadowsocks">
|
||||
<a-form-item label='{{ i18n "encryption" }}'>
|
||||
<a-select v-model="outbound.settings.method" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="(method, method_name) in SSMethods" :value="method">[[ method_name ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='UDP over TCP'>
|
||||
<a-switch v-model="outbound.settings.uot"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='UoTVersion'>
|
||||
<a-input-number v-model.number="outbound.settings.UoTVersion" :min="1" :max="2"></a-input-number>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- stream settings -->
|
||||
<template v-if="outbound.canEnableStream()">
|
||||
<a-form-item label='{{ i18n "transmission" }}'>
|
||||
<a-select v-model="outbound.stream.network" @change="streamNetworkChange" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="tcp">TCP</a-select-option>
|
||||
<a-select-option value="kcp">mKCP</a-select-option>
|
||||
<a-select-option value="ws">WebSocket</a-select-option>
|
||||
<a-select-option value="http">H2</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="httpupgrade">HTTPUpgrade</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<template v-if="outbound.stream.network === 'tcp'">
|
||||
<a-form-item label='HTTP {{ i18n "camouflage" }}'>
|
||||
<a-switch :checked="outbound.stream.tcp.type === 'http'"
|
||||
@change="checked => outbound.stream.tcp.type = checked ? 'http' : 'none'">
|
||||
</a-switch>
|
||||
<a-select-option value="splithttp">SplitHTTP</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<template v-if="outbound.stream.tcp.type == 'http'">
|
||||
<template v-if="outbound.stream.network === 'tcp'">
|
||||
<a-form-item label='HTTP {{ i18n "camouflage" }}'>
|
||||
<a-switch :checked="outbound.stream.tcp.type === 'http'" @change="checked => outbound.stream.tcp.type = checked ? 'http' : 'none'"></a-switch>
|
||||
</a-form-item>
|
||||
<template v-if="outbound.stream.tcp.type == 'http'">
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
<a-input v-model.trim="outbound.stream.tcp.host"></a-input>
|
||||
<a-input v-model.trim="outbound.stream.tcp.host"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="outbound.stream.tcp.path"></a-input>
|
||||
<a-input v-model.trim="outbound.stream.tcp.path"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- kcp -->
|
||||
<template v-if="outbound.stream.network === 'kcp'">
|
||||
<a-form-item label='{{ i18n "camouflage" }}'>
|
||||
<!-- kcp -->
|
||||
<template v-if="outbound.stream.network === 'kcp'">
|
||||
<a-form-item label='{{ i18n "camouflage" }}'>
|
||||
<a-select v-model="outbound.stream.kcp.type" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="none">None</a-select-option>
|
||||
<a-select-option value="srtp">SRTP</a-select-option>
|
||||
<a-select-option value="utp">uTP</a-select-option>
|
||||
<a-select-option value="wechat-video">WeChat</a-select-option>
|
||||
<a-select-option value="dtls">DTLS 1.2</a-select-option>
|
||||
<a-select-option value="wireguard">WireGuard</a-select-option>
|
||||
<a-select-option value="dns">DNS</a-select-option>
|
||||
<a-select-option value="none">None</a-select-option>
|
||||
<a-select-option value="srtp">SRTP</a-select-option>
|
||||
<a-select-option value="utp">uTP</a-select-option>
|
||||
<a-select-option value="wechat-video">WeChat</a-select-option>
|
||||
<a-select-option value="dtls">DTLS 1.2</a-select-option>
|
||||
<a-select-option value="wireguard">WireGuard</a-select-option>
|
||||
<a-select-option value="dns">DNS</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-input v-model="outbound.stream.kcp.seed"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='MTU'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.mtu"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='TTI (ms)'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.tti"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Uplink (MB/s)'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.upCap"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Downlink (MB/s)'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.downCap"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Congestion'>
|
||||
</a-form-item>
|
||||
<a-form-item label='MTU'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.mtu" min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='TTI (ms)'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.tti" min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Uplink (MB/s)'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.upCap" min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Downlink (MB/s)'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.downCap" min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Congestion'>
|
||||
<a-switch v-model="outbound.stream.kcp.congestion"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='Read Buffer (MB)'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.readBuffer"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Write Buffer (MB)'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.writeBuffer"></a-input-number>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</a-form-item>
|
||||
<a-form-item label='Read Buffer (MB)'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.readBuffer" min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Write Buffer (MB)'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.writeBuffer" min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- ws -->
|
||||
<template v-if="outbound.stream.network === 'ws'">
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
<!-- ws -->
|
||||
<template v-if="outbound.stream.network === 'ws'">
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
<a-input v-model="outbound.stream.ws.host"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="outbound.stream.ws.path"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- http -->
|
||||
<template v-if="outbound.stream.network === 'http'">
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
<!-- http -->
|
||||
<template v-if="outbound.stream.network === 'http'">
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
<a-input v-model.trim="outbound.stream.http.host"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="outbound.stream.http.path"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- quic -->
|
||||
<template v-if="outbound.stream.network === 'quic'">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'>
|
||||
<a-select v-model="outbound.stream.quic.security" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<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="chacha20-poly1305">CHACHA20-POLY1305</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-input v-model.trim="outbound.stream.quic.key"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "camouflage" }}'>
|
||||
<a-select v-model="outbound.stream.quic.type" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="none">None</a-select-option>
|
||||
<a-select-option value="srtp">SRTP</a-select-option>
|
||||
<a-select-option value="utp">uTP</a-select-option>
|
||||
<a-select-option value="wechat-video">WeChat</a-select-option>
|
||||
<a-select-option value="dtls">DTLS 1.2</a-select-option>
|
||||
<a-select-option value="wireguard">WireGuard</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- grpc -->
|
||||
<template v-if="outbound.stream.network === 'grpc'">
|
||||
<a-form-item label='Service Name'>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- grpc -->
|
||||
<template v-if="outbound.stream.network === 'grpc'">
|
||||
<a-form-item label='Service Name'>
|
||||
<a-input v-model.trim="outbound.stream.grpc.serviceName"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Authority">
|
||||
</a-form-item>
|
||||
<a-form-item label="Authority">
|
||||
<a-input v-model.trim="outbound.stream.grpc.authority"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Multi Mode'>
|
||||
</a-form-item>
|
||||
<a-form-item label='Multi Mode'>
|
||||
<a-switch v-model="outbound.stream.grpc.multiMode"></a-switch>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- httpupgrade -->
|
||||
<template v-if="outbound.stream.network === 'httpupgrade'">
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
<!-- httpupgrade -->
|
||||
<template v-if="outbound.stream.network === 'httpupgrade'">
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
<a-input v-model="outbound.stream.httpupgrade.host"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="outbound.stream.httpupgrade.path"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- tls settings -->
|
||||
<template v-if="outbound.canEnableTls()">
|
||||
<a-form-item label='{{ i18n "security" }}'>
|
||||
<a-radio-group v-model="outbound.stream.security" button-style="solid">
|
||||
<!-- splithttp -->
|
||||
<template v-if="outbound.stream.network === 'splithttp'">
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
<a-input v-model="outbound.stream.splithttp.host"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="outbound.stream.splithttp.path"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- tls settings -->
|
||||
<template v-if="outbound.canEnableTls()">
|
||||
<a-form-item label='{{ i18n "security" }}'>
|
||||
<a-radio-group v-model="outbound.stream.security" button-style="solid">
|
||||
<a-radio-button value="none">{{ i18n "none" }}</a-radio-button>
|
||||
<a-radio-button value="tls">TLS</a-radio-button>
|
||||
<a-radio-button v-if="outbound.canEnableReality()" value="reality">Reality</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<template v-if="outbound.stream.isTls">
|
||||
<a-form-item label="SNI" placeholder="Server Name Indication">
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<template v-if="outbound.stream.isTls">
|
||||
<a-form-item label="SNI" placeholder="Server Name Indication">
|
||||
<a-input v-model.trim="outbound.stream.tls.serverName"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="uTLS">
|
||||
<a-select v-model="outbound.stream.tls.fingerprint"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value=''>None</a-select-option>
|
||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||
</a-form-item>
|
||||
<a-form-item label="uTLS">
|
||||
<a-select v-model="outbound.stream.tls.fingerprint" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<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-select mode="multiple"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
v-model="outbound.stream.tls.alpn">
|
||||
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
|
||||
</a-form-item>
|
||||
<a-form-item label="ALPN">
|
||||
<a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme" v-model="outbound.stream.tls.alpn">
|
||||
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Allow Insecure">
|
||||
</a-form-item>
|
||||
<a-form-item label="Allow Insecure">
|
||||
<a-switch v-model="outbound.stream.tls.allowInsecure"></a-switch>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- reality settings -->
|
||||
<template v-if="outbound.stream.isReality">
|
||||
<a-form-item label="SNI">
|
||||
<!-- reality settings -->
|
||||
<template v-if="outbound.stream.isReality">
|
||||
<a-form-item label="SNI">
|
||||
<a-input v-model.trim="outbound.stream.reality.serverName"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="uTLS">
|
||||
<a-select v-model="outbound.stream.reality.fingerprint"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||
</a-form-item>
|
||||
<a-form-item label="uTLS">
|
||||
<a-select v-model="outbound.stream.reality.fingerprint" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Short ID">
|
||||
</a-form-item>
|
||||
<a-form-item label="Short ID">
|
||||
<a-input v-model.trim="outbound.stream.reality.shortId" style="width:250px"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="SpiderX">
|
||||
</a-form-item>
|
||||
<a-form-item label="SpiderX">
|
||||
<a-input v-model.trim="outbound.stream.reality.spiderX" style="width:250px"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Public Key">
|
||||
</a-form-item>
|
||||
<a-form-item label="Public Key">
|
||||
<a-input v-model.trim="outbound.stream.reality.publicKey"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- sockopt settings -->
|
||||
<a-form-item label="Sockopts">
|
||||
<!-- sockopt settings -->
|
||||
<a-form-item label="Sockopts">
|
||||
<a-switch v-model="outbound.stream.sockoptSwitch"></a-switch>
|
||||
</a-form-item>
|
||||
<template v-if="outbound.stream.sockoptSwitch">
|
||||
</a-form-item>
|
||||
<template v-if="outbound.stream.sockoptSwitch">
|
||||
<a-form-item label="Dialer Proxy">
|
||||
<a-select v-model="outbound.stream.sockopt.dialerProxy" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="tag in ['', ...outModal.tags]" :value="tag">[[ tag ]]</a-select-option>
|
||||
</a-select>
|
||||
<a-select v-model="outbound.stream.sockopt.dialerProxy" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="tag in ['', ...outModal.tags]" :value="tag">[[ tag ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="TCP Fast Open">
|
||||
<a-switch v-model="outbound.stream.sockopt.tcpFastOpen"></a-switch>
|
||||
<a-switch v-model="outbound.stream.sockopt.tcpFastOpen"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="Keep Alive Interval">
|
||||
<a-input-number v-model="outbound.stream.sockopt.tcpKeepAliveInterval" :min="0"></a-input-number>
|
||||
<a-input-number v-model="outbound.stream.sockopt.tcpKeepAliveInterval" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="TCP No-Delay">
|
||||
<a-switch v-model="outbound.stream.sockopt.tcpNoDelay"></a-switch>
|
||||
<a-form-item label="Multipath TCP">
|
||||
<a-switch v-model.trim="outbound.stream.sockopt.tcpMptcp"></a-switch>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item label="TCP No-Delay" v-if="outbound.stream.sockopt.tcpMptcp">
|
||||
<a-switch v-model="outbound.stream.sockopt.tcpNoDelay"></a-switch>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- mux settings -->
|
||||
<template v-if="outbound.canEnableMux()">
|
||||
<a-form-item label="Mux">
|
||||
<a-switch v-model="outbound.mux.enabled"></a-switch>
|
||||
</a-form-item>
|
||||
<template v-if="outbound.mux.enabled">
|
||||
<a-form-item label="Concurrency">
|
||||
<!-- mux settings -->
|
||||
<template v-if="outbound.canEnableMux()">
|
||||
<a-form-item label="Mux">
|
||||
<a-switch v-model="outbound.mux.enabled"></a-switch>
|
||||
</a-form-item>
|
||||
<template v-if="outbound.mux.enabled">
|
||||
<a-form-item label="Concurrency">
|
||||
<a-input-number v-model="outbound.mux.concurrency" :min="-1" :max="1024"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="xudp Concurrency">
|
||||
</a-form-item>
|
||||
<a-form-item label="xudp Concurrency">
|
||||
<a-input-number v-model="outbound.mux.xudpConcurrency" :min="-1" :max="1024"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="xudp UDP 443">
|
||||
</a-form-item>
|
||||
<a-form-item label="xudp UDP 443">
|
||||
<a-select v-model="outbound.mux.xudpProxyUDP443" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="c in ['reject', 'allow', 'skip']" :value="c">[[ c ]]</a-select-option>
|
||||
<a-select-option v-for="c in ['reject', 'allow', 'skip']" :value="c">[[ c ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" tab="JSON" force-render="true">
|
||||
<a-form-item style="margin: 10px 0">
|
||||
Link: <a-input v-model.trim="outModal.link" style="width: 300px; margin-right: 5px;" placeholder="vmess:// vless:// trojan:// ss://"></a-input>
|
||||
<a-button @click="convertLink" type="primary"><a-icon type="form"></a-icon></a-button>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" tab="JSON" force-render="true">
|
||||
<a-form-item style="margin: 10px 0"> Link: <a-input v-model.trim="outModal.link" style="width: 300px; margin-right: 5px;" placeholder="vmess:// vless:// trojan:// ss://"></a-input>
|
||||
<a-button @click="convertLink" type="primary">
|
||||
<a-icon type="form"></a-icon>
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
<textarea style="position:absolute; left: -800px;" id="outboundJson"></textarea>
|
||||
</a-tab-pane>
|
||||
<textarea style="position:absolute; left: -800px;" id="outboundJson"></textarea>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
{{end}}
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
{{define "form/http"}}
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<table style="width: 100%; text-align: center; margin: 1rem 0;">
|
||||
<tr>
|
||||
<td width="45%">{{ i18n "username" }}</td>
|
||||
<td width="45%">{{ i18n "password" }}</td>
|
||||
<td><a-button size="small" @click="inbound.settings.addAccount(new Inbound.HttpSettings.HttpAccount())">+</a-button></td>
|
||||
</tr>
|
||||
</table>
|
||||
<a-input-group compact v-for="(account, index) in inbound.settings.accounts" style="margin-bottom: 10px;">
|
||||
<a-input style="width: 50%" v-model.trim="account.user" placeholder='{{ i18n "username" }}'>
|
||||
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
|
||||
</a-input>
|
||||
<a-input style="width: 50%" v-model.trim="account.pass" placeholder='{{ i18n "password" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button size="small" @click="inbound.settings.delAccount(index)">-</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
<table style="width: 100%; text-align: center; margin: 1rem 0;">
|
||||
<tr>
|
||||
<td width="45%">{{ i18n "username" }}</td>
|
||||
<td width="45%">{{ i18n "password" }}</td>
|
||||
<td>
|
||||
<a-button icon="plus" size="small" @click="inbound.settings.addAccount(new Inbound.HttpSettings.HttpAccount())"></a-button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<a-input-group compact v-for="(account, index) in inbound.settings.accounts" style="margin-bottom: 10px;">
|
||||
<a-input style="width: 50%" v-model.trim="account.user" placeholder='{{ i18n "username" }}'>
|
||||
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
|
||||
</a-input>
|
||||
<a-input style="width: 50%" v-model.trim="account.pass" placeholder='{{ i18n "password" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button icon="minus" size="small" @click="inbound.settings.delAccount(index)"></a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
<a-form-item label="Allow Transparent">
|
||||
<a-switch v-model="inbound.settings.allowTransparent" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
|
||||
@@ -1,33 +1,34 @@
|
||||
{{define "form/socks"}}
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.enable" }} UDP'>
|
||||
<a-switch v-model="inbound.settings.udp"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="IP" v-if="inbound.settings.udp">
|
||||
<a-input v-model.trim="inbound.settings.ip"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-switch :checked="inbound.settings.auth === 'password'"
|
||||
@change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
|
||||
</a-form-item>
|
||||
<template v-if="inbound.settings.auth === 'password'">
|
||||
<table style="width: 100%; text-align: center; margin: 1rem 0;">
|
||||
<tr>
|
||||
<td width="45%">{{ i18n "username" }}</td>
|
||||
<td width="45%">{{ i18n "password" }}</td>
|
||||
<td><a-button size="small" @click="inbound.settings.addAccount(new Inbound.SocksSettings.SocksAccount())">+</a-button></td>
|
||||
</tr>
|
||||
</table>
|
||||
<a-input-group compact v-for="(account, index) in inbound.settings.accounts" style="margin-bottom: 10px;">
|
||||
<a-input style="width: 50%" v-model.trim="account.user" placeholder='{{ i18n "username" }}'>
|
||||
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
|
||||
</a-input>
|
||||
<a-input style="width: 50%" v-model.trim="account.pass" placeholder='{{ i18n "password" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button size="small" @click="inbound.settings.delAccount(index)">-</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</template>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.enable" }} UDP'>
|
||||
<a-switch v-model="inbound.settings.udp"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="IP" v-if="inbound.settings.udp">
|
||||
<a-input v-model.trim="inbound.settings.ip"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-switch :checked="inbound.settings.auth === 'password'" @change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
|
||||
</a-form-item>
|
||||
<template v-if="inbound.settings.auth === 'password'">
|
||||
<table style="width: 100%; text-align: center; margin: 1rem 0;">
|
||||
<tr>
|
||||
<td width="45%">{{ i18n "username" }}</td>
|
||||
<td width="45%">{{ i18n "password" }}</td>
|
||||
<td>
|
||||
<a-button icon="plus" size="small" @click="inbound.settings.addAccount(new Inbound.SocksSettings.SocksAccount())"></a-button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<a-input-group compact v-for="(account, index) in inbound.settings.accounts" style="margin-bottom: 10px;">
|
||||
<a-input style="width: 50%" v-model.trim="account.user" placeholder='{{ i18n "username" }}'>
|
||||
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
|
||||
</a-input>
|
||||
<a-input style="width: 50%" v-model.trim="account.pass" placeholder='{{ i18n "password" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button icon="minus" size="small" @click="inbound.settings.delAccount(index)"></a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</template>
|
||||
</a-form>
|
||||
{{end}}
|
||||
|
||||
@@ -1,54 +1,50 @@
|
||||
{{define "form/trojan"}}
|
||||
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.trojans.slice(0,1)" v-if="!isEdit">
|
||||
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
|
||||
{{template "form/client"}}
|
||||
</a-collapse-panel>
|
||||
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.trojans.slice(0,1)" v-if="!isEdit">
|
||||
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
|
||||
{{template "form/client"}}
|
||||
</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-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>
|
||||
<template v-if="inbound.isTcp && !inbound.stream.isReality">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Fallbacks">
|
||||
<a-button type="primary" size="small" @click="inbound.settings.addFallback()">+</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Fallbacks">
|
||||
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- trojan fallbacks -->
|
||||
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }"
|
||||
:wrapper-col="{ md: {span:14} }">
|
||||
<a-divider style="margin:0;">
|
||||
Fallback [[ index + 1 ]]
|
||||
<a-icon type="delete" @click="() => inbound.settings.delFallback(index)"
|
||||
style="color: rgb(255, 77, 79);cursor: pointer;" />
|
||||
</a-divider>
|
||||
<a-form-item label='SNI'>
|
||||
<a-input v-model="fallback.name"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='ALPN'>
|
||||
<a-input v-model="fallback.alpn"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Path'>
|
||||
<a-input v-model="fallback.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Dest'>
|
||||
<a-input v-model="fallback.dest"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='xVer'>
|
||||
<a-input-number v-model="fallback.xver" :min="0" :max="2"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-divider style="margin:5px 0;"></a-divider>
|
||||
<!-- trojan fallbacks -->
|
||||
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-divider style="margin:0;"> Fallback [[ index + 1 ]] <a-icon type="delete" @click="() => inbound.settings.delFallback(index)" style="color: rgb(255, 77, 79);cursor: pointer;"></a-icon>
|
||||
</a-divider>
|
||||
<a-form-item label='SNI'>
|
||||
<a-input v-model="fallback.name"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='ALPN'>
|
||||
<a-input v-model="fallback.alpn"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Path'>
|
||||
<a-input v-model="fallback.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Dest'>
|
||||
<a-input v-model="fallback.dest"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='xVer'>
|
||||
<a-input-number v-model="fallback.xver" :min="0" :max="2"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-divider style="margin:5px 0;"></a-divider>
|
||||
</template>
|
||||
{{end}}
|
||||
|
||||
@@ -1,56 +1,50 @@
|
||||
{{define "form/vless"}}
|
||||
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
|
||||
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
|
||||
{{template "form/client"}}
|
||||
</a-collapse-panel>
|
||||
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
|
||||
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
|
||||
{{template "form/client"}}
|
||||
</a-collapse-panel>
|
||||
</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-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>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.id ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<template v-if="inbound.isTcp && !inbound.stream.isReality">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Fallbacks">
|
||||
<a-button type="primary" size="small" @click="inbound.settings.addFallback()">+</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Fallbacks">
|
||||
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- vless fallbacks -->
|
||||
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }"
|
||||
:wrapper-col="{ md: {span:14} }">
|
||||
<a-divider style="margin:0;">
|
||||
Fallback [[ index + 1 ]]
|
||||
<a-icon type="delete" @click="() => inbound.settings.delFallback(index)"
|
||||
style="color: rgb(255, 77, 79);cursor: pointer;" />
|
||||
</a-divider>
|
||||
<a-form-item label='SNI'>
|
||||
<a-input v-model="fallback.name"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='ALPN'>
|
||||
<a-input v-model="fallback.alpn"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Path'>
|
||||
<a-input v-model="fallback.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Dest'>
|
||||
<a-input v-model="fallback.dest"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='xVer'>
|
||||
<a-input-number v-model="fallback.xver" :min="0" :max="2"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-divider style="margin:5px 0;"></a-divider>
|
||||
<!-- vless fallbacks -->
|
||||
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-divider style="margin:0;"> Fallback [[ index + 1 ]] <a-icon type="delete" @click="() => inbound.settings.delFallback(index)" style="color: rgb(255, 77, 79);cursor: pointer;"></a-icon>
|
||||
</a-divider>
|
||||
<a-form-item label='SNI'>
|
||||
<a-input v-model="fallback.name"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='ALPN'>
|
||||
<a-input v-model="fallback.alpn"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Path'>
|
||||
<a-input v-model="fallback.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Dest'>
|
||||
<a-input v-model="fallback.dest"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='xVer'>
|
||||
<a-input-number v-model="fallback.xver" :min="0" :max="2"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-divider style="margin:5px 0;"></a-divider>
|
||||
</template>
|
||||
{{end}}
|
||||
|
||||
@@ -10,10 +10,12 @@
|
||||
<tr class="client-table-header">
|
||||
<th>{{ i18n "pages.inbounds.email" }}</th>
|
||||
<th>ID</th>
|
||||
<th>{{ i18n "security" }}</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>
|
||||
<td>[[ client.security ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-collapse-panel>
|
||||
|
||||
@@ -1,80 +1,76 @@
|
||||
{{define "form/wireguard"}}
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "reset" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.xray.wireguard.secretKey" }}
|
||||
<a-icon type="sync"
|
||||
@click="[inbound.settings.pubKey, inbound.settings.secretKey] = Object.values(Wireguard.generateKeypair())">
|
||||
</a-icon>
|
||||
</a-tooltip>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "reset" }}</span>
|
||||
</template>
|
||||
<a-input v-model.trim="inbound.settings.secretKey"></a-input>
|
||||
{{ i18n "pages.xray.wireguard.secretKey" }}
|
||||
<a-icon type="sync" @click="[inbound.settings.pubKey, inbound.settings.secretKey] = Object.values(Wireguard.generateKeypair())"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="inbound.settings.secretKey"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.wireguard.publicKey" }}'>
|
||||
<a-input disabled v-model="inbound.settings.pubKey"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='MTU'>
|
||||
<a-input-number v-model.number="inbound.settings.mtu"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Kernel Mode'>
|
||||
<a-switch v-model="inbound.settings.kernelMode"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="Peers">
|
||||
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addPeer()"></a-button>
|
||||
</a-form-item>
|
||||
<a-form v-for="(peer, index) in inbound.settings.peers" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-divider style="margin:0;"> Peer [[ index + 1 ]] <a-icon v-if="inbound.settings.peers.length>1" type="delete" @click="() => inbound.settings.delPeer(index)" style="color: rgb(255, 77, 79);cursor: pointer;"></a-icon>
|
||||
</a-divider>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "reset" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.xray.wireguard.secretKey" }}
|
||||
<a-icon @click="[peer.publicKey, peer.privateKey] = Object.values(Wireguard.generateKeypair())" type="sync"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="peer.privateKey"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.wireguard.publicKey" }}'>
|
||||
<a-input disabled v-model="inbound.settings.pubKey"></a-input>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
{{ i18n "pages.xray.wireguard.publicKey" }}
|
||||
</template>
|
||||
<a-input v-model.trim="peer.publicKey"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='MTU'>
|
||||
<a-input-number v-model.number="inbound.settings.mtu"></a-input-number>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "reset" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.xray.wireguard.psk" }}
|
||||
<a-icon @click="peer.psk = Wireguard.keyToBase64(Wireguard.generatePresharedKey())" type="sync"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="peer.psk"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Kernel Mode'>
|
||||
<a-switch v-model="inbound.settings.kernelMode"></a-switch>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
{{ i18n "pages.xray.wireguard.allowedIPs" }}
|
||||
<a-button icon="plus" type="primary" size="small" @click="peer.allowedIPs.push('')"></a-button>
|
||||
</template>
|
||||
<template v-for="(aip, index) in peer.allowedIPs" style="margin-bottom: 10px;">
|
||||
<a-input v-model.trim="peer.allowedIPs[index]">
|
||||
<a-button icon="minus" v-if="peer.allowedIPs.length>1" slot="addonAfter" size="small" @click="peer.allowedIPs.splice(index, 1)"></a-button>
|
||||
</a-input>
|
||||
</template>
|
||||
</a-form-item>
|
||||
<a-form-item label="Peers">
|
||||
<a-button type="primary" size="small" @click="inbound.settings.addPeer()">+</a-button>
|
||||
<a-form-item label='Keep Alive'>
|
||||
<a-input-number v-model.number="peer.keepAlive" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form v-for="(peer, index) in inbound.settings.peers" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-divider style="margin:0;">
|
||||
Peer [[ index + 1 ]]
|
||||
<a-icon v-if="inbound.settings.peers.length>1" type="delete" @click="() => inbound.settings.delPeer(index)"
|
||||
style="color: rgb(255, 77, 79);cursor: pointer;"/>
|
||||
</a-divider>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "reset" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.xray.wireguard.secretKey" }}
|
||||
<a-icon @click="[peer.publicKey, peer.privateKey] = Object.values(Wireguard.generateKeypair())"type="sync"> </a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="peer.privateKey"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
{{ i18n "pages.xray.wireguard.publicKey" }}
|
||||
</template>
|
||||
<a-input v-model.trim="peer.publicKey"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "reset" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.xray.wireguard.psk" }}
|
||||
<a-icon @click="peer.psk = Wireguard.keyToBase64(Wireguard.generatePresharedKey())"type="sync"> </a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="peer.psk"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
{{ i18n "pages.xray.wireguard.allowedIPs" }} <a-button type="primary" size="small" @click="peer.allowedIPs.push('')">+</a-button>
|
||||
</template>
|
||||
<template v-for="(aip, index) in peer.allowedIPs" style="margin-bottom: 10px;">
|
||||
<a-input v-model.trim="peer.allowedIPs[index]">
|
||||
<a-button v-if="peer.allowedIPs.length>1" slot="addonAfter" size="small" @click="peer.allowedIPs.splice(index, 1)">-</a-button>
|
||||
</a-input>
|
||||
</template>
|
||||
</a-form-item>
|
||||
<a-form-item label='Keep Alive'>
|
||||
<a-input-number v-model.number="peer.keepAlive" :min="0"></a-input>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-form>
|
||||
</a-form>
|
||||
{{end}}
|
||||
56
web/html/xui/form/reality_settings.html
Normal file
@@ -0,0 +1,56 @@
|
||||
{{define "form/realitySettings"}}
|
||||
<template>
|
||||
<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.number="inbound.stream.reality.xver" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='uTLS'>
|
||||
<a-select v-model="inbound.stream.reality.settings.fingerprint" style="width: 50%"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Dest'>
|
||||
<a-input v-model.trim="inbound.stream.reality.dest"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='SNI'>
|
||||
<a-input v-model.trim="inbound.stream.reality.serverNames"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Max Time Diff (ms)'>
|
||||
<a-input-number v-model.number="inbound.stream.reality.maxTimediff" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<!-- we also have this but i think it's not necessary
|
||||
<a-form-item label='Min Client'>
|
||||
<a-input v-model.trim="inbound.stream.reality.minClient"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Max Client'>
|
||||
<a-input v-model.trim="inbound.stream.reality.maxClient"></a-input>
|
||||
</a-form-item>
|
||||
-->
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "reset" }}</span>
|
||||
</template> Short IDs <a-icon @click="inbound.stream.reality.shortIds = RandomUtil.randomShortIds()"
|
||||
type="sync"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="inbound.stream.reality.shortIds"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='SpiderX'>
|
||||
<a-input v-model.trim="inbound.stream.reality.settings.spiderX"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
|
||||
<a-input v-model.trim="inbound.stream.reality.privateKey"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
|
||||
<a-input v-model.trim="inbound.stream.reality.settings.publicKey"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get New Cert</a-button>
|
||||
</a-form-item>
|
||||
</template>
|
||||
{{end}}
|
||||
@@ -1,9 +1,8 @@
|
||||
{{define "form/sniffing"}}
|
||||
<a-divider style="margin:5px 0 0;"></a-divider>
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
Sniffing
|
||||
{{ i18n "enabled" }}
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.noRecommendKeepDefault" }}</span>
|
||||
@@ -13,10 +12,18 @@
|
||||
</span>
|
||||
<a-switch v-model="inbound.sniffing.enabled"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{span:24}">
|
||||
<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>
|
||||
<template v-if="inbound.sniffing.enabled">
|
||||
<a-form-item :wrapper-col="{span:24}">
|
||||
<a-checkbox-group v-model="inbound.sniffing.destOverride">
|
||||
<a-checkbox v-for="key,value in SNIFFING_OPTION" :value="key">[[ value ]]</a-checkbox>
|
||||
</a-checkbox-group>
|
||||
</a-form-item>
|
||||
<a-form-item label='Metadata Only'>
|
||||
<a-switch v-model="inbound.sniffing.metadataOnly"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='Route Only'>
|
||||
<a-switch v-model="inbound.sniffing.routeOnly"></a-switch>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</a-form>
|
||||
{{end}}
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
{{define "form/externalProxy"}}
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-divider style="margin:5px 0 0;"></a-divider>
|
||||
<a-form-item label="External Proxy">
|
||||
<a-switch v-model="externalProxy"></a-switch>
|
||||
<a-button v-if="externalProxy" type="primary" style="margin-left: 10px" size="small" @click="inbound.stream.externalProxy.push({forceTls: 'same', dest: '', port: 443, remark: ''})">+</a-button>
|
||||
</a-form-item>
|
||||
<a-input-group style="margin: 8px 0;" compact v-for="(row, index) in inbound.stream.externalProxy">
|
||||
<template>
|
||||
<a-tooltip title="Force TLS">
|
||||
<a-select v-model="row.forceTls" style="width:20%; margin: 0px" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="same">{{ i18n "pages.inbounds.same" }}</a-select-option>
|
||||
<a-select-option value="none">{{ i18n "none" }}</a-select-option>
|
||||
<a-select-option value="tls">TLS</a-select-option>
|
||||
</a-select>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input style="width: 35%" v-model.trim="row.dest" placeholder='{{ i18n "host" }}'></a-input>
|
||||
<a-tooltip title='{{ i18n "pages.inbounds.port" }}'>
|
||||
<a-input-number style="width: 15%;" v-model.number="row.port" min="1" max="65531"></a-input-number>
|
||||
</a-tooltip>
|
||||
<a-input style="width: 20%" v-model.trim="row.remark" placeholder='{{ i18n "remark" }}'></a-input>
|
||||
<a-button style="width: 10%; margin: 0px; border-radius: 0 1rem 1rem 0;" @click="inbound.stream.externalProxy.splice(index, 1)">-</a-button>
|
||||
</a-input-group>
|
||||
<a-divider style="margin:5px 0 0;"></a-divider>
|
||||
<a-form-item label="External Proxy">
|
||||
<a-switch v-model="externalProxy"></a-switch>
|
||||
<a-button icon="plus" v-if="externalProxy" type="primary" style="margin-left: 10px" size="small" @click="inbound.stream.externalProxy.push({forceTls: 'same', dest: '', port: 443, remark: ''})"></a-button>
|
||||
</a-form-item>
|
||||
<a-input-group style="margin: 8px 0;" compact v-for="(row, index) in inbound.stream.externalProxy">
|
||||
<template>
|
||||
<a-tooltip title="Force TLS">
|
||||
<a-select v-model="row.forceTls" style="width:20%; margin: 0px" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="same">{{ i18n "pages.inbounds.same" }}</a-select-option>
|
||||
<a-select-option value="none">{{ i18n "none" }}</a-select-option>
|
||||
<a-select-option value="tls">TLS</a-select-option>
|
||||
</a-select>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input style="width: 35%" v-model.trim="row.dest" placeholder='{{ i18n "host" }}'></a-input>
|
||||
<a-tooltip title='{{ i18n "pages.inbounds.port" }}'>
|
||||
<a-input-number style="width: 15%;" v-model.number="row.port" min="1" max="65531"></a-input-number>
|
||||
</a-tooltip>
|
||||
<a-input style="width: 30%; top: 0;" v-model.trim="row.remark" placeholder='{{ i18n "remark" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button icon="minus" size="small" @click="inbound.stream.externalProxy.splice(index, 1)"></a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form>
|
||||
{{end}}
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
{{define "form/streamHTTP"}}
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="inbound.stream.http.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">{{ i18n "host" }}
|
||||
<a-button size="small" @click="inbound.stream.http.addHost()">+</a-button>
|
||||
</template>
|
||||
<template v-for="(host, index) in inbound.stream.http.host">
|
||||
<a-input v-model.trim="inbound.stream.http.host[index]">
|
||||
<a-button size="small" slot="addonAfter"
|
||||
@click="inbound.stream.http.removeHost(index)"
|
||||
v-if="inbound.stream.http.host.length>1">-</a-button>
|
||||
</a-input>
|
||||
</template>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="inbound.stream.http.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">{{ i18n "host" }}
|
||||
<a-button icon="plus" size="small" @click="inbound.stream.http.addHost()"></a-button>
|
||||
</template>
|
||||
<template v-for="(host, index) in inbound.stream.http.host">
|
||||
<a-input v-model.trim="inbound.stream.http.host[index]">
|
||||
<a-button icon="minus" size="small" slot="addonAfter" @click="inbound.stream.http.removeHost(index)" v-if="inbound.stream.http.host.length>1"></a-button>
|
||||
</a-input>
|
||||
</template>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
@@ -1,26 +1,26 @@
|
||||
{{define "form/streamHTTPUpgrade"}}
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="PROXY Protocol">
|
||||
<a-switch v-model="inbound.stream.httpupgrade.acceptProxyProtocol"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
<a-input v-model.trim="inbound.stream.httpupgrade.host"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="inbound.stream.httpupgrade.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
|
||||
<a-button size="small" @click="inbound.stream.httpupgrade.addHeader('host', '')">+</a-button>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{span:24}">
|
||||
<a-input-group compact v-for="(header, index) in inbound.stream.httpupgrade.headers">
|
||||
<a-input style="width: 50%" v-model.trim="header.name" placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'>
|
||||
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
|
||||
</a-input>
|
||||
<a-input style="width: 50%" v-model.trim="header.value" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<a-button slot="addonAfter" size="small" @click="inbound.stream.httpupgrade.removeHeader(index)">-</a-button>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="Proxy Protocol">
|
||||
<a-switch v-model="inbound.stream.httpupgrade.acceptProxyProtocol"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
<a-input v-model.trim="inbound.stream.httpupgrade.host"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="inbound.stream.httpupgrade.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
|
||||
<a-button icon="plus" size="small" @click="inbound.stream.httpupgrade.addHeader('host', '')"></a-button>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{span:24}">
|
||||
<a-input-group compact v-for="(header, index) in inbound.stream.httpupgrade.headers">
|
||||
<a-input style="width: 50%" v-model.trim="header.name" placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'>
|
||||
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
|
||||
</a-input>
|
||||
<a-input style="width: 50%" v-model.trim="header.value" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<a-button icon="minus" slot="addonAfter" size="small" @click="inbound.stream.httpupgrade.removeHeader(index)"></a-button>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
{{define "form/streamQUIC"}}
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'>
|
||||
<a-select v-model="inbound.stream.quic.security" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<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="chacha20-poly1305">CHACHA20-POLY1305</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "reset" }}</span>
|
||||
</template>
|
||||
{{ i18n "password" }}
|
||||
<a-icon @click="inbound.stream.quic.key = RandomUtil.randomSeq(10)"type="sync"> </a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="inbound.stream.quic.key"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "camouflage" }}'>
|
||||
<a-select v-model="inbound.stream.quic.type" style="width: 50%" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="none">None</a-select-option>
|
||||
<a-select-option value="srtp">SRTP</a-select-option>
|
||||
<a-select-option value="utp">uTP</a-select-option>
|
||||
<a-select-option value="wechat-video">WeChat</a-select-option>
|
||||
<a-select-option value="dtls">DTLS 1.2</a-select-option>
|
||||
<a-select-option value="wireguard">WireGuard</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
@@ -4,13 +4,13 @@
|
||||
<a-form-item label='{{ i18n "transmission" }}'>
|
||||
<a-select v-model="inbound.stream.network" style="width: 75%" @change="streamNetworkChange"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="tcp">TCP</a-select-option>
|
||||
<a-select-option value="tcp">TCP (RAW)</a-select-option>
|
||||
<a-select-option value="kcp">mKCP</a-select-option>
|
||||
<a-select-option value="ws">WebSocket</a-select-option>
|
||||
<a-select-option value="http">H2</a-select-option>
|
||||
<a-select-option value="quic">QUIC</a-select-option>
|
||||
<a-select-option value="http">HTTP</a-select-option>
|
||||
<a-select-option value="grpc">gRPC</a-select-option>
|
||||
<a-select-option value="httpupgrade">HTTPUpgrade</a-select-option>
|
||||
<a-select-option value="splithttp">SplitHTTP</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
@@ -35,11 +35,6 @@
|
||||
{{template "form/streamHTTP"}}
|
||||
</template>
|
||||
|
||||
<!-- quic -->
|
||||
<template v-if="inbound.stream.network === 'quic'">
|
||||
{{template "form/streamQUIC"}}
|
||||
</template>
|
||||
|
||||
<!-- grpc -->
|
||||
<template v-if="inbound.stream.network === 'grpc'">
|
||||
{{template "form/streamGRPC"}}
|
||||
@@ -50,6 +45,11 @@
|
||||
{{template "form/streamHTTPUpgrade"}}
|
||||
</template>
|
||||
|
||||
<!-- splithttp -->
|
||||
<template v-if="inbound.stream.network === 'splithttp'">
|
||||
{{template "form/streamSplitHTTP"}}
|
||||
</template>
|
||||
|
||||
<!-- sockopt -->
|
||||
<template>
|
||||
{{template "form/streamSockopt"}}
|
||||
|
||||
@@ -1,26 +1,66 @@
|
||||
{{define "form/streamSockopt"}}
|
||||
<a-divider style="margin:5px 0 0;"></a-divider>
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="TPROXY">
|
||||
<a-form-item label="Sockopt">
|
||||
<a-switch v-model="inbound.stream.sockoptSwitch"></a-switch>
|
||||
</a-form-item>
|
||||
<template v-if="inbound.stream.sockoptSwitch">
|
||||
<a-form-item label="PROXY Protocol">
|
||||
<a-form-item label="Route Mark">
|
||||
<a-input-number v-model="inbound.stream.sockopt.mark" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="TCP Keep Alive Interval">
|
||||
<a-input-number v-model="inbound.stream.sockopt.tcpKeepAliveInterval" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="TCP Keep Alive Idle">
|
||||
<a-input-number v-model="inbound.stream.sockopt.tcpKeepAliveIdle" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="TCP Max Seg">
|
||||
<a-input-number v-model="inbound.stream.sockopt.tcpMaxSeg" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="TCP User Timeout">
|
||||
<a-input-number v-model="inbound.stream.sockopt.tcpUserTimeout" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="TCP Window Clamp">
|
||||
<a-input-number v-model="inbound.stream.sockopt.tcpWindowClamp" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="Proxy Protocol">
|
||||
<a-switch v-model="inbound.stream.sockopt.acceptProxyProtocol"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="TCP Fast Open">
|
||||
<a-switch v-model.trim="inbound.stream.sockopt.tcpFastOpen"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="Route Mark">
|
||||
<a-input-number v-model="inbound.stream.sockopt.mark" :min="0"></a-input-number>
|
||||
<a-form-item label="Multipath TCP">
|
||||
<a-switch v-model.trim="inbound.stream.sockopt.tcpMptcp"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="TPROXY">
|
||||
<a-select v-model="inbound.stream.sockopt.tproxy" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-form-item label="TCP No-Delay" v-if="inbound.stream.sockopt.tcpMptcp">
|
||||
<a-switch v-model.trim="inbound.stream.sockopt.tcpNoDelay"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="V6 Only">
|
||||
<a-switch v-model.trim="inbound.stream.sockopt.V6Only"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='Domain Strategy'>
|
||||
<a-select v-model="inbound.stream.sockopt.domainStrategy" style="width: 50%" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in DOMAIN_STRATEGY_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='TCP Congestion'>
|
||||
<a-select v-model="inbound.stream.sockopt.tcpcongestion" style="width: 50%" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in TCP_CONGESTION_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="TProxy">
|
||||
<a-select v-model="inbound.stream.sockopt.tproxy" style="width: 50%" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="off">Off</a-select-option>
|
||||
<a-select-option value="redirect">Redirect</a-select-option>
|
||||
<a-select-option value="tproxy">TPROXY</a-select-option>
|
||||
<a-select-option value="tproxy">TProxy</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Dialer Proxy">
|
||||
<a-input v-model="inbound.stream.sockopt.dialerProxy"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Interface Name">
|
||||
<a-input v-model="inbound.stream.sockopt.interfaceName"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</a-form>
|
||||
{{end}}
|
||||
|
||||
50
web/html/xui/form/stream/stream_splithttp.html
Normal file
@@ -0,0 +1,50 @@
|
||||
{{define "form/streamSplitHTTP"}}
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
<a-input v-model.trim="inbound.stream.splithttp.host"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="inbound.stream.splithttp.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
|
||||
<a-button icon="plus" size="small" @click="inbound.stream.splithttp.addHeader('host', '')"></a-button>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{span:24}">
|
||||
<a-input-group compact v-for="(header, index) in inbound.stream.splithttp.headers">
|
||||
<a-input style="width: 50%" v-model.trim="header.name" placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'>
|
||||
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
|
||||
</a-input>
|
||||
<a-input style="width: 50%" v-model.trim="header.value" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<a-button slot="addonAfter" size="small" @click="inbound.stream.splithttp.removeHeader(index)">-</a-button>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Concurrent Upload">
|
||||
<a-input v-model.trim="inbound.stream.splithttp.scMaxConcurrentPosts"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Upload Size (Byte)">
|
||||
<a-input v-model.trim="inbound.stream.splithttp.scMaxEachPostBytes"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Min Upload Interval (Ms)">
|
||||
<a-input v-model.trim="inbound.stream.splithttp.scMinPostsIntervalMs"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Padding Bytes">
|
||||
<a-input v-model.trim="inbound.stream.splithttp.xPaddingBytes"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="No SSE Header">
|
||||
<a-switch v-model="inbound.stream.splithttp.noSSEHeader"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Concurrency" v-if="!inbound.stream.splithttp.xmux.maxConnections">
|
||||
<a-input-number v-model="inbound.stream.splithttp.xmux.maxConcurrency"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Connections" v-if="!inbound.stream.splithttp.xmux.maxConcurrency">
|
||||
<a-input-number v-model="inbound.stream.splithttp.xmux.maxConnections"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Reuse Times">
|
||||
<a-input-number v-model="inbound.stream.splithttp.xmux.cMaxReuseTimes"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Lifetime (ms)">
|
||||
<a-input-number v-model="inbound.stream.splithttp.xmux.cMaxLifetimeMs"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
@@ -1,82 +1,72 @@
|
||||
{{define "form/streamTCP"}}
|
||||
<!-- tcp type -->
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="PROXY Protocol" v-if="inbound.canEnableTls()">
|
||||
<a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='HTTP {{ i18n "camouflage" }}'>
|
||||
<a-switch :checked="inbound.stream.tcp.type === 'http'"
|
||||
@change="checked => inbound.stream.tcp.type = checked ? 'http' : 'none'">
|
||||
</a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="Proxy Protocol" v-if="inbound.canEnableTls()">
|
||||
<a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='HTTP {{ i18n "camouflage" }}'>
|
||||
<a-switch :checked="inbound.stream.tcp.type === 'http'" @change="checked => inbound.stream.tcp.type = checked ? 'http' : 'none'"></a-switch>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<a-form v-if="inbound.stream.tcp.type === 'http'" :colon="false" :label-col="{ md: {span:8} }"
|
||||
:wrapper-col="{ md: {span:14} }">
|
||||
<!-- tcp request -->
|
||||
<a-divider style="margin:0;">{{ i18n "pages.inbounds.stream.general.request" }}</a-divider>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.version" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.request.version"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.method" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.request.method"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">{{ i18n "pages.inbounds.stream.tcp.path" }}
|
||||
<a-button size="small" @click="inbound.stream.tcp.request.addPath('/')">+</a-button>
|
||||
</template>
|
||||
<template v-for="(path, index) in inbound.stream.tcp.request.path">
|
||||
<a-input v-model.trim="inbound.stream.tcp.request.path[index]">
|
||||
<a-button size="small" slot="addonAfter" @click="inbound.stream.tcp.request.removePath(index)"
|
||||
v-if="inbound.stream.tcp.request.path.length>1">-</a-button>
|
||||
</a-input>
|
||||
</template>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
|
||||
<a-button size="small" @click="inbound.stream.tcp.request.addHeader('Host', '')">+</a-button>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{span:24}">
|
||||
<a-input-group compact v-for="(header, index) in inbound.stream.tcp.request.headers">
|
||||
<a-input style="width: 50%" v-model.trim="header.name"
|
||||
placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
|
||||
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
|
||||
</a-input>
|
||||
<a-input style="width: 50%" v-model.trim="header.value"
|
||||
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<a-button slot="addonAfter" size="small"
|
||||
@click="inbound.stream.tcp.request.removeHeader(index)">-</a-button>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<a-form v-if="inbound.stream.tcp.type === 'http'" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<!-- tcp request -->
|
||||
<a-divider style="margin:0;">{{ i18n "pages.inbounds.stream.general.request" }}</a-divider>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.version" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.request.version"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.method" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.request.method"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">{{ i18n "pages.inbounds.stream.tcp.path" }}
|
||||
<a-button icon="plus" size="small" @click="inbound.stream.tcp.request.addPath('/')"></a-button>
|
||||
</template>
|
||||
<template v-for="(path, index) in inbound.stream.tcp.request.path">
|
||||
<a-input v-model.trim="inbound.stream.tcp.request.path[index]">
|
||||
<a-button icon="minus" size="small" slot="addonAfter" @click="inbound.stream.tcp.request.removePath(index)" v-if="inbound.stream.tcp.request.path.length>1"></a-button>
|
||||
</a-input>
|
||||
</template>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
|
||||
<a-button icon="plus" size="small" @click="inbound.stream.tcp.request.addHeader('Host', '')"></a-button>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{span:24}">
|
||||
<a-input-group compact v-for="(header, index) in inbound.stream.tcp.request.headers">
|
||||
<a-input style="width: 50%" v-model.trim="header.name" placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
|
||||
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
|
||||
</a-input>
|
||||
<a-input style="width: 50%" v-model.trim="header.value" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<a-button icon="minus" slot="addonAfter" size="small" @click="inbound.stream.tcp.request.removeHeader(index)"></a-button>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
|
||||
<!-- tcp response -->
|
||||
<a-divider style="margin:0;">{{ i18n "pages.inbounds.stream.general.response" }}</a-divider>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.version" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.response.version"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.status" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.response.status"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.statusDescription" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.response.reason"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}'>
|
||||
<a-button size="small"
|
||||
@click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">+</a-button>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{span:24}">
|
||||
<a-input-group compact v-for="(header, index) in inbound.stream.tcp.response.headers">
|
||||
<a-input style="width: 50%" v-model.trim="header.name"
|
||||
placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
|
||||
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
|
||||
</a-input>
|
||||
<a-input style="width: 50%" v-model.trim="header.value"
|
||||
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button size="small" @click="inbound.stream.tcp.response.removeHeader(index)">-</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<!-- tcp response -->
|
||||
<a-divider style="margin:0;">{{ i18n "pages.inbounds.stream.general.response" }}</a-divider>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.version" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.response.version"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.status" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.response.status"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.statusDescription" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.response.reason"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}'>
|
||||
<a-button icon="plus" size="small" @click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')"></a-button>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{span:24}">
|
||||
<a-input-group compact v-for="(header, index) in inbound.stream.tcp.response.headers">
|
||||
<a-input style="width: 50%" v-model.trim="header.name" placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
|
||||
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
|
||||
</a-input>
|
||||
<a-input style="width: 50%" v-model.trim="header.value" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button icon="minus" size="small" @click="inbound.stream.tcp.response.removeHeader(index)"></a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
{{define "form/streamWS"}}
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="PROXY Protocol">
|
||||
<a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
<a-input v-model.trim="inbound.stream.ws.host"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="inbound.stream.ws.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
|
||||
<a-button size="small" @click="inbound.stream.ws.addHeader('host', '')">+</a-button>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{span:24}">
|
||||
<a-input-group compact v-for="(header, index) in inbound.stream.ws.headers">
|
||||
<a-input style="width: 50%" v-model.trim="header.name" placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'>
|
||||
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
|
||||
</a-input>
|
||||
<a-input style="width: 50%" v-model.trim="header.value" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<a-button slot="addonAfter" size="small" @click="inbound.stream.ws.removeHeader(index)">-</a-button>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="Proxy Protocol">
|
||||
<a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
<a-input v-model.trim="inbound.stream.ws.host"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="inbound.stream.ws.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
|
||||
<a-button icon="plus" size="small" @click="inbound.stream.ws.addHeader('host', '')"></a-button>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{span:24}">
|
||||
<a-input-group compact v-for="(header, index) in inbound.stream.ws.headers">
|
||||
<a-input style="width: 50%" v-model.trim="header.name" placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'>
|
||||
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
|
||||
</a-input>
|
||||
<a-input style="width: 50%" v-model.trim="header.value" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<a-button icon="minus" slot="addonAfter" size="small" @click="inbound.stream.ws.removeHeader(index)"></a-button>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
|
||||
@@ -1,194 +1,129 @@
|
||||
{{define "form/tlsSettings"}}
|
||||
<!-- tls enable -->
|
||||
<a-form v-if="inbound.canEnableTls()" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-divider style="margin:3px 0;"></a-divider>
|
||||
<a-form-item label='{{ i18n "security" }}'>
|
||||
<a-radio-group v-model="inbound.stream.security" button-style="solid">
|
||||
<a-radio-button value="none">{{ i18n "none" }}</a-radio-button>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.xtlsDesc" }}</span>
|
||||
</template>
|
||||
<a-radio-button v-if="inbound.canEnableXtls()" value="xtls">XTLS</a-radio-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.realityDesc" }}</span>
|
||||
</template>
|
||||
<a-radio-button v-if="inbound.canEnableReality()" value="reality">REALITY</a-radio-button>
|
||||
</a-tooltip>
|
||||
<a-radio-button value="tls">TLS</a-radio-button>
|
||||
</a-radio-group>
|
||||
<a-divider style="margin:3px 0;"></a-divider>
|
||||
<a-form-item label='{{ i18n "security" }}'>
|
||||
<a-radio-group v-model="inbound.stream.security" button-style="solid">
|
||||
<a-radio-button value="none">{{ i18n "none" }}</a-radio-button>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.xtlsDesc" }}</span>
|
||||
</template>
|
||||
<a-radio-button v-if="inbound.canEnableXtls()" value="xtls">XTLS</a-radio-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.realityDesc" }}</span>
|
||||
</template>
|
||||
<a-radio-button v-if="inbound.canEnableReality()" value="reality">REALITY</a-radio-button>
|
||||
</a-tooltip>
|
||||
<a-radio-button value="tls">TLS</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<!-- tls settings -->
|
||||
<template v-if="inbound.stream.isTls">
|
||||
<a-form-item label="SNI" placeholder="Server Name Indication">
|
||||
<a-input v-model.trim="inbound.stream.tls.sni"></a-input>
|
||||
</a-form-item>
|
||||
|
||||
<!-- tls settings -->
|
||||
<template v-if="inbound.stream.isTls">
|
||||
<a-form-item label="SNI" placeholder="Server Name Indication">
|
||||
<a-input v-model.trim="inbound.stream.tls.sni"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Cipher Suites">
|
||||
<a-select v-model="inbound.stream.tls.cipherSuites" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="">Auto</a-select-option>
|
||||
<a-select-option v-for="key,value in TLS_CIPHER_OPTION" :value="key">[[ value ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Min/Max Version">
|
||||
<a-input-group compact>
|
||||
<a-select v-model="inbound.stream.tls.minVersion" style="width: 50%" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
<a-select v-model="inbound.stream.tls.maxVersion" style="width: 50%" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="uTLS">
|
||||
<a-select v-model="inbound.stream.tls.settings.fingerprint" style="width: 50%"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<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-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme"
|
||||
v-model="inbound.stream.tls.alpn">
|
||||
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Allow Insecure">
|
||||
<a-switch v-model="inbound.stream.tls.settings.allowInsecure"></a-switch>
|
||||
</a-form-item>
|
||||
<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>
|
||||
<template v-if="cert.useFile">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
|
||||
<a-input v-model.trim="cert.certFile"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
|
||||
<a-input v-model.trim="cert.keyFile"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-button type="primary" icon="import" @click="setDefaultCertData(index)">{{ i18n
|
||||
"pages.inbounds.setDefaultCert" }}</a-button>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
|
||||
<a-input type="textarea" :rows="3" v-model="cert.cert"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
|
||||
<a-input type="textarea" :rows="3" v-model="cert.key"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item label='OCSP stapling'>
|
||||
<a-input-number v-model.number="cert.ocspStapling" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- xtls settings -->
|
||||
<template v-else-if="inbound.xtls">
|
||||
<a-form-item label="SNI" placeholder="Server Name Indication">
|
||||
<a-input v-model.trim="inbound.stream.xtls.sni"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="ALPN">
|
||||
<a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme"
|
||||
v-model="inbound.stream.xtls.alpn">
|
||||
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Allow Insecure">
|
||||
<a-switch v-model="inbound.stream.xtls.settings.allowInsecure"></a-switch>
|
||||
</a-form-item>
|
||||
<template v-for="cert,index in inbound.stream.xtls.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.xtls.addCert()"
|
||||
style="margin-left: 10px">+</a-button>
|
||||
<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>
|
||||
<template v-if="cert.useFile">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
|
||||
<a-input v-model.trim="cert.certFile"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
|
||||
<a-input v-model.trim="cert.keyFile"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-button type="primary" icon="import" @click="setDefaultCertXtls(index)">{{ i18n
|
||||
"pages.inbounds.setDefaultCert" }}</a-button>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
|
||||
<a-input type="textarea" :rows="3" v-model="cert.cert"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
|
||||
<a-input type="textarea" :rows="3" v-model="cert.key"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
<!-- reality settings -->
|
||||
<template v-if="inbound.stream.isReality">
|
||||
<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.number="inbound.stream.reality.xver" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='uTLS'>
|
||||
<a-select v-model="inbound.stream.reality.settings.fingerprint" style="width: 50%"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Dest'>
|
||||
<a-input v-model.trim="inbound.stream.reality.dest"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='SNI'>
|
||||
<a-input v-model.trim="inbound.stream.reality.serverNames"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "reset" }}</span>
|
||||
</template>
|
||||
Short ID
|
||||
<a-icon @click="inbound.stream.reality.shortIds = RandomUtil.randomShortId()" type="sync"> </a-icon>
|
||||
</a-icon>
|
||||
</template>
|
||||
<a-input v-model.trim="inbound.stream.reality.shortIds"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='SpiderX'>
|
||||
<a-input v-model.trim="inbound.stream.reality.settings.spiderX"></a-input>
|
||||
<a-form-item label="Cipher Suites">
|
||||
<a-select v-model="inbound.stream.tls.cipherSuites" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="">Auto</a-select-option>
|
||||
<a-select-option v-for="key,value in TLS_CIPHER_OPTION" :value="key">[[ value ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Min/Max Version">
|
||||
<a-input-group compact>
|
||||
<a-select v-model="inbound.stream.tls.minVersion" style="width: 50%"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
<a-select v-model="inbound.stream.tls.maxVersion" style="width: 50%"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="uTLS">
|
||||
<a-select v-model="inbound.stream.tls.settings.fingerprint" style="width: 50%"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<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-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme" v-model="inbound.stream.tls.alpn">
|
||||
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Allow Insecure">
|
||||
<a-switch v-model="inbound.stream.tls.settings.allowInsecure"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="Reject Unknown SNI">
|
||||
<a-switch v-model="inbound.stream.tls.rejectUnknownSni"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="Disable System Root">
|
||||
<a-switch v-model="inbound.stream.tls.disableSystemRoot"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="Session Resumption">
|
||||
<a-switch v-model="inbound.stream.tls.enableSessionResumption"></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 icon="plus" v-if="index === 0" type="primary" size="small" @click="inbound.stream.tls.addCert()"
|
||||
style="margin-left: 10px"></a-button>
|
||||
<a-button icon="minus" 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>
|
||||
<template v-if="cert.useFile">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
|
||||
<a-input v-model.trim="cert.certFile"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
|
||||
<a-input v-model.trim="inbound.stream.reality.privateKey"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
|
||||
<a-input v-model.trim="inbound.stream.reality.settings.publicKey"></a-input>
|
||||
<a-input v-model.trim="cert.keyFile"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get New Cert</a-button>
|
||||
<a-button type="primary" icon="import" @click="setDefaultCertData(index)">
|
||||
{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
|
||||
<a-input type="textarea" :rows="3" v-model="cert.cert"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
|
||||
<a-input type="textarea" :rows="3" v-model="cert.key"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item label='OCSP stapling'>
|
||||
<a-input-number v-model.number="cert.ocspStapling" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="One Time Loading">
|
||||
<a-switch v-model="cert.oneTimeLoading"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='Usage Option'>
|
||||
<a-select v-model="cert.usage" style="width: 50%" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in USAGE_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Build Chain" v-if="cert.usage === 'issue'">
|
||||
<a-switch v-model="cert.buildChain"></a-switch>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- xtls settings -->
|
||||
<template v-else-if="inbound.stream.isXtls">
|
||||
{{template "form/xtlsSettings"}}
|
||||
</template>
|
||||
|
||||
<!-- reality settings -->
|
||||
<template v-if="inbound.stream.isReality">
|
||||
{{template "form/realitySettings"}}
|
||||
</template>
|
||||
</a-form>
|
||||
{{end}}
|
||||
{{end}}
|
||||
58
web/html/xui/form/xtls_settings.html
Normal file
@@ -0,0 +1,58 @@
|
||||
{{define "form/xtlsSettings"}}
|
||||
<template>
|
||||
<a-form-item label="SNI" placeholder="Server Name Indication">
|
||||
<a-input v-model.trim="inbound.stream.xtls.sni"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="ALPN">
|
||||
<a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme" v-model="inbound.stream.xtls.alpn">
|
||||
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Allow Insecure">
|
||||
<a-switch v-model="inbound.stream.xtls.settings.allowInsecure"></a-switch>
|
||||
</a-form-item>
|
||||
<template v-for="cert,index in inbound.stream.xtls.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 icon="plus" v-if="index === 0" type="primary" size="small" @click="inbound.stream.xtls.addCert()"
|
||||
style="margin-left: 10px"></a-button>
|
||||
<a-button icon="minus" 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>
|
||||
<template v-if="cert.useFile">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
|
||||
<a-input v-model.trim="cert.certFile"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
|
||||
<a-input v-model.trim="cert.keyFile"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-button type="primary" icon="import" @click="setDefaultCertXtls(index)">
|
||||
{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
|
||||
<a-input type="textarea" :rows="3" v-model="cert.cert"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
|
||||
<a-input type="textarea" :rows="3" v-model="cert.key"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item label='OCSP stapling'>
|
||||
<a-input-number v-model.number="cert.ocspStapling" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="One Time Loading">
|
||||
<a-switch v-model="cert.oneTimeLoading"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='Usage Option'>
|
||||
<a-select v-model="cert.usage" style="width: 50%" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in USAGE_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
{{end}}
|
||||
@@ -1,266 +1,267 @@
|
||||
{{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;" class="normal-icon" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record.id,client);"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "pages.client.edit" }}</template>
|
||||
<a-icon style="font-size: 24px;" class="normal-icon" 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;" class="normal-icon" type="info-circle" @click="showInfo(record.id,client);"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
|
||||
<a-popconfirm @confirm="resetClientTraffic(client,record.id,false)"
|
||||
title='{{ i18n "pages.inbounds.resetTrafficContent"}}'
|
||||
:overlay-class-name="themeSwitcher.currentTheme"
|
||||
ok-text='{{ i18n "reset"}}'
|
||||
cancel-text='{{ i18n "cancel"}}'>
|
||||
<a-icon slot="icon" type="question-circle-o" :style="themeSwitcher.isDarkTheme ? 'color: var(--color-primary-100)' : 'color: var(--color-primary-100)'"></a-icon>
|
||||
<a-icon style="font-size: 24px; cursor: pointer;" class="normal-icon" type="retweet" v-if="client.email.length > 0"></a-icon>
|
||||
</a-popconfirm>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title"><span style="color: #FF4D4F"> {{ i18n "delete"}}</span></template>
|
||||
<a-popconfirm @confirm="delClient(record.id,client,false)"
|
||||
title='{{ i18n "pages.inbounds.deleteClientContent"}}'
|
||||
:overlay-class-name="themeSwitcher.currentTheme"
|
||||
ok-text='{{ i18n "delete"}}'
|
||||
ok-type="danger"
|
||||
cancel-text='{{ i18n "cancel"}}'>
|
||||
<a-icon slot="icon" type="question-circle-o" style="color: #e04141"></a-icon>
|
||||
<a-icon style="font-size: 24px; cursor: pointer;" class="delete-icon" type="delete" v-if="isRemovable(record.id)"></a-icon>
|
||||
</a-popconfirm>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "qrCode" }}</template>
|
||||
<a-icon style="font-size: 24px;" class="normal-icon" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record.id,client);"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "pages.client.edit" }}</template>
|
||||
<a-icon style="font-size: 24px;" class="normal-icon" 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;" class="normal-icon" type="info-circle" @click="showInfo(record.id,client);"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
|
||||
<a-popconfirm @confirm="resetClientTraffic(client,record.id,false)" title='{{ i18n "pages.inbounds.resetTrafficContent"}}' :overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "reset"}}' cancel-text='{{ i18n "cancel"}}'>
|
||||
<a-icon slot="icon" type="question-circle-o" :style="themeSwitcher.isDarkTheme ? 'color: var(--color-primary-100)' : 'color: var(--color-primary-100)'"></a-icon>
|
||||
<a-icon style="font-size: 24px; cursor: pointer;" class="normal-icon" type="retweet" v-if="client.email.length > 0"></a-icon>
|
||||
</a-popconfirm>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span style="color: #FF4D4F"> {{ i18n "delete"}}</span>
|
||||
</template>
|
||||
<a-popconfirm @confirm="delClient(record.id,client,false)" title='{{ i18n "pages.inbounds.deleteClientContent"}}' :overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "delete"}}' ok-type="danger" cancel-text='{{ i18n "cancel"}}'>
|
||||
<a-icon slot="icon" type="question-circle-o" style="color: #e04141"></a-icon>
|
||||
<a-icon style="font-size: 24px; cursor: pointer;" class="delete-icon" type="delete" v-if="isRemovable(record.id)"></a-icon>
|
||||
</a-popconfirm>
|
||||
</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>
|
||||
<a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch>
|
||||
</template>
|
||||
<template slot="online" slot-scope="text, client, index">
|
||||
<template v-if="client.enable && isClientOnline(client.email)">
|
||||
<a-tag color="green">{{ i18n "online" }}</a-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag>{{ i18n "offline" }}</a-tag>
|
||||
</template>
|
||||
<template v-if="client.enable && isClientOnline(client.email)">
|
||||
<a-tag color="green">{{ i18n "online" }}</a-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag>{{ i18n "offline" }}</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
<template slot="client" slot-scope="text, client">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<template v-if="!isClientEnabled(record, client.email)">{{ i18n "depleted" }}</template>
|
||||
<template v-else-if="!client.enable">{{ i18n "disabled" }}</template>
|
||||
<template v-else-if="client.enable && isClientOnline(client.email)">{{ i18n "online" }}</template>
|
||||
</template>
|
||||
<a-badge :class="isClientOnline(client.email)? 'online-animation' : ''" :color="client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'">
|
||||
</a-badge>
|
||||
</a-tooltip>
|
||||
[[ client.email ]]
|
||||
</template>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<template v-if="!isClientEnabled(record, client.email)">{{ i18n "depleted" }}</template>
|
||||
<template v-else-if="!client.enable">{{ i18n "disabled" }}</template>
|
||||
<template v-else-if="client.enable && isClientOnline(client.email)">{{ i18n "online" }}</template>
|
||||
</template>
|
||||
<a-badge :class="isClientOnline(client.email)? 'online-animation' : ''" :color="client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-badge>
|
||||
</a-tooltip> [[ client.email ]]
|
||||
</template>
|
||||
<template slot="traffic" slot-scope="text, client">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<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(getRemStats(record, client.email)) ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
<table>
|
||||
<tr>
|
||||
<td width="80px" style="margin:0; text-align: right;font-size: 1em;">
|
||||
[[ sizeFormat(getSumStats(record, client.email)) ]]
|
||||
</td>
|
||||
<td width="120px" v-if="!client.enable">
|
||||
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'"
|
||||
:show-info="false"
|
||||
:percent="statsProgress(record, client.email)"/>
|
||||
</td>
|
||||
<td width="120px" v-else-if="client.totalGB > 0">
|
||||
<a-progress :stroke-color="clientStatsColor(record, client.email)"
|
||||
:show-info="false"
|
||||
:status="isClientEnabled(record, client.email)? 'exception' : ''"
|
||||
:percent="statsProgress(record, client.email)"/>
|
||||
</td>
|
||||
<td width="120px" v-else class="infinite-bar">
|
||||
<a-progress
|
||||
:show-info="false"
|
||||
:percent="100"></a-progress>
|
||||
</td>
|
||||
<td width="60px">
|
||||
<template v-if="client.totalGB > 0">[[ client._totalGB + "GB" ]]</template>
|
||||
<span v-else style="font-weight: 100;font-size: 14pt;">∞</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-popover>
|
||||
</template>
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<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(getRemStats(record, client.email)) ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
<table>
|
||||
<tr class="tr-table-box">
|
||||
<td class="tr-table-rt"> [[ sizeFormat(getSumStats(record, client.email)) ]] </td>
|
||||
<td class="tr-table-bar" v-if="!client.enable">
|
||||
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'" :show-info="false" :percent="statsProgress(record, client.email)" />
|
||||
</td>
|
||||
<td class="tr-table-bar" v-else-if="client.totalGB > 0">
|
||||
<a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="statsProgress(record, client.email)" />
|
||||
</td>
|
||||
<td v-else class="infinite-bar tr-table-bar">
|
||||
<a-progress :show-info="false" :percent="100"></a-progress>
|
||||
</td>
|
||||
<td class="tr-table-lt">
|
||||
<template v-if="client.totalGB > 0">[[ client._totalGB + "GB" ]]</template>
|
||||
<span v-else class="tr-infinity-ch">∞</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-popover>
|
||||
</template>
|
||||
<template slot="expiryTime" slot-scope="text, client, index">
|
||||
<template v-if="client.expiryTime !=0 && client.reset >0">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
|
||||
<span v-else>[[ DateUtil.formatMillis(client._expiryTime) ]]</span>
|
||||
</template>
|
||||
<table>
|
||||
<tr>
|
||||
<td width="80px" style="margin:0; text-align: right;font-size: 1em;">
|
||||
[[ remainedDays(client.expiryTime) ]]
|
||||
</td>
|
||||
<td width="120px" class="infinite-bar">
|
||||
<a-progress :show-info="false"
|
||||
:status="isClientEnabled(record, client.email)? 'exception' : ''"
|
||||
:percent="expireProgress(client.expiryTime, client.reset)"/>
|
||||
</td>
|
||||
<td width="60px">[[ client.reset + "d" ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-popover>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-popover v-if="client.expiryTime != 0" :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
|
||||
<span v-else>[[ DateUtil.formatMillis(client._expiryTime) ]]</span>
|
||||
</template>
|
||||
<a-tag style="min-width: 50px; border: none;" :color="userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)">
|
||||
[[ remainedDays(client.expiryTime) ]]
|
||||
</a-tag>
|
||||
</a-popover>
|
||||
<a-tag v-else :color="userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)" style="border: none;" class="infinite-tag">∞</a-tag>
|
||||
</template>
|
||||
<template v-if="client.expiryTime !=0 && client.reset >0">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}
|
||||
</span>
|
||||
<span v-else>
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
[[ DateUtil.formatMillis(client._expiryTime) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[ DateUtil.convertToJalalian(moment(client._expiryTime)) ]]
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
<table>
|
||||
<tr class="tr-table-box">
|
||||
<td class="tr-table-rt"> [[ remainedDays(client.expiryTime) ]] </td>
|
||||
<td class="infinite-bar tr-table-bar">
|
||||
<a-progress :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" />
|
||||
</td>
|
||||
<td class="tr-table-lt">[[ client.reset + "d" ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-popover>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-popover v-if="client.expiryTime != 0" :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}
|
||||
</span>
|
||||
<span v-else>
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
[[ DateUtil.formatMillis(client._expiryTime) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[ DateUtil.convertToJalalian(moment(client._expiryTime)) ]]
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
<a-tag style="min-width: 50px; border: none;" :color="userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> [[ remainedDays(client.expiryTime) ]] </a-tag>
|
||||
</a-popover>
|
||||
<a-tag v-else :color="userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)" style="border: none;" class="infinite-tag">
|
||||
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
|
||||
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
|
||||
</svg>
|
||||
</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
<template slot="actionMenu" slot-scope="text, client, index">
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-icon @click="e => e.preventDefault()" type="ellipsis" style="font-size: 20px;"></a-icon>
|
||||
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
|
||||
<a-menu-item v-if="record.hasLink()" @click="showQrcode(record.id,client);">
|
||||
<a-icon style="font-size: 14px;" type="qrcode"></a-icon>
|
||||
{{ i18n "qrCode" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="openEditClient(record.id,client);">
|
||||
<a-icon style="font-size: 14px;" type="edit"></a-icon>
|
||||
{{ i18n "pages.client.edit" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="showInfo(record.id,client);">
|
||||
<a-icon style="font-size: 14px;" type="info-circle"></a-icon>
|
||||
{{ i18n "info" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="resetClientTraffic(client,record.id)" v-if="client.email.length > 0">
|
||||
<a-icon style="font-size: 14px;" type="retweet"></a-icon>
|
||||
{{ i18n "pages.inbounds.resetTraffic" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item v-if="isRemovable(record.id)" @click="delClient(record.id,client)">
|
||||
<a-icon style="font-size: 14px;" type="delete"></a-icon>
|
||||
<span style="color: #FF4D4F"> {{ i18n "delete"}}</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item>
|
||||
<a-switch v-model="client.enable" size="small" @change="switchEnableClient(record.id,client)">
|
||||
</a-switch>
|
||||
{{ i18n "enable"}}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-icon @click="e => e.preventDefault()" type="ellipsis" style="font-size: 20px;"></a-icon>
|
||||
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
|
||||
<a-menu-item v-if="record.hasLink()" @click="showQrcode(record.id,client);">
|
||||
<a-icon style="font-size: 14px;" type="qrcode"></a-icon>
|
||||
{{ i18n "qrCode" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="openEditClient(record.id,client);">
|
||||
<a-icon style="font-size: 14px;" type="edit"></a-icon>
|
||||
{{ i18n "pages.client.edit" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="showInfo(record.id,client);">
|
||||
<a-icon style="font-size: 14px;" type="info-circle"></a-icon>
|
||||
{{ i18n "info" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="resetClientTraffic(client,record.id)" v-if="client.email.length > 0">
|
||||
<a-icon style="font-size: 14px;" type="retweet"></a-icon>
|
||||
{{ i18n "pages.inbounds.resetTraffic" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item v-if="isRemovable(record.id)" @click="delClient(record.id,client)">
|
||||
<a-icon style="font-size: 14px;" type="delete"></a-icon>
|
||||
<span style="color: #FF4D4F"> {{ i18n "delete"}}</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item>
|
||||
<a-switch v-model="client.enable" size="small" @change="switchEnableClient(record.id,client)"></a-switch>
|
||||
{{ i18n "enable"}}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
<template slot="info" slot-scope="text, client, index">
|
||||
<a-popover placement="bottomRight" :overlay-class-name="themeSwitcher.currentTheme" trigger="click">
|
||||
<template slot="content">
|
||||
<table>
|
||||
<tr>
|
||||
<td colspan="3" style="text-align: center;">{{ i18n "pages.inbounds.traffic" }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="80px" style="margin:0; text-align: right;font-size: 1em;">
|
||||
[[ sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]]
|
||||
</td>
|
||||
<td width="120px" v-if="!client.enable">
|
||||
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'"
|
||||
:show-info="false"
|
||||
:percent="statsProgress(record, client.email)"/>
|
||||
</td>
|
||||
<td width="120px" v-else-if="client.totalGB > 0">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<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>
|
||||
<td>{{ i18n "remained" }}</td>
|
||||
<td>[[ sizeFormat(getRemStats(record, client.email)) ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
<a-progress :stroke-color="clientStatsColor(record, client.email)"
|
||||
:show-info="false"
|
||||
:status="isClientEnabled(record, client.email)? 'exception' : ''"
|
||||
:percent="statsProgress(record, client.email)"/>
|
||||
</a-popover>
|
||||
</td>
|
||||
<td width="120px" v-else class="infinite-bar">
|
||||
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? '#2c1e32':'#F2EAF1'"
|
||||
:show-info="false"
|
||||
:percent="100"></a-progress>
|
||||
</td>
|
||||
<td width="80px">
|
||||
<template v-if="client.totalGB > 0">[[ client._totalGB + "GB" ]]</template>
|
||||
<span v-else style="font-weight: 100;font-size: 14pt;">∞</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3" style="text-align: center;">
|
||||
<a-divider style="margin: 0; border-collapse: separate;"></a-divider>
|
||||
{{ i18n "pages.inbounds.expireDate" }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<template v-if="client.expiryTime !=0 && client.reset >0">
|
||||
<td width="80px" style="margin:0; text-align: right;font-size: 1em;">
|
||||
[[ remainedDays(client.expiryTime) ]]
|
||||
</td>
|
||||
<td width="120px" class="infinite-bar">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
|
||||
<span v-else>[[ DateUtil.formatMillis(client._expiryTime) ]]</span>
|
||||
</template>
|
||||
<a-progress :show-info="false"
|
||||
:status="isClientEnabled(record, client.email)? 'exception' : ''"
|
||||
:percent="expireProgress(client.expiryTime, client.reset)"/>
|
||||
</a-popover>
|
||||
</td>
|
||||
<td width="60px">[[ client.reset + "d" ]]</td>
|
||||
<a-popover placement="bottomRight" :overlay-class-name="themeSwitcher.currentTheme" trigger="click">
|
||||
<template slot="content">
|
||||
<table>
|
||||
<tr>
|
||||
<td colspan="3" style="text-align: center;">{{ i18n "pages.inbounds.traffic" }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="80px" style="margin:0; text-align: right;font-size: 1em;"> [[ sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]] </td>
|
||||
<td width="120px" v-if="!client.enable">
|
||||
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'" :show-info="false" :percent="statsProgress(record, client.email)" />
|
||||
</td>
|
||||
<td width="120px" v-else-if="client.totalGB > 0">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<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>
|
||||
<td>{{ i18n "remained" }}</td>
|
||||
<td>[[ sizeFormat(getRemStats(record, client.email)) ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
<a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="statsProgress(record, client.email)" />
|
||||
</a-popover>
|
||||
</td>
|
||||
<td width="120px" v-else class="infinite-bar">
|
||||
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? '#2c1e32':'#F2EAF1'" :show-info="false" :percent="100"></a-progress>
|
||||
</td>
|
||||
<td width="80px">
|
||||
<template v-if="client.totalGB > 0">[[ client._totalGB + "GB" ]]</template>
|
||||
<span v-else class="tr-infinity-ch">∞</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3" style="text-align: center;">
|
||||
<a-divider style="margin: 0; border-collapse: separate;"></a-divider>
|
||||
{{ i18n "pages.inbounds.expireDate" }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<template v-if="client.expiryTime !=0 && client.reset >0">
|
||||
<td width="80px" style="margin:0; text-align: right;font-size: 1em;"> [[ remainedDays(client.expiryTime) ]] </td>
|
||||
<td width="120px" class="infinite-bar">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}
|
||||
</span>
|
||||
<span v-else>
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
[[ DateUtil.formatMillis(client._expiryTime) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[ DateUtil.convertToJalalian(moment(client._expiryTime)) ]]
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<td colspan="3" style="text-align: center;">
|
||||
<a-popover v-if="client.expiryTime != 0" :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
|
||||
<span v-else>[[ DateUtil.formatMillis(client._expiryTime) ]]</span>
|
||||
</template>
|
||||
<a-tag style="min-width: 50px; border: none;"
|
||||
:color="userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)">
|
||||
[[ remainedDays(client.expiryTime) ]]
|
||||
</a-tag>
|
||||
</a-popover>
|
||||
<a-tag v-else :color="client.enable ? 'purple' : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'" class="infinite-tag">∞</a-tag>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
<a-badge>
|
||||
<a-icon v-if="!client.enable" slot="count" type="pause-circle" theme="filled" :style="'color: ' + themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-icon>
|
||||
<a-button shape="round" size="small" style="font-size: 14px; padding: 0 10px;"><a-icon type="solution"></a-icon></a-button>
|
||||
</a-badge>
|
||||
</a-popover>
|
||||
<a-progress :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" />
|
||||
</a-popover>
|
||||
</td>
|
||||
<td width="60px">[[ client.reset + "d" ]]</td>
|
||||
</template>
|
||||
<template v-else>
|
||||
<td colspan="3" style="text-align: center;">
|
||||
<a-popover v-if="client.expiryTime != 0" :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}
|
||||
</span>
|
||||
<span v-else>
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
[[ DateUtil.formatMillis(client._expiryTime) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[ DateUtil.convertToJalalian(moment(client._expiryTime)) ]]
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
<a-tag style="min-width: 50px; border: none;" :color="userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> [[ remainedDays(client.expiryTime) ]] </a-tag>
|
||||
</a-popover>
|
||||
<a-tag v-else :color="client.enable ? 'purple' : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'" class="infinite-tag">
|
||||
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
|
||||
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
|
||||
</svg>
|
||||
</a-tag>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
<a-badge>
|
||||
<a-icon v-if="!client.enable" slot="count" type="pause-circle" theme="filled" :style="'color: ' + themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-icon>
|
||||
<a-button shape="round" size="small" style="font-size: 14px; padding: 0 10px;">
|
||||
<a-icon type="solution"></a-icon>
|
||||
</a-button>
|
||||
</a-badge>
|
||||
</a-popover>
|
||||
</template>
|
||||
{{end}}
|
||||
|
||||
@@ -1,436 +1,509 @@
|
||||
{{define "inboundInfoModal"}}
|
||||
<a-modal id="inbound-info-modal"
|
||||
v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}'
|
||||
:closable="true"
|
||||
:mask-closable="true"
|
||||
:footer="null"
|
||||
width="600px"
|
||||
:class="themeSwitcher.currentTheme"
|
||||
>
|
||||
<a-row>
|
||||
<a-col :xs="24" :md="12">
|
||||
<table>
|
||||
<tr><td>{{ i18n "protocol" }}</td><td><a-tag color="purple">[[ dbInbound.protocol ]]</a-tag></td></tr>
|
||||
<tr><td>{{ i18n "pages.inbounds.address" }}</td><td>
|
||||
<a-tooltip :title="[[ dbInbound.address ]]">
|
||||
<a-tag class="info-large-tag">[[ dbInbound.address ]]</a-tag>
|
||||
</a-tooltip>
|
||||
</td></tr>
|
||||
<tr><td>{{ i18n "pages.inbounds.port" }}</td><td><a-tag>[[ dbInbound.port ]]</a-tag></td></tr>
|
||||
</table>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="12">
|
||||
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
|
||||
<table>
|
||||
<tr>
|
||||
<td>{{ i18n "transmission" }}</td><td><a-tag color="green">[[ inbound.network ]]</a-tag></td>
|
||||
</tr>
|
||||
<template v-if="inbound.isTcp || inbound.isWs || inbound.isH2 || inbound.isHttpupgrade ">
|
||||
<tr>
|
||||
<td>{{ i18n "host" }}</td>
|
||||
<td v-if="inbound.host">
|
||||
<a-tooltip :title="[[ inbound.host ]]">
|
||||
<a-tag class="info-large-tag">[[ inbound.host ]]</a-tag>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td v-else><a-tag color="orange">{{ i18n "none" }}</a-tag></td></tr>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "path" }}</td>
|
||||
<td v-if="inbound.path">
|
||||
<a-tooltip :title="[[ inbound.path ]]">
|
||||
<a-tag class="info-large-tag">[[ inbound.path ]]</a-tag>
|
||||
</a-tooltip>
|
||||
<td v-else><a-tag color="orange">{{ i18n "none" }}</a-tag></td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<template v-if="inbound.isQuic">
|
||||
<tr><td>quic {{ i18n "encryption" }}</td><td><a-tag>[[ inbound.quicSecurity ]]</a-tag></td></tr>
|
||||
<tr><td>quic {{ i18n "password" }}</td><td><a-tag>[[ inbound.quicKey ]]</a-tag></td></tr>
|
||||
<tr><td>quic {{ i18n "camouflage" }}</td><td><a-tag>[[ inbound.quicType ]]</a-tag></td></tr>
|
||||
</template>
|
||||
|
||||
<template v-if="inbound.isKcp">
|
||||
<tr><td>kcp {{ i18n "encryption" }}</td><td><a-tag>[[ inbound.kcpType ]]</a-tag></td></tr>
|
||||
<tr><td>kcp {{ i18n "password" }}</td><td><a-tag>[[ inbound.kcpSeed ]]</a-tag></td></tr>
|
||||
</template>
|
||||
|
||||
<template v-if="inbound.isGrpc">
|
||||
<tr><td>grpc serviceName</td><td>
|
||||
<a-tooltip :title="[[ inbound.serviceName ]]">
|
||||
<a-tag class="info-large-tag">[[ inbound.serviceName ]]</a-tag>
|
||||
</a-tooltip>
|
||||
<tr><td>grpc multiMode</td><td><a-tag>[[ inbound.stream.grpc.multiMode ]]</a-tag></td></tr>
|
||||
</template>
|
||||
</table>
|
||||
</template>
|
||||
</a-col>
|
||||
<template v-if="dbInbound.hasLink()">
|
||||
{{ i18n "security" }}
|
||||
<a-tag :color="inbound.stream.security == 'none' ? 'red' : 'green'">[[ inbound.stream.security ]]</a-tag>
|
||||
<br />
|
||||
<template v-if="inbound.stream.security != 'none'">
|
||||
{{ i18n "domainName" }}
|
||||
<a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
<table v-if="dbInbound.isSS" style="margin-bottom: 10px; width: 100%;">
|
||||
<a-modal id="inbound-info-modal" v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}' :closable="true" :mask-closable="true" :footer="null" width="600px" :class="themeSwitcher.currentTheme">
|
||||
<a-row>
|
||||
<a-col :xs="24" :md="12">
|
||||
<table>
|
||||
<tr>
|
||||
<td>{{ i18n "encryption" }}</td>
|
||||
<td><a-tag color="green">[[ inbound.settings.method ]]</a-tag></td>
|
||||
</tr><tr v-if="inbound.isSS2022">
|
||||
<td>{{ i18n "password" }}</td>
|
||||
<td>
|
||||
<a-tooltip :title="[[ inbound.settings.password ]]">
|
||||
<a-tag class="info-large-tag">[[ inbound.settings.password ]]</a-tag>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td>{{ i18n "pages.inbounds.network" }}</td>
|
||||
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
|
||||
<td>{{ i18n "protocol" }}</td>
|
||||
<td>
|
||||
<a-tag color="purple">[[ dbInbound.protocol ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.address" }}</td>
|
||||
<td>
|
||||
<a-tooltip :title="[[ dbInbound.address ]]">
|
||||
<a-tag class="info-large-tag">[[ dbInbound.address ]]</a-tag>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.port" }}</td>
|
||||
<td>
|
||||
<a-tag>[[ dbInbound.port ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="12">
|
||||
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
|
||||
<table>
|
||||
<tr>
|
||||
<td>{{ i18n "transmission" }}</td>
|
||||
<td>
|
||||
<a-tag color="green">[[ inbound.network ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<template v-if="inbound.isTcp || inbound.isWs || inbound.isH2 || inbound.isHttpupgrade || inbound.isSplithttp">
|
||||
<tr>
|
||||
<td>{{ i18n "host" }}</td>
|
||||
<td v-if="inbound.host">
|
||||
<a-tooltip :title="[[ inbound.host ]]">
|
||||
<a-tag class="info-large-tag">[[ inbound.host ]]</a-tag>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td v-else>
|
||||
<a-tag color="orange">{{ i18n "none" }}</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "path" }}</td>
|
||||
<td v-if="inbound.path">
|
||||
<a-tooltip :title="[[ inbound.path ]]">
|
||||
<a-tag class="info-large-tag">[[ inbound.path ]]</a-tag>
|
||||
</a-tooltip>
|
||||
<td v-else>
|
||||
<a-tag color="orange">{{ i18n "none" }}</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template v-if="inbound.isKcp">
|
||||
<tr>
|
||||
<td>kcp {{ i18n "encryption" }}</td>
|
||||
<td>
|
||||
<a-tag>[[ inbound.kcpType ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>kcp {{ i18n "password" }}</td>
|
||||
<td>
|
||||
<a-tag>[[ inbound.kcpSeed ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template v-if="inbound.isGrpc">
|
||||
<tr>
|
||||
<td>grpc serviceName</td>
|
||||
<td>
|
||||
<a-tooltip :title="[[ inbound.serviceName ]]">
|
||||
<a-tag class="info-large-tag">[[ inbound.serviceName ]]</a-tag>
|
||||
</a-tooltip>
|
||||
<tr>
|
||||
<td>grpc multiMode</td>
|
||||
<td>
|
||||
<a-tag>[[ inbound.stream.grpc.multiMode ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</table>
|
||||
</template>
|
||||
</a-col>
|
||||
<template v-if="dbInbound.hasLink()">
|
||||
{{ i18n "security" }}
|
||||
<a-tag :color="inbound.stream.security == 'none' ? 'red' : 'green'">[[ inbound.stream.security ]]</a-tag>
|
||||
<br />
|
||||
<template v-if="inbound.stream.security != 'none'">
|
||||
{{ i18n "domainName" }}
|
||||
<a-tag v-if="inbound.serverName" :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
|
||||
<a-tag v-else color="orange">{{ i18n "none" }}</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
<table v-if="dbInbound.isSS" style="margin-bottom: 10px; width: 100%;">
|
||||
<tr>
|
||||
<td>{{ i18n "encryption" }}</td>
|
||||
<td>
|
||||
<a-tag color="green">[[ inbound.settings.method ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="inbound.isSS2022">
|
||||
<td>{{ i18n "password" }}</td>
|
||||
<td>
|
||||
<a-tooltip :title="[[ inbound.settings.password ]]">
|
||||
<a-tag class="info-large-tag">[[ inbound.settings.password ]]</a-tag>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.network" }}</td>
|
||||
<td>
|
||||
<a-tag color="green">[[ inbound.settings.network ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<template v-if="infoModal.clientSettings">
|
||||
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
|
||||
<table style="margin-bottom: 10px;">
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.email" }}</td>
|
||||
<td><a-tag color="green">[[ infoModal.clientSettings.email ]]</a-tag></td>
|
||||
</tr>
|
||||
<tr v-if="infoModal.clientSettings.id">
|
||||
<td>ID</td>
|
||||
<td><a-tag>[[ infoModal.clientSettings.id ]]</a-tag></td>
|
||||
</tr>
|
||||
<tr v-if="infoModal.inbound.canEnableTlsFlow()">
|
||||
<td>Flow</td>
|
||||
<td><a-tag>[[ infoModal.clientSettings.flow ]]</a-tag></td>
|
||||
</tr>
|
||||
<tr v-if="infoModal.inbound.xtls">
|
||||
<td>Flow</td>
|
||||
<td><a-tag>[[ infoModal.clientSettings.flow ]]</a-tag></td>
|
||||
</tr>
|
||||
<tr v-if="infoModal.clientSettings.password">
|
||||
<td>{{ i18n "password" }}</td>
|
||||
<td>
|
||||
<a-tooltip :title="[[ infoModal.clientSettings.password ]]">
|
||||
<a-tag class="info-large-tag">[[ infoModal.clientSettings.password ]]</a-tag>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "status" }}</td>
|
||||
<td>
|
||||
<a-tag v-if="isEnable" color="green">{{ i18n "enabled" }}</a-tag>
|
||||
<a-tag v-else>{{ 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>↑ [[ sizeFormat(infoModal.clientStats.up) ]] / [[ sizeFormat(infoModal.clientStats.down) ]] ↓</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table style="margin-bottom: 10px; width: 100%; text-align: center;">
|
||||
<tr>
|
||||
<th>{{ i18n "remained" }}</th>
|
||||
<th>{{ i18n "pages.inbounds.totalFlow" }}</th>
|
||||
<th>{{ i18n "pages.inbounds.expireDate" }}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a-tag v-if="infoModal.clientStats && infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)">
|
||||
[[ getRemStats() ]]
|
||||
</a-tag>
|
||||
</td>
|
||||
<td>
|
||||
<a-tag v-if="infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)">
|
||||
[[ sizeFormat(infoModal.clientSettings.totalGB) ]]
|
||||
</a-tag>
|
||||
<a-tag v-else color="purple" class="infinite-tag">∞</a-tag>
|
||||
</td>
|
||||
<td>
|
||||
<template v-if="infoModal.clientSettings.expiryTime > 0">
|
||||
<a-tag :color="usageColor(new Date().getTime(), app.expireDiff, infoModal.clientSettings.expiryTime)">
|
||||
[[ DateUtil.formatMillis(infoModal.clientSettings.expiryTime) ]]
|
||||
</a-tag>
|
||||
</template>
|
||||
<a-tag v-else-if="infoModal.clientSettings.expiryTime < 0" color="green">[[ infoModal.clientSettings.expiryTime / -86400000 ]] {{ i18n "pages.client.days" }}</a-tag>
|
||||
<a-tag v-else color="purple" class="infinite-tag">∞</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<template v-if="app.subSettings.enable && infoModal.clientSettings.subId">
|
||||
<a-divider>Subscription URL</a-divider>
|
||||
<a-row>
|
||||
<a-col :sx="24" :md="22">SUB: <a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a></a-col>
|
||||
<a-col :sx="24" :md="2" style="text-align: right;">
|
||||
<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>
|
||||
<a-row>
|
||||
<a-col :sx="24" :md="22">JSON: <a :href="[[ infoModal.subJsonLink ]]" target="_blank">[[ infoModal.subJsonLink ]]</a></a-col>
|
||||
<a-col :sx="24" :md="2" style="text-align: right; margin-top: 5px;">
|
||||
<a-tooltip title='{{ i18n "copy" }}'>
|
||||
<button class="ant-btn ant-btn-primary" id="copy-subJson-link" @click="copyToClipboard('copy-subJson-link', infoModal.subJsonLink)">
|
||||
<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 ID</a-divider>
|
||||
<a-row>
|
||||
<a-col :sx="24" :md="22">[[ infoModal.clientSettings.tgId ]]</a-col>
|
||||
<a-col :sx="24" :md="2" style="text-align: right;">
|
||||
<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-row v-for="(link,index) in infoModal.links">
|
||||
<a-col :sx="24" :md="22"><a-tag color="green">[[ link.remark ]]</a-tag><br />[[ link.link ]]</a-col>
|
||||
<a-col :sx="24" :md="2" style="text-align: right;">
|
||||
<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>
|
||||
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
|
||||
<table style="margin-bottom: 10px;">
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.email" }}</td>
|
||||
<td v-if="infoModal.clientSettings.email">
|
||||
<a-tag color="green">[[ infoModal.clientSettings.email ]]</a-tag>
|
||||
</td>
|
||||
<td v-else>
|
||||
<a-tag color="red">{{ i18n "none" }}</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="infoModal.clientSettings.id">
|
||||
<td>ID</td>
|
||||
<td>
|
||||
<a-tag>[[ infoModal.clientSettings.id ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="dbInbound.isVMess">
|
||||
<td>{{ i18n "security" }}</td>
|
||||
<td>
|
||||
<a-tag>[[ infoModal.clientSettings.security ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="infoModal.inbound.canEnableTlsFlow()">
|
||||
<td>Flow</td>
|
||||
<td v-if="infoModal.clientSettings.flow">
|
||||
<a-tag>[[ infoModal.clientSettings.flow ]]</a-tag>
|
||||
</td>
|
||||
<td v-else>
|
||||
<a-tag color="orange">{{ i18n "none" }}</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="infoModal.inbound.xtls">
|
||||
<td>Flow</td>
|
||||
<td v-if="infoModal.clientSettings.flow">
|
||||
<a-tag>[[ infoModal.clientSettings.flow ]]</a-tag>
|
||||
</td>
|
||||
<td v-else>
|
||||
<a-tag color="orange">{{ i18n "none" }}</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="infoModal.clientSettings.password">
|
||||
<td>{{ i18n "password" }}</td>
|
||||
<td>
|
||||
<a-tooltip :title="[[ infoModal.clientSettings.password ]]">
|
||||
<a-tag class="info-large-tag">[[ infoModal.clientSettings.password ]]</a-tag>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "status" }}</td>
|
||||
<td>
|
||||
<a-tag v-if="isEnable" color="green">{{ i18n "enabled" }}</a-tag>
|
||||
<a-tag v-else>{{ 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>↑ [[ sizeFormat(infoModal.clientStats.up) ]] / [[ sizeFormat(infoModal.clientStats.down) ]] ↓</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table style="display: inline-table; margin-block: 10px; width: 100%; text-align: center;">
|
||||
<tr>
|
||||
<th>{{ i18n "remained" }}</th>
|
||||
<th>{{ i18n "pages.inbounds.totalFlow" }}</th>
|
||||
<th>{{ i18n "pages.inbounds.expireDate" }}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a-tag v-if="infoModal.clientStats && infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)"> [[ getRemStats() ]] </a-tag>
|
||||
</td>
|
||||
<td>
|
||||
<a-tag v-if="infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)"> [[ sizeFormat(infoModal.clientSettings.totalGB) ]] </a-tag>
|
||||
<a-tag v-else color="purple" class="infinite-tag">
|
||||
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
|
||||
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
|
||||
</svg>
|
||||
</a-tag>
|
||||
</td>
|
||||
<td>
|
||||
<template v-if="infoModal.clientSettings.expiryTime > 0">
|
||||
<a-tag :color="usageColor(new Date().getTime(), app.expireDiff, infoModal.clientSettings.expiryTime)">
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
[[ DateUtil.formatMillis(infoModal.clientSettings.expiryTime) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[ DateUtil.convertToJalalian(moment(infoModal.clientSettings.expiryTime)) ]]
|
||||
</template>
|
||||
</a-tag>
|
||||
</template>
|
||||
<a-tag v-else-if="infoModal.clientSettings.expiryTime < 0" color="green">[[ infoModal.clientSettings.expiryTime / -86400000 ]] {{ i18n "pages.client.days" }}
|
||||
</a-tag>
|
||||
<a-tag v-else color="purple" class="infinite-tag">
|
||||
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
|
||||
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
|
||||
</svg>
|
||||
</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<template v-if="app.subSettings.enable && infoModal.clientSettings.subId">
|
||||
<a-divider>Subscription URL</a-divider>
|
||||
<tr-info-row class="tr-info-row">
|
||||
<tr-info-title class="tr-info-title">
|
||||
<a-tag color="purple">Subscription Link</a-tag>
|
||||
<a-tooltip title='{{ i18n "copy" }}'>
|
||||
<a-button size="small" icon="snippets" id="copy-sub-link" @click="copyToClipboard('copy-sub-link', infoModal.subLink)"></a-button>
|
||||
</a-tooltip>
|
||||
</tr-info-title>
|
||||
<a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a>
|
||||
</tr-info-row>
|
||||
<tr-info-row class="tr-info-row">
|
||||
<tr-info-title class="tr-info-title">
|
||||
<a-tag color="purple">Json Link</a-tag>
|
||||
<a-tooltip title='{{ i18n "copy" }}'>
|
||||
<a-button size="small" icon="snippets" id="copy-subJson-link" @click="copyToClipboard('copy-subJson-link', infoModal.subJsonLink)"></a-button>
|
||||
</a-tooltip>
|
||||
</tr-info-title>
|
||||
<a :href="[[ infoModal.subJsonLink ]]" target="_blank">[[ infoModal.subJsonLink ]]</a>
|
||||
</tr-info-row>
|
||||
</template>
|
||||
<template v-if="app.tgBotEnable && infoModal.clientSettings.tgId">
|
||||
<a-divider>Telegram ChatID</a-divider>
|
||||
<tr-info-row class="tr-info-row">
|
||||
<tr-info-title class="tr-info-title">
|
||||
<a-tag color="blue">[[ infoModal.clientSettings.tgId ]]</a-tag>
|
||||
<a-tooltip title='{{ i18n "copy" }}'>
|
||||
<a-button size="small" icon="snippets" id="copy-tg-link" @click="copyToClipboard('copy-tg-link', infoModal.clientSettings.tgId)"></a-button>
|
||||
</a-tooltip>
|
||||
</tr-info-title>
|
||||
</tr-info-row>
|
||||
</template>
|
||||
<template v-if="dbInbound.hasLink()">
|
||||
<a-divider>URL</a-divider>
|
||||
<tr-info-row v-for="(link,index) in infoModal.links" class="tr-info-row">
|
||||
<tr-info-title class="tr-info-title">
|
||||
<a-tag class="tr-info-tag" color="green">[[ link.remark ]]</a-tag>
|
||||
<a-tooltip title='{{ i18n "copy" }}'>
|
||||
<a-button style="min-width: 24px;" size="small" icon="snippets" :id="'copy-url-link-'+index" @click="copyToClipboard('copy-url-link-'+index, link.link)"></a-button>
|
||||
</a-tooltip>
|
||||
</tr-info-title>
|
||||
<code>[[ link.link ]]</code>
|
||||
</tr-info-row>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="dbInbound.isSS && !inbound.isSSMultiUser">
|
||||
<a-divider>URL</a-divider>
|
||||
<a-row v-for="(link,index) in infoModal.links">
|
||||
<a-col :span="22"><a-tag color="green">[[ link.remark ]]</a-tag><br />[[ link.link ]]</a-col>
|
||||
<a-col :span="2" style="text-align: right;">
|
||||
<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 v-if="dbInbound.isSS && !inbound.isSSMultiUser">
|
||||
<a-divider>URL</a-divider>
|
||||
<tr-info-row v-for="(link,index) in infoModal.links" class="tr-info-row">
|
||||
<tr-info-title class="tr-info-title">
|
||||
<a-tag class="tr-info-tag" color="green">[[ link.remark ]]</a-tag>
|
||||
<a-tooltip title='{{ i18n "copy" }}'>
|
||||
<a-button style="min-width: 24px;" size="small" icon="snippets" :id="'copy-url-link-'+index" @click="copyToClipboard('copy-url-link-'+index, link.link)"></a-button>
|
||||
</a-tooltip>
|
||||
</tr-info-title>
|
||||
<code>[[ link.link ]]</code>
|
||||
</tr-info-row>
|
||||
</template>
|
||||
<table v-if="inbound.protocol == Protocols.DOKODEMO" class="tr-info-table">
|
||||
<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="green">[[ inbound.settings.port ]]</a-tag>
|
||||
</td>
|
||||
<td>
|
||||
<a-tag color="green">[[ inbound.settings.network ]]</a-tag>
|
||||
</td>
|
||||
<td>
|
||||
<a-tag color="green">[[ inbound.settings.followRedirect ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table v-if="dbInbound.isSocks" class="tr-info-table">
|
||||
<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="green">[[ inbound.settings.udp]]</a-tag>
|
||||
</td>
|
||||
<td>
|
||||
<a-tag color="green">[[ inbound.settings.ip ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<template v-if="inbound.settings.auth == 'password'">
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>{{ i18n "username" }}</td>
|
||||
<td>{{ i18n "password" }}</td>
|
||||
</tr>
|
||||
<tr v-for="account,index in inbound.settings.accounts">
|
||||
<td>[[ index ]]</td>
|
||||
<td>
|
||||
<a-tag color="green">[[ account.user ]]</a-tag>
|
||||
</td>
|
||||
<td>
|
||||
<a-tag color="green">[[ account.pass ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<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="green">[[ inbound.settings.port ]]</a-tag></td>
|
||||
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
|
||||
<td><a-tag color="green">[[ inbound.settings.followRedirect ]]</a-tag></td>
|
||||
</tr>
|
||||
</table>
|
||||
<table v-if="dbInbound.isSocks" 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="green">[[ inbound.settings.udp]]</a-tag></td>
|
||||
<td><a-tag color="green">[[ inbound.settings.ip ]]</a-tag></td>
|
||||
</tr>
|
||||
<template v-if="inbound.settings.auth == 'password'">
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td>{{ i18n "username" }}</td>
|
||||
<td>{{ i18n "password" }}</td>
|
||||
</tr><tr v-for="account,index in inbound.settings.accounts">
|
||||
<td>[[ index ]]</td>
|
||||
<td><a-tag color="green">[[ account.user ]]</a-tag></td>
|
||||
<td><a-tag color="green">[[ account.pass ]]</a-tag></td>
|
||||
</tr>
|
||||
</template>
|
||||
</table>
|
||||
<table v-if="dbInbound.isHTTP" 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>[[ index ]]</td>
|
||||
<td><a-tag color="green">[[ account.user ]]</a-tag></td>
|
||||
<td><a-tag color="green">[[ account.pass ]]</a-tag></td>
|
||||
</tr>
|
||||
</table>
|
||||
<table v-if="dbInbound.isWireguard" style="margin-bottom: 10px; width: 100%;">
|
||||
<tr class="client-table-odd-row">
|
||||
<td>{{ i18n "pages.xray.wireguard.secretKey" }}</td>
|
||||
<td>[[ inbound.settings.secretKey ]]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.xray.wireguard.publicKey" }}</td>
|
||||
<td>[[ inbound.settings.pubKey ]]</td>
|
||||
</tr>
|
||||
<tr class="client-table-odd-row">
|
||||
<td>MTU</td>
|
||||
<td>[[ inbound.settings.mtu ]]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kernel Mode</td>
|
||||
<td>[[ inbound.settings.kernelMode ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table v-if="dbInbound.isHTTP" class="tr-info-table">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{{ i18n "username" }}</th>
|
||||
<th>{{ i18n "password" }}</th>
|
||||
</tr>
|
||||
<tr v-for="account,index in inbound.settings.accounts">
|
||||
<td>[[ index ]]</td>
|
||||
<td>
|
||||
<a-tag color="green">[[ account.user ]]</a-tag>
|
||||
</td>
|
||||
<td>
|
||||
<a-tag color="green">[[ account.pass ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table v-if="dbInbound.isWireguard" class="tr-info-table">
|
||||
<tr class="client-table-odd-row">
|
||||
<td>{{ i18n "pages.xray.wireguard.secretKey" }}</td>
|
||||
<td>[[ inbound.settings.secretKey ]]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.xray.wireguard.publicKey" }}</td>
|
||||
<td>[[ inbound.settings.pubKey ]]</td>
|
||||
</tr>
|
||||
<tr class="client-table-odd-row">
|
||||
<td>MTU</td>
|
||||
<td>[[ inbound.settings.mtu ]]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kernel Mode</td>
|
||||
<td>[[ inbound.settings.kernelMode ]]</td>
|
||||
</tr>
|
||||
<template v-for="(peer, index) in inbound.settings.peers">
|
||||
<tr>
|
||||
<td colspan="2"><a-divider>Peer [[ index + 1 ]]</a-divider></td>
|
||||
</tr>
|
||||
<tr class="client-table-odd-row">
|
||||
<td>{{ i18n "pages.xray.wireguard.secretKey" }}</td>
|
||||
<td>[[ peer.privateKey ]]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.xray.wireguard.publicKey" }}</td>
|
||||
<td>[[ peer.publicKey ]]</td>
|
||||
</tr>
|
||||
<tr class="client-table-odd-row">
|
||||
<td>{{ i18n "pages.xray.wireguard.psk" }}</td>
|
||||
<td>[[ peer.psk ]]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.xray.wireguard.allowedIPs" }}</td>
|
||||
<td>[[ peer.allowedIPs.join(",") ]]</td>
|
||||
</tr>
|
||||
<tr class="client-table-odd-row">
|
||||
<td>Keep Alive</td>
|
||||
<td>[[ peer.keepAlive ]]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<a-row>
|
||||
<a-col :span="22" style="overflow-wrap: anywhere;">
|
||||
<a-tag color="blue">Config</a-tag>
|
||||
<div
|
||||
v-html="infoModal.links[index].replaceAll(`\n`,`<br />`)"
|
||||
style="border-radius: 1rem; padding: 0.5rem;"
|
||||
class="client-table-odd-row"></div>
|
||||
</a-col>
|
||||
<a-col :span="2" style="text-align: right;">
|
||||
<a-tooltip title='{{ i18n "copy" }}'>
|
||||
<button class="ant-btn ant-btn-primary"
|
||||
:id="'copy-url-link-'+index"
|
||||
@click="copyToClipboard('copy-url-link-'+index, infoModal.links[index])">
|
||||
<a-icon type="snippets"></a-icon>
|
||||
</button>
|
||||
</a-tooltip>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<a-divider>Peer [[ index + 1 ]]</a-divider>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="client-table-odd-row">
|
||||
<td>{{ i18n "pages.xray.wireguard.secretKey" }}</td>
|
||||
<td>[[ peer.privateKey ]]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.xray.wireguard.publicKey" }}</td>
|
||||
<td>[[ peer.publicKey ]]</td>
|
||||
</tr>
|
||||
<tr class="client-table-odd-row">
|
||||
<td>{{ i18n "pages.xray.wireguard.psk" }}</td>
|
||||
<td>[[ peer.psk ]]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.xray.wireguard.allowedIPs" }}</td>
|
||||
<td>[[ peer.allowedIPs.join(",") ]]</td>
|
||||
</tr>
|
||||
<tr class="client-table-odd-row">
|
||||
<td>Keep Alive</td>
|
||||
<td>[[ peer.keepAlive ]]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<tr-info-row v-for="(link,index) in infoModal.links" class="tr-info-row">
|
||||
<tr-info-title class="tr-info-title">
|
||||
<a-tag color="blue">Config</a-tag>
|
||||
<a-tooltip title='{{ i18n "copy" }}'>
|
||||
<a-button style="min-width: 24px;" size="small" icon="snippets" :id="'copy-url-link-'+index" @click="copyToClipboard('copy-url-link-'+index, infoModal.links[index])"></a-button>
|
||||
</a-tooltip>
|
||||
</tr-info-title>
|
||||
<div v-html="infoModal.links[index].replaceAll(`\n`,`<br />`)" style="border-radius: 1rem; padding: 0.5rem;" class="client-table-odd-row">
|
||||
</div>
|
||||
</tr-info-row>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
</template>
|
||||
</a-modal>
|
||||
<script>
|
||||
const infoModal = {
|
||||
visible: false,
|
||||
inbound: new Inbound(),
|
||||
dbInbound: new DBInbound(),
|
||||
clientSettings: null,
|
||||
clientStats: [],
|
||||
upStats: 0,
|
||||
downStats: 0,
|
||||
clipboard: null,
|
||||
links: [],
|
||||
index: null,
|
||||
isExpired: false,
|
||||
subLink: '',
|
||||
subJsonLink: '',
|
||||
show(dbInbound, index) {
|
||||
this.index = index;
|
||||
this.inbound = dbInbound.toInbound();
|
||||
this.dbInbound = new DBInbound(dbInbound);
|
||||
this.clientSettings = this.inbound.clients ? this.inbound.clients[index] : null;
|
||||
this.isExpired = this.inbound.clients ? this.inbound.isExpiry(index): this.dbInbound.isExpiry;
|
||||
this.clientStats = this.inbound.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
|
||||
if (this.inbound.protocol == Protocols.WIREGUARD){
|
||||
this.links = this.inbound.genInboundLinks(dbInbound.remark).split('\r\n')
|
||||
} else {
|
||||
this.links = this.inbound.genAllLinks(this.dbInbound.remark, app.remarkModel, this.clientSettings);
|
||||
}
|
||||
if (this.clientSettings) {
|
||||
if (this.clientSettings.subId) {
|
||||
this.subLink = this.genSubLink(this.clientSettings.subId);
|
||||
this.subJsonLink = this.genSubJsonLink(this.clientSettings.subId);
|
||||
}
|
||||
}
|
||||
this.visible = true;
|
||||
},
|
||||
close() {
|
||||
infoModal.visible = false;
|
||||
},
|
||||
genSubLink(subID) {
|
||||
return app.subSettings.subURI+subID;
|
||||
},
|
||||
genSubJsonLink(subID) {
|
||||
return app.subSettings.subJsonURI+subID;
|
||||
const infoModal = {
|
||||
visible: false,
|
||||
inbound: new Inbound(),
|
||||
dbInbound: new DBInbound(),
|
||||
clientSettings: null,
|
||||
clientStats: [],
|
||||
upStats: 0,
|
||||
downStats: 0,
|
||||
clipboard: null,
|
||||
links: [],
|
||||
index: null,
|
||||
isExpired: false,
|
||||
subLink: '',
|
||||
subJsonLink: '',
|
||||
show(dbInbound, index) {
|
||||
this.index = index;
|
||||
this.inbound = dbInbound.toInbound();
|
||||
this.dbInbound = new DBInbound(dbInbound);
|
||||
this.clientSettings = this.inbound.clients ? this.inbound.clients[index] : null;
|
||||
this.isExpired = this.inbound.clients ? this.inbound.isExpiry(index) : this.dbInbound.isExpiry;
|
||||
this.clientStats = this.inbound.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
|
||||
if (this.inbound.protocol == Protocols.WIREGUARD) {
|
||||
this.links = this.inbound.genInboundLinks(dbInbound.remark).split('\r\n')
|
||||
} else {
|
||||
this.links = this.inbound.genAllLinks(this.dbInbound.remark, app.remarkModel, this.clientSettings);
|
||||
}
|
||||
if (this.clientSettings) {
|
||||
if (this.clientSettings.subId) {
|
||||
this.subLink = this.genSubLink(this.clientSettings.subId);
|
||||
this.subJsonLink = this.genSubJsonLink(this.clientSettings.subId);
|
||||
}
|
||||
};
|
||||
|
||||
const infoModalApp = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#inbound-info-modal',
|
||||
data: {
|
||||
infoModal,
|
||||
get dbInbound() {
|
||||
return this.infoModal.dbInbound;
|
||||
},
|
||||
get inbound() {
|
||||
return this.infoModal.inbound;
|
||||
},
|
||||
get isActive() {
|
||||
if(infoModal.clientStats){
|
||||
return infoModal.clientStats.enable;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
get isEnable() {
|
||||
if(infoModal.clientSettings){
|
||||
return infoModal.clientSettings.enable;
|
||||
}
|
||||
return infoModal.dbInbound.isEnable;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
copyToClipboard(elmentId,content) {
|
||||
this.infoModal.clipboard = new ClipboardJS('#' + elmentId, {
|
||||
text: () => content,
|
||||
});
|
||||
this.infoModal.clipboard.on('success', () => {
|
||||
app.$message.success('{{ i18n "copied" }}')
|
||||
this.infoModal.clipboard.destroy();
|
||||
});
|
||||
},
|
||||
statsColor(stats) {
|
||||
return usageColor(stats.up + stats.down, app.trafficDiff, stats.total);
|
||||
},
|
||||
getRemStats() {
|
||||
remained = this.infoModal.clientStats.total - this.infoModal.clientStats.up - this.infoModal.clientStats.down;
|
||||
return remained>0 ? sizeFormat(remained) : '-';
|
||||
},
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
this.visible = true;
|
||||
},
|
||||
close() {
|
||||
infoModal.visible = false;
|
||||
},
|
||||
genSubLink(subID) {
|
||||
return app.subSettings.subURI + subID;
|
||||
},
|
||||
genSubJsonLink(subID) {
|
||||
return app.subSettings.subJsonURI + subID;
|
||||
}
|
||||
};
|
||||
const infoModalApp = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#inbound-info-modal',
|
||||
data: {
|
||||
infoModal,
|
||||
get dbInbound() {
|
||||
return this.infoModal.dbInbound;
|
||||
},
|
||||
get inbound() {
|
||||
return this.infoModal.inbound;
|
||||
},
|
||||
get isActive() {
|
||||
if (infoModal.clientStats) {
|
||||
return infoModal.clientStats.enable;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
get isEnable() {
|
||||
if (infoModal.clientSettings) {
|
||||
return infoModal.clientSettings.enable;
|
||||
}
|
||||
return infoModal.dbInbound.isEnable;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
copyToClipboard(elementId, content) {
|
||||
this.infoModal.clipboard = new ClipboardJS('#' + elementId, {
|
||||
text: () => content,
|
||||
});
|
||||
this.infoModal.clipboard.on('success', () => {
|
||||
app.$message.success('{{ i18n "copied" }}')
|
||||
this.infoModal.clipboard.destroy();
|
||||
});
|
||||
},
|
||||
statsColor(stats) {
|
||||
return usageColor(stats.up + stats.down, app.trafficDiff, stats.total);
|
||||
},
|
||||
getRemStats() {
|
||||
remained = this.infoModal.clientStats.total - this.infoModal.clientStats.up - this.infoModal.clientStats.down;
|
||||
return remained > 0 ? sizeFormat(remained) : '-';
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
@@ -2,6 +2,34 @@
|
||||
<html lang="en">
|
||||
{{template "head" .}}
|
||||
<style>
|
||||
.ant-table:not(.ant-table-expanded-row .ant-table) {
|
||||
outline: 1px solid #f0f0f0;
|
||||
outline-offset: -1px;
|
||||
border-radius: 1rem;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.dark .ant-table:not(.ant-table-expanded-row .ant-table) {
|
||||
outline-color: var(--dark-color-table-ring);
|
||||
}
|
||||
.ant-table .ant-table-content .ant-table-scroll .ant-table-body {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
.ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper {
|
||||
margin:-10px 22px -10px !important;
|
||||
}
|
||||
.ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper .ant-table {
|
||||
border-bottom-left-radius: 1rem;
|
||||
border-bottom-right-radius: 1rem;
|
||||
}
|
||||
.ant-table .ant-table-content .ant-table-tbody tr:last-child tr:last-child td {
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
.ant-table .ant-table-tbody tr:last-child.ant-table-expanded-row .ant-table-wrapper .ant-table-tbody>tr:last-child>td:first-child {
|
||||
border-bottom-left-radius: 6px;
|
||||
}
|
||||
.ant-table .ant-table-tbody tr:last-child.ant-table-expanded-row .ant-table-wrapper .ant-table-tbody>tr:last-child>td:last-child {
|
||||
border-bottom-right-radius: 6px;
|
||||
}
|
||||
@media (min-width: 769px) {
|
||||
.ant-layout-content {
|
||||
margin: 24px 16px;
|
||||
@@ -11,6 +39,9 @@
|
||||
.ant-card-body {
|
||||
padding: .5rem;
|
||||
}
|
||||
.ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper {
|
||||
margin:-10px 2px -10px !important;
|
||||
}
|
||||
}
|
||||
.ant-col-sm-24 {
|
||||
margin: 0.5rem -2rem 0.5rem 2rem;
|
||||
@@ -22,13 +53,14 @@
|
||||
padding: 0 5px;
|
||||
border-radius: 2rem;
|
||||
min-width: 50px;
|
||||
min-height: 22px;
|
||||
}
|
||||
.infinite-bar .ant-progress-inner .ant-progress-bg {
|
||||
background-color: #F2EAF1;
|
||||
border: #D5BED2 solid 1px;
|
||||
}
|
||||
.dark .infinite-bar .ant-progress-inner .ant-progress-bg {
|
||||
background-color: #7a316f;
|
||||
background-color: #7a316f !important;
|
||||
border: #7a316f solid 1px;
|
||||
}
|
||||
.ant-collapse {
|
||||
@@ -53,6 +85,41 @@
|
||||
opacity: .2;
|
||||
}
|
||||
}
|
||||
.tr-table-box {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.tr-table-rt {
|
||||
flex-basis: 70px;
|
||||
min-width: 70px;
|
||||
text-align: end;
|
||||
}
|
||||
.tr-table-lt {
|
||||
flex-basis: 70px;
|
||||
min-width: 70px;
|
||||
text-align: start;
|
||||
}
|
||||
.tr-table-bar {
|
||||
flex-basis: 160px;
|
||||
min-width: 60px;
|
||||
}
|
||||
.tr-infinity-ch {
|
||||
font-size: 14pt;
|
||||
max-height: 24px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
.ant-table-expanded-row .ant-table .ant-table-body {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.ant-table-expanded-row .ant-table-tbody>tr>td {
|
||||
padding: 10px 2px;
|
||||
}
|
||||
.ant-table-expanded-row .ant-table-thead>tr>th {
|
||||
padding: 12px 2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
@@ -324,8 +391,8 @@
|
||||
[[ sizeFormat(dbInbound.total) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
<svg style="fill: currentColor; height: 10px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512">
|
||||
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" />
|
||||
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
|
||||
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
|
||||
</svg>
|
||||
</template>
|
||||
</a-tag>
|
||||
@@ -336,14 +403,21 @@
|
||||
</template>
|
||||
<template slot="expiryTime" slot-scope="text, dbInbound">
|
||||
<a-popover v-if="dbInbound.expiryTime > 0" :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<template slot="content" v-if="app.datepicker === 'gregorian'">
|
||||
[[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
|
||||
</template>
|
||||
<template v-else slot="content">
|
||||
[[ DateUtil.convertToJalalian(moment(dbInbound.expiryTime)) ]]
|
||||
</template>
|
||||
<a-tag style="min-width: 50px;" :color="usageColor(new Date().getTime(), app.expireDiff, dbInbound._expiryTime)">
|
||||
[[ remainedDays(dbInbound._expiryTime) ]]
|
||||
</a-tag>
|
||||
</a-popover>
|
||||
<a-tag v-else color="purple" class="infinite-tag">∞</a-tag>
|
||||
<a-tag v-else color="purple" class="infinite-tag">
|
||||
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
|
||||
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
|
||||
</svg>
|
||||
</a-tag>
|
||||
</template>
|
||||
<template slot="info" slot-scope="text, dbInbound">
|
||||
<a-popover placement="bottomRight" :overlay-class-name="themeSwitcher.currentTheme" trigger="click">
|
||||
@@ -415,7 +489,11 @@
|
||||
<template v-if="dbInbound.total > 0">
|
||||
[[ sizeFormat(dbInbound.total) ]]
|
||||
</template>
|
||||
<template v-else>∞</template>
|
||||
<template v-else>
|
||||
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
|
||||
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
|
||||
</svg>
|
||||
</template>
|
||||
</a-tag>
|
||||
</a-popover>
|
||||
</td>
|
||||
@@ -423,10 +501,20 @@
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.expireDate" }}</td>
|
||||
<td>
|
||||
<a-tag style="min-width: 50px; text-align: center;" v-if="dbInbound.expiryTime > 0" :color="dbInbound.isExpiry? 'red': 'blue'">
|
||||
[[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
|
||||
<a-tag style="min-width: 50px; text-align: center;" v-if="dbInbound.expiryTime > 0"
|
||||
:color="dbInbound.isExpiry? 'red': 'blue'">
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
[[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[ DateUtil.convertToJalalian(moment(dbInbound.expiryTime)) ]]
|
||||
</template>
|
||||
</a-tag>
|
||||
<a-tag v-else style="text-align: center;" color="purple" class="infinite-tag">
|
||||
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
|
||||
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
|
||||
</svg>
|
||||
</a-tag>
|
||||
<a-tag v-else style="text-align: center;" color="purple" class="infinite-tag">∞</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -445,7 +533,7 @@
|
||||
:columns="isMobile ? innerMobileColumns : innerColumns"
|
||||
:data-source="getInboundClients(record)"
|
||||
:pagination=pagination(getInboundClients(record))
|
||||
:style="isMobile ? 'margin: -12px 2px -13px;' : 'margin: -12px 22px -13px;'">
|
||||
:style="isMobile ? 'margin: -10px 2px -11px;' : 'margin: -10px 22px -11px;'">
|
||||
{{template "client_table"}}
|
||||
</a-table>
|
||||
</template>
|
||||
@@ -543,7 +631,7 @@
|
||||
{ title: '{{ i18n "online" }}', width: 30, scopedSlots: { customRender: 'online' } },
|
||||
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
|
||||
{ title: '{{ i18n "pages.inbounds.traffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'traffic' } },
|
||||
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 100, align: 'center', scopedSlots: { customRender: 'expiryTime' } },
|
||||
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, align: 'center', scopedSlots: { customRender: 'expiryTime' } },
|
||||
];
|
||||
|
||||
const innerMobileColumns = [
|
||||
@@ -585,7 +673,7 @@
|
||||
tgBotEnable: false,
|
||||
showAlert: false,
|
||||
ipLimitEnable: false,
|
||||
pageSize: 0,
|
||||
pageSize: 50,
|
||||
isMobile: window.innerWidth <= 768,
|
||||
},
|
||||
methods: {
|
||||
@@ -645,7 +733,7 @@
|
||||
this.inbounds.push(to_inbound);
|
||||
this.dbInbounds.push(dbInbound);
|
||||
if ([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(inbound.protocol)) {
|
||||
if (inbound.protocol === Protocols.SHADOWSOCKS && (!to_inbound.isSSMultiUser)) {
|
||||
if (dbInbound.isSS && (!to_inbound.isSSMultiUser)) {
|
||||
continue;
|
||||
}
|
||||
this.clientCount[inbound.id] = this.getClientCounts(inbound, to_inbound);
|
||||
@@ -847,6 +935,7 @@
|
||||
settings: Inbound.Settings.getSettings(baseInbound.protocol).toString(),
|
||||
streamSettings: baseInbound.stream.toString(),
|
||||
sniffing: baseInbound.sniffing.toString(),
|
||||
allocate: baseInbound.allocate.toString(),
|
||||
};
|
||||
await this.submit('/panel/inbound/add', data, inModal);
|
||||
},
|
||||
@@ -892,6 +981,7 @@
|
||||
};
|
||||
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
|
||||
data.sniffing = inbound.sniffing.toString();
|
||||
data.allocate = inbound.allocate.toString();
|
||||
|
||||
await this.submit('/panel/inbound/add', data, inModal);
|
||||
},
|
||||
@@ -911,6 +1001,7 @@
|
||||
};
|
||||
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
|
||||
data.sniffing = inbound.sniffing.toString();
|
||||
data.allocate = inbound.allocate.toString();
|
||||
|
||||
await this.submit(`/panel/inbound/update/${dbInbound.id}`, data, inModal);
|
||||
},
|
||||
@@ -979,7 +1070,7 @@
|
||||
resetTraffic(dbInboundId) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
|
||||
title: '{{ i18n "pages.inbounds.resetTraffic"}}' + ' #' + dbInboundId,
|
||||
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
|
||||
class: themeSwitcher.currentTheme,
|
||||
okText: '{{ i18n "reset"}}',
|
||||
@@ -1062,9 +1153,9 @@
|
||||
infoModal.show(newDbInbound, index);
|
||||
},
|
||||
switchEnable(dbInboundId,state) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
dbInbound.enable = state;
|
||||
this.submit(`/panel/inbound/update/${dbInboundId}`, dbInbound);
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
dbInbound.enable = state;
|
||||
this.submit(`/panel/inbound/update/${dbInboundId}`, dbInbound);
|
||||
},
|
||||
async switchEnableClient(dbInboundId, client) {
|
||||
this.loading()
|
||||
@@ -1089,7 +1180,7 @@
|
||||
resetClientTraffic(client, dbInboundId, confirmation = true) {
|
||||
if (confirmation){
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
|
||||
title: '{{ i18n "pages.inbounds.resetTraffic"}}' + ' ' + client.email,
|
||||
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
|
||||
class: themeSwitcher.currentTheme,
|
||||
okText: '{{ i18n "reset"}}',
|
||||
@@ -1206,12 +1297,12 @@
|
||||
return this.onlineClients.includes(email);
|
||||
},
|
||||
isRemovable(dbInboundId) {
|
||||
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInboundId)).length > 1
|
||||
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInboundId)).length > 1;
|
||||
},
|
||||
inboundLinks(dbInboundId) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
newDbInbound = this.checkFallback(dbInbound);
|
||||
txtModal.show('{{ i18n "pages.inbounds.export"}}', newDbInbound.genInboundLinks(), newDbInbound.remark);
|
||||
txtModal.show('{{ i18n "pages.inbounds.export"}}', newDbInbound.genInboundLinks(this.remarkModel), newDbInbound.remark);
|
||||
},
|
||||
exportSubs(dbInboundId) {
|
||||
const dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
|
||||
@@ -2,20 +2,23 @@
|
||||
<html lang="en">
|
||||
{{template "head" .}}
|
||||
<style>
|
||||
@media (min-width: 769px) {
|
||||
.ant-layout-content {
|
||||
margin: 24px 16px;
|
||||
}
|
||||
.ant-card-hoverable {
|
||||
margin-inline: 0.3rem;
|
||||
}
|
||||
@media (min-width: 769px) {
|
||||
.ant-layout-content {
|
||||
margin: 24px 16px;
|
||||
}
|
||||
.ant-col-sm-24 {
|
||||
margin-top: 10px;
|
||||
.ant-card-hoverable {
|
||||
margin-inline: 0.3rem;
|
||||
}
|
||||
.ant-card-dark h2 {
|
||||
color: var(--dark-color-text-primary);
|
||||
.ant-alert-error {
|
||||
margin-inline: 0.3rem;
|
||||
}
|
||||
}
|
||||
.ant-col-sm-24 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.ant-card-dark h2 {
|
||||
color: var(--dark-color-text-primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
@@ -42,8 +45,13 @@
|
||||
<a-progress type="dashboard" status="normal"
|
||||
:stroke-color="status.cpu.color"
|
||||
:percent="status.cpu.percent"></a-progress>
|
||||
<div><b>CPU:</b> [[ cpuCoreFormat(status.cpuCores) ]]</div>
|
||||
<div><b>Speed:</b> [[ cpuSpeedFormat(status.cpuSpeedMhz) ]]</div>
|
||||
<div><b>CPU:</b> [[ cpuCoreFormat(status.cpuCores) ]] <a-tooltip>
|
||||
<a-icon type="area-chart"></a-icon>
|
||||
<template slot="title">
|
||||
<div><b>Logical Processors:</b> [[ (status.logicalPro) ]]</div>
|
||||
<div><b>Speed:</b> [[ cpuSpeedFormat(status.cpuSpeedMhz) ]]</div>
|
||||
</template>
|
||||
</a-tooltip></div>
|
||||
</a-col>
|
||||
<a-col :span="12" style="text-align: center">
|
||||
<a-progress type="dashboard" status="normal"
|
||||
@@ -364,6 +372,7 @@
|
||||
constructor(data) {
|
||||
this.cpu = new CurTotal(0, 0);
|
||||
this.cpuCores = 0;
|
||||
this.logicalPro = 0;
|
||||
this.cpuSpeedMhz = 0;
|
||||
this.disk = new CurTotal(0, 0);
|
||||
this.loads = [0, 0, 0];
|
||||
@@ -384,6 +393,7 @@
|
||||
}
|
||||
this.cpu = new CurTotal(data.cpu, 100);
|
||||
this.cpuCores = data.cpuCores;
|
||||
this.logicalPro = data.logicalPro;
|
||||
this.cpuSpeedMhz = data.cpuSpeedMhz;
|
||||
this.disk = new CurTotal(data.disk.current, data.disk.total);
|
||||
this.loads = data.loads.map(load => toFixed(load, 2));
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.publicKeyPath"}}' desc='{{ i18n "pages.settings.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.privateKeyPath"}}' desc='{{ i18n "pages.settings.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.panelUrlPath"}}' desc='{{ i18n "pages.settings.panelUrlPathDesc"}}' v-model="allSetting.webBasePath"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.sessionMaxAge" }}' desc='{{ i18n "pages.settings.sessionMaxAgeDesc" }}' v-model="allSetting.sessionMaxAge" :min="0"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.sessionMaxAge" }}' desc='{{ i18n "pages.settings.sessionMaxAgeDesc" }}' v-model="allSetting.sessionMaxAge" :min="60"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.pageSize" }}' desc='{{ i18n "pages.settings.pageSizeDesc" }}' v-model="allSetting.pageSize" :min="0" :step="5"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.expireTimeDiff" }}' desc='{{ i18n "pages.settings.expireTimeDiffDesc" }}' v-model="allSetting.expireDiff" :min="0"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.trafficDiff" }}' desc='{{ i18n "pages.settings.trafficDiffDesc" }}' v-model="allSetting.trafficDiff" :min="0"></setting-list-item>
|
||||
@@ -296,23 +296,48 @@
|
||||
</a-row>
|
||||
<a-collapse v-if="fragment" style="margin-top: 14px;">
|
||||
<a-collapse-panel header='{{ i18n "pages.settings.fragmentSett"}}' v-if="fragment">
|
||||
<a-list-item style="padding: 10px 20px">
|
||||
<a-row>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title='Packets'></a-list-item-meta>
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-select v-model="fragmentPackets" style="width: 100%" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option :value="p" :label="p" v-for="p in ['1-1', '1-3', 'tlshello']"> [[ p ]] </a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
<setting-list-item style="padding: 10px 20px" type="text" title='Packets' v-model="fragmentPackets" placeholder="1-1 | 1-3 | tlshello | ..."></setting-list-item>
|
||||
<setting-list-item style="padding: 10px 20px" type="text" title='Length' v-model="fragmentLength" placeholder="100-200"></setting-list-item>
|
||||
<setting-list-item style="padding: 10px 20px" type="text" title='Interval' v-model="fragmentInterval" placeholder="10-20"></setting-list-item>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</a-list-item>
|
||||
<a-list-item style="padding: 20px">
|
||||
<a-row>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title='Noises'>
|
||||
<template slot="description">{{ i18n "pages.settings.noisesDesc"}}</template>
|
||||
</a-list-item-meta>
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-switch v-model="noises"></a-switch>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-collapse v-if="noises" style="margin-top: 14px;">
|
||||
<a-collapse-panel v-for="(noise, index) in noisesArray" :key="index" :header="`Noise ${index + 1}`">
|
||||
<a-list-item style="padding: 10px 20px">
|
||||
<a-row>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title='Type'></a-list-item-meta>
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-select :value="noise.type" style="width: 100%" :dropdown-class-name="themeSwitcher.currentTheme"
|
||||
@change="(value) => updateNoiseType(index, value)">
|
||||
<a-select-option :value="p" :label="p" v-for="p in ['rand', 'base64', 'str']" :key="p">
|
||||
[[ p ]] </a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
<setting-list-item style="padding: 10px 20px" type="text" title='Packet' :value="noise.packet"
|
||||
@input="(value) => updateNoisePacket(index, value)" placeholder="5-10"></setting-list-item>
|
||||
<setting-list-item style="padding: 10px 20px" type="text" title='Delay (ms)' :value="noise.delay"
|
||||
@input="(value) => updateNoiseDelay(index, value)" placeholder="10-20"></setting-list-item>
|
||||
<a-button v-if="noisesArray.length > 1" type="danger" @click="removeNoise(index)">Remove</a-button>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<a-button v-if="noises" type="primary" @click="addNoise" style="margin-top: 10px">Add Noise</a-button>
|
||||
</a-list-item>
|
||||
<a-list-item style="padding: 20px">
|
||||
<a-row>
|
||||
<a-col :lg="24" :xl="12">
|
||||
@@ -355,9 +380,14 @@
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-collapse v-if="enableDirect" style="margin-top: 14px;">
|
||||
<a-collapse-panel header='{{ i18n "pages.settings.directSett"}}'>
|
||||
<a-collapse-panel header='{{ i18n "pages.xray.directips"}}'>
|
||||
<a-list-item style="padding: 10px 20px">
|
||||
<a-checkbox-group v-model="directCountries" name="Countries" :options="countryOptions"></a-checkbox-group>
|
||||
<a-checkbox-group v-model="geoIP" :options="geoIPOptions"></a-checkbox-group>
|
||||
</a-list-item>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel header='{{ i18n "pages.xray.directdomains"}}'>
|
||||
<a-list-item style="padding: 10px 20px">
|
||||
<a-checkbox-group v-model="geoSite" :options="geoSiteOptions"></a-checkbox-group>
|
||||
</a-list-item>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
@@ -407,10 +437,21 @@
|
||||
streamSettings: {
|
||||
sockopt: {
|
||||
tcpKeepAliveIdle: 100,
|
||||
tcpMptcp: true,
|
||||
tcpNoDelay: true
|
||||
}
|
||||
}
|
||||
},
|
||||
defaultNoises: {
|
||||
tag: "noises",
|
||||
protocol: "freedom",
|
||||
settings: {
|
||||
domainStrategy: "AsIs",
|
||||
noises: [
|
||||
{ type: "rand", packet: "10-20", delay: "10-16" },
|
||||
],
|
||||
},
|
||||
},
|
||||
defaultMux: {
|
||||
enabled: true,
|
||||
concurrency: 8,
|
||||
@@ -422,27 +463,37 @@
|
||||
type: "field",
|
||||
outboundTag: "direct",
|
||||
domain: [
|
||||
"geosite:category-ir",
|
||||
"geosite:cn"
|
||||
],
|
||||
"enabled": true
|
||||
"geosite:category-ir"
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "field",
|
||||
outboundTag: "direct",
|
||||
ip: [
|
||||
"geoip:private",
|
||||
"geoip:ir",
|
||||
"geoip:cn"
|
||||
],
|
||||
enabled: true
|
||||
"geoip:ir"
|
||||
]
|
||||
},
|
||||
],
|
||||
countryOptions: [
|
||||
{ label: 'Private IP/Domain', value: 'private' },
|
||||
geoIPOptions: [
|
||||
{ label: 'Private IP', value: 'private' },
|
||||
{ label: '🇮🇷 Iran', value: 'ir' },
|
||||
{ label: '🇨🇳 China', value: 'cn' },
|
||||
{ label: '🇷🇺 Russia', value: 'ru' },
|
||||
{ label: '🇻🇳 Vietnam', value: 'vn' },
|
||||
{ label: '🇪🇸 Spain', value: 'es' },
|
||||
{ label: '🇮🇩 Indonesia', value: 'id' },
|
||||
{ label: '🇺🇦 Ukraine', value: 'ua' },
|
||||
{ label: '🇹🇷 Türkiye', value: 'tr' },
|
||||
{ label: '🇧🇷 Brazil', value: 'br' },
|
||||
],
|
||||
geoSiteOptions: [
|
||||
{ label: '🇮🇷 Iran', value: 'ir' },
|
||||
{ label: '🇨🇳 China', value: 'cn' },
|
||||
{ label: '🇷🇺 Russia', value: 'ru' },
|
||||
{ label: 'Apple', value: 'apple' },
|
||||
{ label: 'Meta', value: 'meta' },
|
||||
{ label: 'Google', value: 'google' },
|
||||
],
|
||||
get remarkModel() {
|
||||
rm = this.allSetting.remarkModel;
|
||||
@@ -502,6 +553,7 @@
|
||||
this.loading(false);
|
||||
if (msg.success) {
|
||||
this.user = {};
|
||||
window.location.replace(basePath + "logout");
|
||||
}
|
||||
},
|
||||
async restartPanel() {
|
||||
@@ -521,7 +573,9 @@
|
||||
if (msg.success) {
|
||||
this.loading(true);
|
||||
await PromiseUtil.sleep(5000);
|
||||
let { webCertFile, webKeyFile, webDomain: host, webPort: port, webBasePath: base } = this.allSetting;
|
||||
var { webCertFile, webKeyFile, webDomain: host, webPort: port, webBasePath: base } = this.allSetting;
|
||||
if (host == this.oldAllSetting.webDomain) host = null;
|
||||
if (port == this.oldAllSetting.webPort) port = null;
|
||||
const isTLS = webCertFile !== "" || webKeyFile !== "";
|
||||
const url = buildURL({ host, port, isTLS, base, path: "panel/settings" });
|
||||
window.location.replace(url);
|
||||
@@ -569,6 +623,30 @@
|
||||
this.user.loginSecret = "";
|
||||
}
|
||||
},
|
||||
addNoise() {
|
||||
const newNoise = { type: "rand", packet: "10-20", delay: "10-16" };
|
||||
this.noisesArray = [...this.noisesArray, newNoise];
|
||||
},
|
||||
removeNoise(index) {
|
||||
const newNoises = [...this.noisesArray];
|
||||
newNoises.splice(index, 1);
|
||||
this.noisesArray = newNoises;
|
||||
},
|
||||
updateNoiseType(index, value) {
|
||||
const updatedNoises = [...this.noisesArray];
|
||||
updatedNoises[index] = { ...updatedNoises[index], type: value };
|
||||
this.noisesArray = updatedNoises;
|
||||
},
|
||||
updateNoisePacket(index, value) {
|
||||
const updatedNoises = [...this.noisesArray];
|
||||
updatedNoises[index] = { ...updatedNoises[index], packet: value };
|
||||
this.noisesArray = updatedNoises;
|
||||
},
|
||||
updateNoiseDelay(index, value) {
|
||||
const updatedNoises = [...this.noisesArray];
|
||||
updatedNoises[index] = { ...updatedNoises[index], delay: value };
|
||||
this.noisesArray = updatedNoises;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
fragment: {
|
||||
@@ -607,6 +685,30 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
noises: {
|
||||
get() {
|
||||
return this.allSetting?.subJsonNoises != "";
|
||||
},
|
||||
set(v) {
|
||||
if (v) {
|
||||
this.allSetting.subJsonNoises = JSON.stringify(this.defaultNoises);
|
||||
} else {
|
||||
this.allSetting.subJsonNoises = "";
|
||||
}
|
||||
}
|
||||
},
|
||||
noisesArray: {
|
||||
get() {
|
||||
return this.noises ? JSON.parse(this.allSetting.subJsonNoises).settings.noises : [];
|
||||
},
|
||||
set(value) {
|
||||
if (this.noises) {
|
||||
const newNoises = JSON.parse(this.allSetting.subJsonNoises);
|
||||
newNoises.settings.noises = value;
|
||||
this.allSetting.subJsonNoises = JSON.stringify(newNoises);
|
||||
}
|
||||
}
|
||||
},
|
||||
enableMux: {
|
||||
get: function () { return this.allSetting?.subJsonMux != ""; },
|
||||
set: function (v) {
|
||||
@@ -643,29 +745,67 @@
|
||||
this.allSetting.subJsonRules = v ? JSON.stringify(this.defaultRules) : "";
|
||||
}
|
||||
},
|
||||
directCountries: {
|
||||
geoIP: {
|
||||
get: function () {
|
||||
if (!this.enableDirect) return [];
|
||||
rules = JSON.parse(this.allSetting.subJsonRules);
|
||||
return Array.isArray(rules) ? rules[1].ip.map(d => d.replace("geoip:", "")) : [];
|
||||
const rules = JSON.parse(this.allSetting.subJsonRules);
|
||||
if (!Array.isArray(rules)) return [];
|
||||
const ipRule = rules.find(r => r.ip);
|
||||
return ipRule?.ip.map(d => d.replace("geoip:", "")) ?? [];
|
||||
},
|
||||
set: function (v) {
|
||||
rules = JSON.parse(this.allSetting.subJsonRules);
|
||||
let rules = JSON.parse(this.allSetting.subJsonRules);
|
||||
if (!Array.isArray(rules)) return;
|
||||
rules[0].domain = [];
|
||||
rules[1].ip = [];
|
||||
v.forEach(d => {
|
||||
let category = '';
|
||||
if (["cn", "private"].includes(d)) {
|
||||
category = "";
|
||||
} else if (d === 'ru') {
|
||||
category = "category-gov-";
|
||||
} else {
|
||||
category = "category-";
|
||||
|
||||
if (v.length == 0) {
|
||||
rules = rules.filter(r => !r.ip);
|
||||
} else {
|
||||
let ruleIndex = rules.findIndex(r => r.ip);
|
||||
if (ruleIndex == -1) ruleIndex = rules.push(this.defaultRules[1]) - 1;
|
||||
|
||||
rules[ruleIndex].ip = [];
|
||||
v.forEach(d => {
|
||||
rules[ruleIndex].ip.push("geoip:" + d);
|
||||
});
|
||||
}
|
||||
this.allSetting.subJsonRules = JSON.stringify(rules);
|
||||
}
|
||||
},
|
||||
geoSite: {
|
||||
get: function () {
|
||||
if (!this.enableDirect) return [];
|
||||
const rules = JSON.parse(this.allSetting.subJsonRules);
|
||||
if (!Array.isArray(rules)) return [];
|
||||
const domainRule = rules.find(r => r.domain);
|
||||
return domainRule?.domain.map(d => {
|
||||
if (d.startsWith("geosite:category-")) {
|
||||
return d.replace("geosite:category-", "");
|
||||
}
|
||||
rules[0].domain.push("geosite:" + category + d);
|
||||
rules[1].ip.push("geoip:" + d);
|
||||
});
|
||||
return d.replace("geosite:", "");
|
||||
})
|
||||
?? [];
|
||||
},
|
||||
set: function (v) {
|
||||
let rules = JSON.parse(this.allSetting.subJsonRules);
|
||||
if (!Array.isArray(rules)) return;
|
||||
|
||||
if (v.length == 0) {
|
||||
rules = rules.filter(r => !r.domain);
|
||||
} else {
|
||||
let ruleIndex = rules.findIndex(r => r.domain);
|
||||
if (ruleIndex == -1) ruleIndex = rules.push(this.defaultRules[0]) - 1;
|
||||
|
||||
rules[ruleIndex].domain = [];
|
||||
v.forEach(d => {
|
||||
let category = '';
|
||||
if (["cn", "apple", "meta", "google"].includes(d)) {
|
||||
category = "";
|
||||
} else if (["ru", "ir"].includes(d)) {
|
||||
category = "category-";
|
||||
}
|
||||
rules[ruleIndex].domain.push("geosite:" + category + d);
|
||||
});
|
||||
}
|
||||
this.allSetting.subJsonRules = JSON.stringify(rules);
|
||||
}
|
||||
},
|
||||
|
||||