Compare commits

...

28 commits
v2.8.3 ... main

Author SHA1 Message Date
Slava M.
8afa39144e
feat: add file logger support (#3575)
Some checks failed
Release 3X-UI / build (386) (push) Has been cancelled
Release 3X-UI / build (amd64) (push) Has been cancelled
Release 3X-UI / build (arm64) (push) Has been cancelled
Release 3X-UI / build (armv5) (push) Has been cancelled
Release 3X-UI / build (armv6) (push) Has been cancelled
Release 3X-UI / build (armv7) (push) Has been cancelled
Release 3X-UI / build (s390x) (push) Has been cancelled
Release 3X-UI / Build for Windows (push) Has been cancelled
* feat: add backend for file logger
2025-10-09 17:39:29 +02:00
fpointsstar
00baeffe74
Update translate.ru_RU.toml (#3574)
Some checks failed
Release 3X-UI / build (386) (push) Has been cancelled
Release 3X-UI / build (amd64) (push) Has been cancelled
Release 3X-UI / build (arm64) (push) Has been cancelled
Release 3X-UI / build (armv5) (push) Has been cancelled
Release 3X-UI / build (armv6) (push) Has been cancelled
Release 3X-UI / build (armv7) (push) Has been cancelled
Release 3X-UI / build (s390x) (push) Has been cancelled
Release 3X-UI / Build for Windows (push) Has been cancelled
Fix RU translation for login title: replace “Приветствие!” with “Добро пожаловать!” to match English “Welcome”.
2025-10-07 16:31:32 +02:00
mhsanaei
b578a33518
update dependencies 2025-10-07 13:49:08 +02:00
mhsanaei
8153e0ac05
fragment : MaxSplit 2025-10-07 13:46:30 +02:00
mhsanaei
2eb9d2e2e8
DevTools
Some checks failed
Release 3X-UI / build (386) (push) Has been cancelled
Release 3X-UI / build (amd64) (push) Has been cancelled
Release 3X-UI / build (arm64) (push) Has been cancelled
Release 3X-UI / build (armv5) (push) Has been cancelled
Release 3X-UI / build (armv6) (push) Has been cancelled
Release 3X-UI / build (armv7) (push) Has been cancelled
Release 3X-UI / build (s390x) (push) Has been cancelled
Release 3X-UI / Build for Windows (push) Has been cancelled
2025-10-02 01:47:12 +02:00
Vadim Iskuchekov
a824875c4f
fix: improve error handling in periodic traffic reset job (#3572)
Some checks are pending
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run
2025-10-01 23:12:09 +02:00
JieXu
cafcb250ec
Add support for OpenSUSE Leap (#3573)
* Update update.sh

* Update install.sh

* Update x-ui.sh

* Update x-ui.sh
2025-10-01 23:11:37 +02:00
mhsanaei
e7cfee570b
first try native CPU implementation 2025-10-01 20:13:32 +02:00
JieXu
90c3529301
[Security] Replace timestamp-based password generation with random generator (#3571)
* Update x-ui.sh

* Update x-ui.sh

* Update x-ui.sh

* Update x-ui.sh
2025-10-01 18:37:31 +02:00
konstpic
b65ec83c39
fix: fix delete method (#3569)
Some checks failed
Release 3X-UI / build (386) (push) Has been cancelled
Release 3X-UI / build (amd64) (push) Has been cancelled
Release 3X-UI / build (arm64) (push) Has been cancelled
Release 3X-UI / build (armv5) (push) Has been cancelled
Release 3X-UI / build (armv6) (push) Has been cancelled
Release 3X-UI / build (armv7) (push) Has been cancelled
Release 3X-UI / build (s390x) (push) Has been cancelled
Release 3X-UI / Build for Windows (push) Has been cancelled
Co-authored-by: Пичугин Константин <k.pichugin@comagic.dev>
2025-09-29 18:16:46 +02:00
konstpic
28a17a80ec
feat: add ldap component (#3568)
Some checks are pending
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run
* add ldap component

* fix: fix russian comments, tls cert verify default true

* feat: remove replaces go mod for local dev
2025-09-28 21:04:54 +02:00
Mikhail Grigorev
3056583388
feat: Add update script (#3555)
Some checks are pending
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run
* feat: Add update script

* Small fix

* Fixed typo

* Fixed typo

* chmod +x

* Update x-ui

* Fixed update message

* Fixed typo

* Added downloading via IPv4

* Remove check_glibc_version

* Fixed self destroy

* Fixed typo

* Fixed self destroy

---------
2025-09-28 14:09:27 +02:00
Дмитрий Олегович Саенко
172f2ddaa7
fix russian translate in tgbot (#3557)
Some checks failed
Release 3X-UI / build (386) (push) Has been cancelled
Release 3X-UI / build (amd64) (push) Has been cancelled
Release 3X-UI / build (arm64) (push) Has been cancelled
Release 3X-UI / build (armv5) (push) Has been cancelled
Release 3X-UI / build (armv6) (push) Has been cancelled
Release 3X-UI / build (armv7) (push) Has been cancelled
Release 3X-UI / build (s390x) (push) Has been cancelled
Release 3X-UI / Build for Windows (push) Has been cancelled
2025-09-25 15:21:40 +02:00
Tara Rostami
d69af328dc
fix: login animation (#3559)
* Add IPv4 for wget in install

* fix: login animation
2025-09-25 15:16:50 +02:00
mhsanaei
ee0e3093ba
Add IPv4 for wget in install 2025-09-25 15:08:13 +02:00
mhsanaei
89def9aee6
fix
Some checks are pending
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run
2025-09-24 21:30:58 +02:00
mhsanaei
b2b0024648
login: autocomplete password
Some checks are pending
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run
2025-09-24 20:41:32 +02:00
mhsanaei
5822758b7c
tiny changes 2025-09-24 19:51:01 +02:00
mhsanaei
49430b3991
Update docker.yml 2025-09-24 15:42:01 +02:00
mhsanaei
104526aab2
v2.8.4
Some checks are pending
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run
2025-09-24 11:47:43 +02:00
mhsanaei
a0c07241c0
minor changes 2025-09-24 11:47:14 +02:00
mhsanaei
adf3242602
bug fix 2025-09-24 11:44:02 +02:00
mhsanaei
3f62592e4b
API improve security: returns 404 for unauthenticated API requests 2025-09-24 11:29:55 +02:00
Дмитрий Олегович Саенко
02bff4db6c
max port to 65535 (#3536)
Some checks are pending
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run
* add EXPOSE port in Dockerfile

* fix: max port 65 531 -> 65 535

* fix

---------

Co-authored-by: mhsanaei <ho3ein.sanaei@gmail.com>
2025-09-23 19:43:56 +02:00
Happ-dev
8ff4e1ff31
Add Happ client export open link (#3542)
Co-authored-by: y.sivushkin <y.sivushkin@corp.101xp.com>
2025-09-23 16:46:45 +02:00
mhsanaei
26c6438ec2
fix api : subid, uuid from inbound settings
Some checks are pending
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run
2025-09-23 11:52:40 +02:00
Evgeny Volferts
b3e96230c4
Add Alpine Linux support (#3534)
Some checks are pending
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run
* Add Alpine linux support

* Fix for reading logs
2025-09-22 21:56:43 +02:00
mhsanaei
1016f3b4f9
fix: outbound address for vless
Some checks are pending
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run
2025-09-22 00:20:05 +02:00
46 changed files with 1992 additions and 389 deletions

View file

@ -1,7 +1,9 @@
name: Release 3X-UI for Docker
permissions:
contents: read
packages: write
on:
workflow_dispatch:
push:
@ -13,48 +15,48 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
submodules: true
- uses: actions/checkout@v5
with:
submodules: true
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
hsanaeii/3x-ui
ghcr.io/mhsanaei/3x-ui
tags: |
type=ref,event=branch
type=ref,event=tag
type=pep440,pattern={{version}}
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
hsanaeii/3x-ui
ghcr.io/mhsanaei/3x-ui
tags: |
type=ref,event=branch
type=ref,event=tag
type=semver,pattern={{version}}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
install: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
install: true
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64, linux/arm64/v8, linux/arm/v7, linux/arm/v6, linux/386
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6,linux/386
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

49
.vscode/tasks.json vendored
View file

@ -5,36 +5,71 @@
"label": "go: build",
"type": "shell",
"command": "go",
"args": ["build", "-o", "bin/3x-ui.exe", "./main.go"],
"args": [
"build",
"-o",
"bin/3x-ui.exe",
"./main.go"
],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": ["$go"],
"group": { "kind": "build", "isDefault": true }
"problemMatcher": [
"$go"
],
"group": {
"kind": "build",
"isDefault": true
}
},
{
"label": "go: run",
"type": "shell",
"command": "go",
"args": ["run", "./main.go"],
"args": [
"run",
"./main.go"
],
"options": {
"cwd": "${workspaceFolder}",
"env": {
"XUI_DEBUG": "true"
}
},
"problemMatcher": ["$go"]
"problemMatcher": [
"$go"
]
},
{
"label": "go: test",
"type": "shell",
"command": "go",
"args": ["test", "./..."],
"args": [
"test",
"./..."
],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": ["$go"],
"problemMatcher": [
"$go"
],
"group": "test"
},
{
"label": "go: vet",
"type": "shell",
"command": "go",
"args": [
"vet",
"./..."
],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [
"$go"
]
}
]
}

View file

@ -1 +1 @@
2.8.3
2.8.4

20
go.mod
View file

@ -6,6 +6,7 @@ require (
github.com/gin-contrib/gzip v1.2.3
github.com/gin-contrib/sessions v1.0.4
github.com/gin-gonic/gin v1.11.0
github.com/go-ldap/ldap/v3 v3.4.12
github.com/goccy/go-json v0.10.5
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
@ -14,7 +15,7 @@ require (
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pelletier/go-toml/v2 v2.2.4
github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.25.8
github.com/shirou/gopsutil/v4 v4.25.9
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/valyala/fasthttp v1.66.0
github.com/xlzd/gotp v0.1.0
@ -23,12 +24,13 @@ require (
golang.org/x/crypto v0.42.0
golang.org/x/sys v0.36.0
golang.org/x/text v0.29.0
google.golang.org/grpc v1.75.1
google.golang.org/grpc v1.76.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.0
)
require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.1 // indirect
@ -39,10 +41,11 @@ require (
github.com/ebitengine/purego v0.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // 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.27.0 // indirect
github.com/go-playground/validator/v10 v10.28.0 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/gorilla/context v1.1.2 // indirect
@ -67,11 +70,11 @@ require (
github.com/pires/go-proxyproto v0.8.1 // 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.54.0 // indirect
github.com/quic-go/quic-go v0.55.0 // indirect
github.com/refraction-networking/utls v1.8.0 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/sagernet/sing v0.7.10 // indirect
github.com/sagernet/sing v0.7.12 // indirect
github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
@ -83,9 +86,8 @@ require (
github.com/valyala/fastjson v1.6.4 // indirect
github.com/vishvananda/netlink v1.3.1 // indirect
github.com/vishvananda/netns v0.0.5 // indirect
github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c // indirect
github.com/xtls/reality v0.0.0-20251005124704-8f4f0a188196 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.uber.org/mock v0.6.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/arch v0.21.0 // indirect
golang.org/x/mod v0.28.0 // indirect
@ -95,8 +97,8 @@ require (
golang.org/x/tools v0.37.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 // indirect
google.golang.org/protobuf v1.36.9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251006185510-65f7160b3a87 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
lukechampine.com/blake3 v1.4.1 // indirect
)

54
go.sum
View file

@ -1,5 +1,9 @@
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
@ -33,6 +37,10 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@ -46,8 +54,8 @@ 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.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
@ -75,6 +83,20 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
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/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
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=
@ -126,8 +148,8 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt
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.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
github.com/refraction-networking/utls v1.8.0 h1:L38krhiTAyj9EeiQQa2sg+hYb4qwLCqdMcpZrRfbONE=
github.com/refraction-networking/utls v1.8.0/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
@ -136,14 +158,14 @@ 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.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sagernet/sing v0.7.10 h1:2yPhZFx+EkyHPH8hXNezgyRSHyGY12CboId7CtwLROw=
github.com/sagernet/sing v0.7.10/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.7.12 h1:MpMbO56crPRZTbltoj1wGk4Xj9+GiwH1wTO4s3fz1EA=
github.com/sagernet/sing v0.7.12/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM=
github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8=
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.25.8 h1:NnAsw9lN7587WHxjJA9ryDnqhJpFH6A+wagYWTOH970=
github.com/shirou/gopsutil/v4 v4.25.8/go.mod h1:q9QdMmfAOVIw7a+eF86P7ISEU6ka+NLgkUxlopV4RwI=
github.com/shirou/gopsutil/v4 v4.25.9 h1:JImNpf6gCVhKgZhtaAHJ0serfFGtlfIlSC08eaKdTrU=
github.com/shirou/gopsutil/v4 v4.25.9/go.mod h1:gxIxoC+7nQRwUl/xNhutXlD8lq+jxTgpIkEf3rADHL8=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -178,8 +200,8 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c h1:LHLhQY3mKXSpTcQAkjFR4/6ar3rXjQryNeM7khK3AHU=
github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c/go.mod h1:XxvnCCgBee4WWE0bc4E+a7wbk8gkJ/rS0vNVNtC5qp0=
github.com/xtls/reality v0.0.0-20251005124704-8f4f0a188196 h1:jb1y+Rm6UBW/CEV0FehsKlQ/2dnLsQjyUjn3UfWwbic=
github.com/xtls/reality v0.0.0-20251005124704-8f4f0a188196/go.mod h1:XxvnCCgBee4WWE0bc4E+a7wbk8gkJ/rS0vNVNtC5qp0=
github.com/xtls/xray-core v1.250911.0 h1:KMN8zVurAjHFixiUoFV/jwmzYohf27dQRntjV+8LQno=
github.com/xtls/xray-core v1.250911.0/go.mod h1:LkqA/BFVtPS2e5fRzg/bkYas9nQu4Uztlx+/fjlLM9k=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
@ -234,12 +256,12 @@ golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+Z
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 h1:/OQuEa4YWtDt7uQWHd3q3sUMb+QOLQUg1xa8CEsRv5w=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251006185510-65f7160b3a87 h1:WgGZrMngVRRve7T3P5gbXdmedSmUpkf8uIUu1fg+biY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251006185510-65f7160b3a87/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
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=

View file

@ -53,9 +53,12 @@ install_base() {
arch | manjaro | parch)
pacman -Syu && pacman -Syu --noconfirm wget curl tar tzdata
;;
opensuse-tumbleweed)
opensuse-tumbleweed | opensuse-leap)
zypper refresh && zypper -q install -y wget curl tar timezone
;;
alpine)
apk update && apk add wget curl tar tzdata
;;
*)
apt-get update && apt-get install -y -q wget curl tar tzdata
;;
@ -146,11 +149,15 @@ install_x-ui() {
if [ $# == 0 ]; then
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
echo -e "${yellow}Trying to fetch version with IPv4...${plain}"
tag_version=$(curl -4 -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
fi
echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..."
wget -N -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
wget --inet4-only -N -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}"
exit 1
@ -167,17 +174,25 @@ install_x-ui() {
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 -O /usr/local/x-ui-linux-$(arch).tar.gz ${url}
wget --inet4-only -N -O /usr/local/x-ui-linux-$(arch).tar.gz ${url}
if [[ $? -ne 0 ]]; then
echo -e "${red}Download x-ui $1 failed, please check if the version exists ${plain}"
exit 1
fi
fi
wget -O /usr/bin/x-ui-temp https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh
wget --inet4-only -O /usr/bin/x-ui-temp https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh
if [[ $? -ne 0 ]]; then
echo -e "${red}Failed to download x-ui.sh${plain}"
exit 1
fi
# Stop x-ui service and remove old resources
if [[ -e /usr/local/x-ui/ ]]; then
systemctl stop x-ui
if [[ $release == "alpine" ]]; then
rc-service x-ui stop
else
systemctl stop x-ui
fi
rm /usr/local/x-ui/ -rf
fi
@ -201,10 +216,22 @@ install_x-ui() {
chmod +x /usr/bin/x-ui
config_after_install
cp -f x-ui.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable x-ui
systemctl start x-ui
if [[ $release == "alpine" ]]; then
wget --inet4-only -O /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc
if [[ $? -ne 0 ]]; then
echo -e "${red}Failed to download x-ui.rc${plain}"
exit 1
fi
chmod +x /etc/init.d/x-ui
rc-update add x-ui
rc-service x-ui start
else
cp -f x-ui.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable x-ui
systemctl start x-ui
fi
echo -e "${green}x-ui ${tag_version}${plain} installation finished, it is running now..."
echo -e ""
echo -e "┌───────────────────────────────────────────────────────┐

View file

@ -1,21 +1,29 @@
// Package logger provides logging functionality for the 3x-ui panel with
// buffered log storage and multiple log levels.
// dual-backend logging (console/syslog and file) and buffered log storage for web UI.
package logger
import (
"fmt"
"os"
"path/filepath"
"runtime"
"time"
"github.com/mhsanaei/3x-ui/v2/config"
"github.com/op/go-logging"
)
var (
logger *logging.Logger
const (
maxLogBufferSize = 10240 // Maximum log entries kept in memory
logFileName = "3xui.log" // Log file name
timeFormat = "2006/01/02 15:04:05" // Log timestamp format
)
// addToBuffer appends a log entry into the in-memory ring buffer used for
// retrieving recent logs via the web UI. It keeps the buffer bounded to avoid
// uncontrolled growth.
var (
logger *logging.Logger
logFile *os.File
// logBuffer maintains recent log entries in memory for web UI retrieval
logBuffer []struct {
time string
level logging.Level
@ -23,37 +31,100 @@ var (
}
)
func init() {
InitLogger(logging.INFO)
}
// InitLogger initializes the logger with the specified logging level.
// InitLogger initializes dual logging backends: console/syslog and file.
// Console logging uses the specified level, file logging always uses DEBUG level.
func InitLogger(level logging.Level) {
newLogger := logging.MustGetLogger("x-ui")
var err error
var backend logging.Backend
var format logging.Formatter
ppid := os.Getppid()
backends := make([]logging.Backend, 0, 2)
backend, err = logging.NewSyslogBackend("")
if err != nil {
println(err)
backend = logging.NewLogBackend(os.Stderr, "", 0)
}
if ppid > 0 && err != nil {
format = logging.MustStringFormatter(`%{time:2006/01/02 15:04:05} %{level} - %{message}`)
} else {
format = logging.MustStringFormatter(`%{level} - %{message}`)
// Console/syslog backend with configurable level
if consoleBackend := initDefaultBackend(); consoleBackend != nil {
leveledBackend := logging.AddModuleLevel(consoleBackend)
leveledBackend.SetLevel(level, "x-ui")
backends = append(backends, leveledBackend)
}
backendFormatter := logging.NewBackendFormatter(backend, format)
backendLeveled := logging.AddModuleLevel(backendFormatter)
backendLeveled.SetLevel(level, "x-ui")
newLogger.SetBackend(backendLeveled)
// File backend with DEBUG level for comprehensive logging
if fileBackend := initFileBackend(); fileBackend != nil {
leveledBackend := logging.AddModuleLevel(fileBackend)
leveledBackend.SetLevel(logging.DEBUG, "x-ui")
backends = append(backends, leveledBackend)
}
multiBackend := logging.MultiLogger(backends...)
newLogger.SetBackend(multiBackend)
logger = newLogger
}
// initDefaultBackend creates the console/syslog logging backend.
// Windows: Uses stderr directly (no syslog support)
// Unix-like: Attempts syslog, falls back to stderr
func initDefaultBackend() logging.Backend {
var backend logging.Backend
includeTime := false
if runtime.GOOS == "windows" {
// Windows: Use stderr directly (no syslog support)
backend = logging.NewLogBackend(os.Stderr, "", 0)
includeTime = true
} else {
// Unix-like: Try syslog, fallback to stderr
if syslogBackend, err := logging.NewSyslogBackend(""); err != nil {
fmt.Fprintf(os.Stderr, "syslog backend disabled: %v\n", err)
backend = logging.NewLogBackend(os.Stderr, "", 0)
includeTime = os.Getppid() > 0
} else {
backend = syslogBackend
}
}
return logging.NewBackendFormatter(backend, newFormatter(includeTime))
}
// initFileBackend creates the file logging backend.
// Creates log directory and truncates log file on startup for fresh logs.
func initFileBackend() logging.Backend {
logDir := config.GetLogFolder()
if err := os.MkdirAll(logDir, 0o750); err != nil {
fmt.Fprintf(os.Stderr, "failed to create log folder %s: %v\n", logDir, err)
return nil
}
logPath := filepath.Join(logDir, logFileName)
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o660)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to open log file %s: %v\n", logPath, err)
return nil
}
// Close previous log file if exists
if logFile != nil {
_ = logFile.Close()
}
logFile = file
backend := logging.NewLogBackend(file, "", 0)
return logging.NewBackendFormatter(backend, newFormatter(true))
}
// newFormatter creates a log formatter with optional timestamp.
func newFormatter(withTime bool) logging.Formatter {
format := `%{level} - %{message}`
if withTime {
format = `%{time:` + timeFormat + `} %{level} - %{message}`
}
return logging.MustStringFormatter(format)
}
// CloseLogger closes the log file and cleans up resources.
// Should be called during application shutdown.
func CloseLogger() {
if logFile != nil {
_ = logFile.Close()
logFile = nil
}
}
// Debug logs a debug message and adds it to the log buffer.
func Debug(args ...any) {
logger.Debug(args...)
@ -114,9 +185,10 @@ func Errorf(format string, args ...any) {
addToBuffer("ERROR", fmt.Sprintf(format, args...))
}
// addToBuffer adds a log entry to the in-memory ring buffer for web UI retrieval.
func addToBuffer(level string, newLog string) {
t := time.Now()
if len(logBuffer) >= 10240 {
if len(logBuffer) >= maxLogBufferSize {
logBuffer = logBuffer[1:]
}
@ -126,7 +198,7 @@ func addToBuffer(level string, newLog string) {
level logging.Level
log string
}{
time: t.Format("2006/01/02 15:04:05"),
time: t.Format(timeFormat),
level: logLevel,
log: newLog,
})

View file

@ -98,8 +98,14 @@ func (s *Server) initRouter() (*gin.Engine, error) {
}
// Set base_path based on LinksPath for template rendering
// Ensure LinksPath ends with "/" for proper asset URL generation
basePath := LinksPath
if basePath != "/" && !strings.HasSuffix(basePath, "/") {
basePath += "/"
}
// logger.Debug("sub: Setting base_path to:", basePath)
engine.Use(func(c *gin.Context) {
c.Set("base_path", LinksPath)
c.Set("base_path", basePath)
})
Encrypt, err := s.settingService.GetSubEncrypt()
@ -179,22 +185,48 @@ func (s *Server) initRouter() (*gin.Engine, error) {
linksPathForAssets = strings.TrimRight(LinksPath, "/") + "/assets"
}
// Mount assets in multiple paths to handle different URL patterns
var assetsFS http.FileSystem
if _, err := os.Stat("web/assets"); err == nil {
engine.StaticFS("/assets", http.FS(os.DirFS("web/assets")))
if linksPathForAssets != "/assets" {
engine.StaticFS(linksPathForAssets, http.FS(os.DirFS("web/assets")))
}
assetsFS = http.FS(os.DirFS("web/assets"))
} else {
if subFS, err := fs.Sub(webpkg.EmbeddedAssets(), "assets"); err == nil {
engine.StaticFS("/assets", http.FS(subFS))
if linksPathForAssets != "/assets" {
engine.StaticFS(linksPathForAssets, http.FS(subFS))
}
assetsFS = http.FS(subFS)
} else {
logger.Error("sub: failed to mount embedded assets:", err)
}
}
if assetsFS != nil {
engine.StaticFS("/assets", assetsFS)
if linksPathForAssets != "/assets" {
engine.StaticFS(linksPathForAssets, assetsFS)
}
// Add middleware to handle dynamic asset paths with subid
if LinksPath != "/" {
engine.Use(func(c *gin.Context) {
path := c.Request.URL.Path
// Check if this is an asset request with subid pattern: /sub/path/{subid}/assets/...
pathPrefix := strings.TrimRight(LinksPath, "/") + "/"
if strings.HasPrefix(path, pathPrefix) && strings.Contains(path, "/assets/") {
// Extract the asset path after /assets/
assetsIndex := strings.Index(path, "/assets/")
if assetsIndex != -1 {
assetPath := path[assetsIndex+8:] // +8 to skip "/assets/"
if assetPath != "" {
// Serve the asset file
c.FileFromFS(assetPath, assetsFS)
c.Abort()
return
}
}
}
c.Next()
})
}
}
g := engine.Group("/")
s.sub = NewSUBController(

View file

@ -87,7 +87,20 @@ func (a *SUBController) subs(c *gin.Context) {
if !a.jsonEnabled {
subJsonURL = ""
}
page := a.subService.BuildPageData(subId, hostHeader, traffic, lastOnline, subs, subURL, subJsonURL)
// Get base_path from context (set by middleware)
basePath, exists := c.Get("base_path")
if !exists {
basePath = "/"
}
// Add subId to base_path for asset URLs
basePathStr := basePath.(string)
if basePathStr == "/" {
basePathStr = "/" + subId + "/"
} else {
// Remove trailing slash if exists, add subId, then add trailing slash
basePathStr = strings.TrimRight(basePathStr, "/") + "/" + subId + "/"
}
page := a.subService.BuildPageData(subId, hostHeader, traffic, lastOnline, subs, subURL, subJsonURL, basePathStr)
c.HTML(200, "subpage.html", gin.H{
"title": "subscription.title",
"cur_ver": config.GetVersion(),

View file

@ -1148,7 +1148,7 @@ func (s *SubService) joinPathWithID(basePath, subId string) string {
// BuildPageData parses header and prepares the template view model.
// BuildPageData constructs page data for rendering the subscription information page.
func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray.ClientTraffic, lastOnline int64, subs []string, subURL, subJsonURL string) PageData {
func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray.ClientTraffic, lastOnline int64, subs []string, subURL, subJsonURL string, basePath string) PageData {
download := common.FormatTraffic(traffic.Down)
upload := common.FormatTraffic(traffic.Up)
total := "∞"
@ -1167,7 +1167,7 @@ func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray
return PageData{
Host: hostHeader,
BasePath: "/", // kept as "/"; templates now use context base_path injected from router
BasePath: basePath,
SId: subId,
Download: download,
Upload: upload,

258
update.sh Executable file
View file

@ -0,0 +1,258 @@
#!/bin/bash
red='\033[0;31m'
green='\033[0;32m'
blue='\033[0;34m'
yellow='\033[0;33m'
plain='\033[0m'
# Don't edit this config
b_source="${BASH_SOURCE[0]}"
while [ -h "$b_source" ]; do
b_dir="$(cd -P "$(dirname "$b_source")" >/dev/null 2>&1 && pwd || pwd -P)"
b_source="$(readlink "$b_source")"
[[ $b_source != /* ]] && b_source="$b_dir/$b_source"
done
cur_dir="$(cd -P "$(dirname "$b_source")" >/dev/null 2>&1 && pwd || pwd -P)"
script_name=$(basename "$0")
# Check command exist function
_command_exists() {
type "$1" &>/dev/null
}
# Fail, log and exit script function
_fail() {
local msg=${1}
echo -e "${red}${msg}${plain}"
exit 2
}
# check root
[[ $EUID -ne 0 ]] && _fail "FATAL ERROR: Please run this script with root privilege."
if _command_exists wget; then
wget_bin=$(which wget)
else
_fail "ERROR: Command 'wget' not found."
fi
if _command_exists curl; then
curl_bin=$(which curl)
else
_fail "ERROR: Command 'curl' not found."
fi
# Check OS and set release variable
if [[ -f /etc/os-release ]]; then
source /etc/os-release
release=$ID
elif [[ -f /usr/lib/os-release ]]; then
source /usr/lib/os-release
release=$ID
else
_fail "Failed to check the system OS, please contact the author!"
fi
echo "The OS release is: $release"
arch() {
case "$(uname -m)" in
x86_64 | x64 | amd64) echo 'amd64' ;;
i*86 | x86) echo '386' ;;
armv8* | armv8 | arm64 | aarch64) echo 'arm64' ;;
armv7* | armv7 | arm) echo 'armv7' ;;
armv6* | armv6) echo 'armv6' ;;
armv5* | armv5) echo 'armv5' ;;
s390x) echo 's390x' ;;
*) echo -e "${red}Unsupported CPU architecture!${plain}" && rm -f "${cur_dir}/${script_name}" >/dev/null 2>&1 && exit 2;;
esac
}
echo "Arch: $(arch)"
install_base() {
echo -e "${green}Updating and install dependency packages...${plain}"
case "${release}" in
ubuntu | debian | armbian)
apt-get update >/dev/null 2>&1 && apt-get install -y -q wget curl tar tzdata >/dev/null 2>&1
;;
centos | rhel | almalinux | rocky | ol)
yum -y update >/dev/null 2>&1 && yum install -y -q wget curl tar tzdata >/dev/null 2>&1
;;
fedora | amzn | virtuozzo)
dnf -y update >/dev/null 2>&1 && dnf install -y -q wget curl tar tzdata >/dev/null 2>&1
;;
arch | manjaro | parch)
pacman -Syu >/dev/null 2>&1 && pacman -Syu --noconfirm wget curl tar tzdata >/dev/null 2>&1
;;
opensuse-tumbleweed | opensuse-leap)
zypper refresh >/dev/null 2>&1 && zypper -q install -y wget curl tar timezone >/dev/null 2>&1
;;
alpine)
apk update >/dev/null 2>&1 && apk add wget curl tar tzdata >/dev/null 2>&1
;;
*)
apt-get update >/dev/null 2>&1 && apt install -y -q wget curl tar tzdata >/dev/null 2>&1
;;
esac
}
config_after_update() {
echo -e "${yellow}x-ui settings:${plain}"
/usr/local/x-ui/x-ui setting -show true
/usr/local/x-ui/x-ui migrate
}
update_x-ui() {
cd /usr/local/
if [ -f "/usr/local/x-ui/x-ui" ]; then
current_xui_version=$(/usr/local/x-ui/x-ui -v)
echo -e "${green}Current x-ui version: ${current_xui_version}${plain}"
else
_fail "ERROR: Current x-ui version: unknown"
fi
echo -e "${green}Downloading new x-ui version...${plain}"
tag_version=$(${curl_bin} -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" 2>/dev/null | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
if [[ ! -n "$tag_version" ]]; then
echo -e "${yellow}Trying to fetch version with IPv4...${plain}"
tag_version=$(${curl_bin} -4 -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
if [[ ! -n "$tag_version" ]]; then
_fail "ERROR: Failed to fetch x-ui version, it may be due to GitHub API restrictions, please try it later"
fi
fi
echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..."
${wget_bin} -N -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 2>/dev/null
if [[ $? -ne 0 ]]; then
echo -e "${yellow}Trying to fetch version with IPv4...${plain}"
${wget_bin} --inet4-only -N -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 2>/dev/null
if [[ $? -ne 0 ]]; then
_fail "ERROR: Failed to download x-ui, please be sure that your server can access GitHub"
fi
fi
if [[ -e /usr/local/x-ui/ ]]; then
echo -e "${green}Stopping x-ui...${plain}"
if [[ $release == "alpine" ]]; then
if [ -f "/etc/init.d/x-ui" ]; then
rc-service x-ui stop >/dev/null 2>&1
rc-update del x-ui >/dev/null 2>&1
echo -e "${green}Removing old service unit version...${plain}"
rm -f /etc/init.d/x-ui >/dev/null 2>&1
else
rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
_fail "ERROR: x-ui service unit not installed."
fi
else
if [ -f "/etc/systemd/system/x-ui.service" ]; then
systemctl stop x-ui >/dev/null 2>&1
systemctl disable x-ui >/dev/null 2>&1
echo -e "${green}Removing old systemd unit version...${plain}"
rm /etc/systemd/system/x-ui.service -f >/dev/null 2>&1
systemctl daemon-reload >/dev/null 2>&1
else
rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
_fail "ERROR: x-ui systemd unit not installed."
fi
fi
echo -e "${green}Removing old x-ui version...${plain}"
rm /usr/bin/x-ui -f >/dev/null 2>&1
rm /usr/local/x-ui/x-ui.service -f >/dev/null 2>&1
rm /usr/local/x-ui/x-ui -f >/dev/null 2>&1
rm /usr/local/x-ui/x-ui.sh -f >/dev/null 2>&1
echo -e "${green}Removing old xray version...${plain}"
rm /usr/local/x-ui/bin/xray-linux-amd64 -f >/dev/null 2>&1
echo -e "${green}Removing old README and LICENSE file...${plain}"
rm /usr/local/x-ui/bin/README.md -f >/dev/null 2>&1
rm /usr/local/x-ui/bin/LICENSE -f >/dev/null 2>&1
else
rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
_fail "ERROR: x-ui not installed."
fi
echo -e "${green}Installing new x-ui version...${plain}"
tar zxvf x-ui-linux-$(arch).tar.gz >/dev/null 2>&1
rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
cd x-ui >/dev/null 2>&1
chmod +x x-ui >/dev/null 2>&1
# Check the system's architecture and rename the file accordingly
if [[ $(arch) == "armv5" || $(arch) == "armv6" || $(arch) == "armv7" ]]; then
mv bin/xray-linux-$(arch) bin/xray-linux-arm >/dev/null 2>&1
chmod +x bin/xray-linux-arm >/dev/null 2>&1
fi
chmod +x x-ui bin/xray-linux-$(arch) >/dev/null 2>&1
echo -e "${green}Downloading and installing x-ui.sh script...${plain}"
${wget_bin} -O /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh >/dev/null 2>&1
if [[ $? -ne 0 ]]; then
echo -e "${yellow}Trying to fetch x-ui with IPv4...${plain}"
${wget_bin} --inet4-only -O /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh >/dev/null 2>&1
if [[ $? -ne 0 ]]; then
_fail "ERROR: Failed to download x-ui.sh script, please be sure that your server can access GitHub"
fi
fi
chmod +x /usr/local/x-ui/x-ui.sh >/dev/null 2>&1
chmod +x /usr/bin/x-ui >/dev/null 2>&1
echo -e "${green}Changing owner...${plain}"
chown -R root:root /usr/local/x-ui >/dev/null 2>&1
if [ -f "/usr/local/x-ui/bin/config.json" ]; then
echo -e "${green}Changing on config file permissions...${plain}"
chmod 640 /usr/local/x-ui/bin/config.json >/dev/null 2>&1
fi
if [[ $release == "alpine" ]]; then
echo -e "${green}Downloading and installing startup unit x-ui.rc...${plain}"
${wget_bin} -O /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc >/dev/null 2>&1
if [[ $? -ne 0 ]]; then
${wget_bin} --inet4-only -O /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc >/dev/null 2>&1
if [[ $? -ne 0 ]]; then
_fail "ERROR: Failed to download startup unit x-ui.rc, please be sure that your server can access GitHub"
fi
fi
chmod +x /etc/init.d/x-ui >/dev/null 2>&1
chown root:root /etc/init.d/x-ui >/dev/null 2>&1
rc-update add x-ui >/dev/null 2>&1
rc-service x-ui start >/dev/null 2>&1
else
echo -e "${green}Installing systemd unit...${plain}"
cp -f x-ui.service /etc/systemd/system/ >/dev/null 2>&1
chown root:root /etc/systemd/system/x-ui.service >/dev/null 2>&1
systemctl daemon-reload >/dev/null 2>&1
systemctl enable x-ui >/dev/null 2>&1
systemctl start x-ui >/dev/null 2>&1
fi
config_after_update
echo -e "${green}x-ui ${tag_version}${plain} updating finished, it is running now..."
echo -e ""
echo -e "┌───────────────────────────────────────────────────────┐
${blue}x-ui control menu usages (subcommands):${plain}
│ │
${blue}x-ui${plain} - Admin Management Script │
${blue}x-ui start${plain} - Start │
${blue}x-ui stop${plain} - Stop │
${blue}x-ui restart${plain} - Restart │
${blue}x-ui status${plain} - Current Status │
${blue}x-ui settings${plain} - Current Settings │
${blue}x-ui enable${plain} - Enable Autostart on OS Startup │
${blue}x-ui disable${plain} - Disable Autostart on OS Startup │
${blue}x-ui log${plain} - Check logs │
${blue}x-ui banlog${plain} - Check Fail2ban ban logs │
${blue}x-ui update${plain} - Update │
${blue}x-ui legacy${plain} - legacy version │
${blue}x-ui install${plain} - Install │
${blue}x-ui uninstall${plain} - Uninstall │
└───────────────────────────────────────────────────────┘"
}
echo -e "${green}Running...${plain}"
install_base
update_x-ui $1

144
util/ldap/ldap.go Normal file
View file

@ -0,0 +1,144 @@
package ldaputil
import (
"crypto/tls"
"fmt"
"github.com/go-ldap/ldap/v3"
)
type Config struct {
Host string
Port int
UseTLS bool
BindDN string
Password string
BaseDN string
UserFilter string
UserAttr string
FlagField string
TruthyVals []string
Invert bool
}
// FetchVlessFlags returns map[email]enabled
func FetchVlessFlags(cfg Config) (map[string]bool, error) {
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
var conn *ldap.Conn
var err error
if cfg.UseTLS {
conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false})
} else {
conn, err = ldap.Dial("tcp", addr)
}
if err != nil {
return nil, err
}
defer conn.Close()
if cfg.BindDN != "" {
if err := conn.Bind(cfg.BindDN, cfg.Password); err != nil {
return nil, err
}
}
if cfg.UserFilter == "" {
cfg.UserFilter = "(objectClass=person)"
}
if cfg.UserAttr == "" {
cfg.UserAttr = "mail"
}
// if field not set we fallback to legacy vless_enabled
if cfg.FlagField == "" {
cfg.FlagField = "vless_enabled"
}
req := ldap.NewSearchRequest(
cfg.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
cfg.UserFilter,
[]string{cfg.UserAttr, cfg.FlagField},
nil,
)
res, err := conn.Search(req)
if err != nil {
return nil, err
}
result := make(map[string]bool, len(res.Entries))
for _, e := range res.Entries {
user := e.GetAttributeValue(cfg.UserAttr)
if user == "" {
continue
}
val := e.GetAttributeValue(cfg.FlagField)
enabled := false
for _, t := range cfg.TruthyVals {
if val == t {
enabled = true
break
}
}
if cfg.Invert {
enabled = !enabled
}
result[user] = enabled
}
return result, nil
}
// AuthenticateUser searches user by cfg.UserAttr and attempts to bind with provided password.
func AuthenticateUser(cfg Config, username, password string) (bool, error) {
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
var conn *ldap.Conn
var err error
if cfg.UseTLS {
conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false})
} else {
conn, err = ldap.Dial("tcp", addr)
}
if err != nil {
return false, err
}
defer conn.Close()
// Optional initial bind for search
if cfg.BindDN != "" {
if err := conn.Bind(cfg.BindDN, cfg.Password); err != nil {
return false, err
}
}
if cfg.UserFilter == "" {
cfg.UserFilter = "(objectClass=person)"
}
if cfg.UserAttr == "" {
cfg.UserAttr = "uid"
}
// Build filter to find specific user
filter := fmt.Sprintf("(&%s(%s=%s))", cfg.UserFilter, cfg.UserAttr, ldap.EscapeFilter(username))
req := ldap.NewSearchRequest(
cfg.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false,
filter,
[]string{"dn"},
nil,
)
res, err := conn.Search(req)
if err != nil {
return false, err
}
if len(res.Entries) == 0 {
return false, nil
}
userDN := res.Entries[0].DN
// Try to bind as the user
if err := conn.Bind(userDN, password); err != nil {
return false, nil
}
return true, nil
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -50,6 +50,28 @@ class AllSetting {
this.timeLocation = "Local";
// LDAP settings
this.ldapEnable = false;
this.ldapHost = "";
this.ldapPort = 389;
this.ldapUseTLS = false;
this.ldapBindDN = "";
this.ldapPassword = "";
this.ldapBaseDN = "";
this.ldapUserFilter = "(objectClass=person)";
this.ldapUserAttr = "mail";
this.ldapVlessField = "vless_enabled";
this.ldapSyncCron = "@every 1m";
this.ldapFlagField = "";
this.ldapTruthyValues = "true,1,yes,on";
this.ldapInvertFlag = false;
this.ldapInboundTags = "";
this.ldapAutoCreate = false;
this.ldapAutoDelete = false;
this.ldapDefaultTotalGB = 0;
this.ldapDefaultExpiryDays = 0;
this.ldapDefaultLimitIP = 0;
if (data == null) {
return
}

View file

@ -142,7 +142,10 @@
},
npvtunUrl() {
return this.app.subUrl;
}
},
happUrl() {
return `happ://add/${encodeURIComponent(this.app.subUrl)}`;
}
},
methods: {
renderLink,

View file

@ -316,23 +316,13 @@ class ObjectUtil {
}
static equals(a, b) {
for (const key in a) {
if (!a.hasOwnProperty(key)) {
continue;
}
if (!b.hasOwnProperty(key)) {
return false;
} else if (a[key] !== b[key]) {
return false;
}
}
for (const key in b) {
if (!b.hasOwnProperty(key)) {
continue;
}
if (!a.hasOwnProperty(key)) {
return false;
}
// shallow, symmetric comparison so newly added fields also affect equality
const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
if (aKeys.length !== bKeys.length) return false;
for (const key of aKeys) {
if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
if (a[key] !== b[key]) return false;
}
return true;
}

File diff suppressed because one or more lines are too long

View file

@ -1,19 +1,19 @@
//! otpauth 9.4.0 | (c) Héctor Molinero Fernández | MIT | https://github.com/hectorm/otpauth
//! noble-hashes 1.7.1 | (c) Paul Miller | MIT | https://github.com/paulmillr/noble-hashes
//! otpauth 9.4.1 | (c) Héctor Molinero Fernández | MIT | https://github.com/hectorm/otpauth
//! noble-hashes 1.8.0 | (c) Paul Miller | MIT | https://github.com/paulmillr/noble-hashes
/// <reference types="./otpauth.d.ts" />
// @ts-nocheck
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).OTPAuth={})}(this,(function(t){"use strict";function e(t){if(!Number.isSafeInteger(t)||t<0)throw new Error("positive integer expected, got "+t)}function s(t,...e){if(!((s=t)instanceof Uint8Array||ArrayBuffer.isView(s)&&"Uint8Array"===s.constructor.name))throw new Error("Uint8Array expected");var s;if(e.length>0&&!e.includes(t.length))throw new Error("Uint8Array expected of length "+e+", got length="+t.length)}function i(t,e=!0){if(t.destroyed)throw new Error("Hash instance has been destroyed");if(e&&t.finished)throw new Error("Hash#digest() has already been called")}function r(t,e){s(t);const i=e.outputLen;if(t.length<i)throw new Error("digestInto() expects output buffer of length at least "+i)}function n(t){return new DataView(t.buffer,t.byteOffset,t.byteLength)}function o(t,e){return t<<32-e|t>>>e}function h(t,e){return t<<e|t>>>32-e>>>0}const a=(()=>68===new Uint8Array(new Uint32Array([287454020]).buffer)[0])();function l(t){for(let s=0;s<t.length;s++)t[s]=(e=t[s])<<24&4278190080|e<<8&16711680|e>>>8&65280|e>>>24&255;var e}function c(t){return"string"==typeof t&&(t=function(t){if("string"!=typeof t)throw new Error("utf8ToBytes expected string, got "+typeof t);return new Uint8Array((new TextEncoder).encode(t))}(t)),s(t),t}class u{clone(){return this._cloneInto()}}function d(t){const e=e=>t().update(c(e)).digest(),s=t();return e.outputLen=s.outputLen,e.blockLen=s.blockLen,e.create=()=>t(),e}class f extends u{update(t){return i(this),this.iHash.update(t),this}digestInto(t){i(this),s(t,this.outputLen),this.finished=!0,this.iHash.digestInto(t),this.oHash.update(t),this.oHash.digestInto(t),this.destroy()}digest(){const t=new Uint8Array(this.oHash.outputLen);return this.digestInto(t),t}_cloneInto(t){t||(t=Object.create(Object.getPrototypeOf(this),{}));const{oHash:e,iHash:s,finished:i,destroyed:r,blockLen:n,outputLen:o}=this
;return t.finished=i,t.destroyed=r,t.blockLen=n,t.outputLen=o,t.oHash=e._cloneInto(t.oHash),t.iHash=s._cloneInto(t.iHash),t}destroy(){this.destroyed=!0,this.oHash.destroy(),this.iHash.destroy()}constructor(t,s){super(),this.finished=!1,this.destroyed=!1,function(t){if("function"!=typeof t||"function"!=typeof t.create)throw new Error("Hash should be wrapped by utils.wrapConstructor");e(t.outputLen),e(t.blockLen)}(t);const i=c(s);if(this.iHash=t.create(),"function"!=typeof this.iHash.update)throw new Error("Expected instance of class which extends utils.Hash");this.blockLen=this.iHash.blockLen,this.outputLen=this.iHash.outputLen;const r=this.blockLen,n=new Uint8Array(r);n.set(i.length>r?t.create().update(i).digest():i);for(let t=0;t<n.length;t++)n[t]^=54;this.iHash.update(n),this.oHash=t.create();for(let t=0;t<n.length;t++)n[t]^=106;this.oHash.update(n),n.fill(0)}}const b=(t,e,s)=>new f(t,e).update(s).digest();function g(t,e,s){return t&e^~t&s}function p(t,e,s){return t&e^t&s^e&s}b.create=(t,e)=>new f(t,e);class w extends u{update(t){i(this);const{view:e,buffer:s,blockLen:r}=this,o=(t=c(t)).length;for(let i=0;i<o;){const h=Math.min(r-this.pos,o-i);if(h!==r)s.set(t.subarray(i,i+h),this.pos),this.pos+=h,i+=h,this.pos===r&&(this.process(e,0),this.pos=0);else{const e=n(t);for(;r<=o-i;i+=r)this.process(e,i)}}return this.length+=t.length,this.roundClean(),this}digestInto(t){i(this),r(t,this),this.finished=!0;const{buffer:e,view:s,blockLen:o,isLE:h}=this;let{pos:a}=this;e[a++]=128,this.buffer.subarray(a).fill(0),this.padOffset>o-a&&(this.process(s,0),a=0);for(let t=a;t<o;t++)e[t]=0;!function(t,e,s,i){if("function"==typeof t.setBigUint64)return t.setBigUint64(e,s,i);const r=BigInt(32),n=BigInt(4294967295),o=Number(s>>r&n),h=Number(s&n),a=i?4:0,l=i?0:4;t.setUint32(e+a,o,i),t.setUint32(e+l,h,i)}(s,o-8,BigInt(8*this.length),h),this.process(s,0);const l=n(t),c=this.outputLen;if(c%4)throw new Error("_sha2: outputLen should be aligned to 32bit");const u=c/4,d=this.get()
;if(u>d.length)throw new Error("_sha2: outputLen bigger than state");for(let t=0;t<u;t++)l.setUint32(4*t,d[t],h)}digest(){const{buffer:t,outputLen:e}=this;this.digestInto(t);const s=t.slice(0,e);return this.destroy(),s}_cloneInto(t){t||(t=new this.constructor),t.set(...this.get());const{blockLen:e,buffer:s,length:i,finished:r,destroyed:n,pos:o}=this;return t.length=i,t.pos=o,t.finished=r,t.destroyed=n,i%e&&t.buffer.set(s),t}constructor(t,e,s,i){super(),this.blockLen=t,this.outputLen=e,this.padOffset=s,this.isLE=i,this.finished=!1,this.length=0,this.pos=0,this.destroyed=!1,this.buffer=new Uint8Array(t),this.view=n(this.buffer)}}const y=new Uint32Array([1732584193,4023233417,2562383102,271733878,3285377520]),x=new Uint32Array(80);class A extends w{get(){const{A:t,B:e,C:s,D:i,E:r}=this;return[t,e,s,i,r]}set(t,e,s,i,r){this.A=0|t,this.B=0|e,this.C=0|s,this.D=0|i,this.E=0|r}process(t,e){for(let s=0;s<16;s++,e+=4)x[s]=t.getUint32(e,!1);for(let t=16;t<80;t++)x[t]=h(x[t-3]^x[t-8]^x[t-14]^x[t-16],1);let{A:s,B:i,C:r,D:n,E:o}=this;for(let t=0;t<80;t++){let e,a;t<20?(e=g(i,r,n),a=1518500249):t<40?(e=i^r^n,a=1859775393):t<60?(e=p(i,r,n),a=2400959708):(e=i^r^n,a=3395469782);const l=h(s,5)+e+o+a+x[t]|0;o=n,n=r,r=h(i,30),i=s,s=l}s=s+this.A|0,i=i+this.B|0,r=r+this.C|0,n=n+this.D|0,o=o+this.E|0,this.set(s,i,r,n,o)}roundClean(){x.fill(0)}destroy(){this.set(0,0,0,0,0),this.buffer.fill(0)}constructor(){super(64,20,8,!1),this.A=0|y[0],this.B=0|y[1],this.C=0|y[2],this.D=0|y[3],this.E=0|y[4]}}
const m=d((()=>new A)),H=new Uint32Array([1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298]),L=new Uint32Array([1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225]),I=new Uint32Array(64);class S extends w{get(){const{A:t,B:e,C:s,D:i,E:r,F:n,G:o,H:h}=this;return[t,e,s,i,r,n,o,h]}set(t,e,s,i,r,n,o,h){this.A=0|t,this.B=0|e,this.C=0|s,this.D=0|i,this.E=0|r,this.F=0|n,this.G=0|o,this.H=0|h}process(t,e){for(let s=0;s<16;s++,e+=4)I[s]=t.getUint32(e,!1);for(let t=16;t<64;t++){const e=I[t-15],s=I[t-2],i=o(e,7)^o(e,18)^e>>>3,r=o(s,17)^o(s,19)^s>>>10;I[t]=r+I[t-7]+i+I[t-16]|0}let{A:s,B:i,C:r,D:n,E:h,F:a,G:l,H:c}=this;for(let t=0;t<64;t++){const e=c+(o(h,6)^o(h,11)^o(h,25))+g(h,a,l)+H[t]+I[t]|0,u=(o(s,2)^o(s,13)^o(s,22))+p(s,i,r)|0;c=l,l=a,a=h,h=n+e|0,n=r,r=i,i=s,s=e+u|0}s=s+this.A|0,i=i+this.B|0,r=r+this.C|0,n=n+this.D|0,h=h+this.E|0,a=a+this.F|0,l=l+this.G|0,c=c+this.H|0,this.set(s,i,r,n,h,a,l,c)}roundClean(){I.fill(0)}destroy(){this.set(0,0,0,0,0,0,0,0),this.buffer.fill(0)}constructor(){super(64,32,8,!1),this.A=0|L[0],this.B=0|L[1],this.C=0|L[2],this.D=0|L[3],this.E=0|L[4],this.F=0|L[5],this.G=0|L[6],this.H=0|L[7]}}class B extends S{constructor(){super(),this.A=-1056596264,this.B=914150663,this.C=812702999,this.D=-150054599,this.E=-4191439,this.F=1750603025,this.G=1694076839,this.H=-1090891868,this.outputLen=28}}
const E=d((()=>new S)),U=d((()=>new B)),C=BigInt(2**32-1),O=BigInt(32);function v(t,e=!1){return e?{h:Number(t&C),l:Number(t>>O&C)}:{h:0|Number(t>>O&C),l:0|Number(t&C)}}function k(t,e=!1){let s=new Uint32Array(t.length),i=new Uint32Array(t.length);for(let r=0;r<t.length;r++){const{h:n,l:o}=v(t[r],e);[s[r],i[r]]=[n,o]}return[s,i]}const T=(t,e,s)=>t<<s|e>>>32-s,$=(t,e,s)=>e<<s|t>>>32-s,D=(t,e,s)=>e<<s-32|t>>>64-s,_=(t,e,s)=>t<<s-32|e>>>64-s,F={fromBig:v,split:k,toBig:(t,e)=>BigInt(t>>>0)<<O|BigInt(e>>>0),shrSH:(t,e,s)=>t>>>s,shrSL:(t,e,s)=>t<<32-s|e>>>s,rotrSH:(t,e,s)=>t>>>s|e<<32-s,rotrSL:(t,e,s)=>t<<32-s|e>>>s,rotrBH:(t,e,s)=>t<<64-s|e>>>s-32,rotrBL:(t,e,s)=>t>>>s-32|e<<64-s,rotr32H:(t,e)=>e,rotr32L:(t,e)=>t,rotlSH:T,rotlSL:$,rotlBH:D,rotlBL:_,add:function(t,e,s,i){const r=(e>>>0)+(i>>>0);return{h:t+s+(r/2**32|0)|0,l:0|r}},add3L:(t,e,s)=>(t>>>0)+(e>>>0)+(s>>>0),add3H:(t,e,s,i)=>e+s+i+(t/2**32|0)|0,add4L:(t,e,s,i)=>(t>>>0)+(e>>>0)+(s>>>0)+(i>>>0),add4H:(t,e,s,i,r)=>e+s+i+r+(t/2**32|0)|0,add5H:(t,e,s,i,r,n)=>e+s+i+r+n+(t/2**32|0)|0,add5L:(t,e,s,i,r)=>(t>>>0)+(e>>>0)+(s>>>0)+(i>>>0)+(r>>>0)
},[G,P]=(()=>F.split(["0x428a2f98d728ae22","0x7137449123ef65cd","0xb5c0fbcfec4d3b2f","0xe9b5dba58189dbbc","0x3956c25bf348b538","0x59f111f1b605d019","0x923f82a4af194f9b","0xab1c5ed5da6d8118","0xd807aa98a3030242","0x12835b0145706fbe","0x243185be4ee4b28c","0x550c7dc3d5ffb4e2","0x72be5d74f27b896f","0x80deb1fe3b1696b1","0x9bdc06a725c71235","0xc19bf174cf692694","0xe49b69c19ef14ad2","0xefbe4786384f25e3","0x0fc19dc68b8cd5b5","0x240ca1cc77ac9c65","0x2de92c6f592b0275","0x4a7484aa6ea6e483","0x5cb0a9dcbd41fbd4","0x76f988da831153b5","0x983e5152ee66dfab","0xa831c66d2db43210","0xb00327c898fb213f","0xbf597fc7beef0ee4","0xc6e00bf33da88fc2","0xd5a79147930aa725","0x06ca6351e003826f","0x142929670a0e6e70","0x27b70a8546d22ffc","0x2e1b21385c26c926","0x4d2c6dfc5ac42aed","0x53380d139d95b3df","0x650a73548baf63de","0x766a0abb3c77b2a8","0x81c2c92e47edaee6","0x92722c851482353b","0xa2bfe8a14cf10364","0xa81a664bbc423001","0xc24b8b70d0f89791","0xc76c51a30654be30","0xd192e819d6ef5218","0xd69906245565a910","0xf40e35855771202a","0x106aa07032bbd1b8","0x19a4c116b8d2d0c8","0x1e376c085141ab53","0x2748774cdf8eeb99","0x34b0bcb5e19b48a8","0x391c0cb3c5c95a63","0x4ed8aa4ae3418acb","0x5b9cca4f7763e373","0x682e6ff3d6b2b8a3","0x748f82ee5defb2fc","0x78a5636f43172f60","0x84c87814a1f0ab72","0x8cc702081a6439ec","0x90befffa23631e28","0xa4506cebde82bde9","0xbef9a3f7b2c67915","0xc67178f2e372532b","0xca273eceea26619c","0xd186b8c721c0c207","0xeada7dd6cde0eb1e","0xf57d4f7fee6ed178","0x06f067aa72176fba","0x0a637dc5a2c898a6","0x113f9804bef90dae","0x1b710b35131c471b","0x28db77f523047d84","0x32caab7b40c72493","0x3c9ebe0a15c9bebc","0x431d67c49c100d4c","0x4cc5d4becb3e42b6","0x597f299cfc657e2a","0x5fcb6fab3ad6faec","0x6c44198c4a475817"].map((t=>BigInt(t)))))(),j=new Uint32Array(80),M=new Uint32Array(80);class R extends w{get(){const{Ah:t,Al:e,Bh:s,Bl:i,Ch:r,Cl:n,Dh:o,Dl:h,Eh:a,El:l,Fh:c,Fl:u,Gh:d,Gl:f,Hh:b,Hl:g}=this;return[t,e,s,i,r,n,o,h,a,l,c,u,d,f,b,g]}set(t,e,s,i,r,n,o,h,a,l,c,u,d,f,b,g){this.Ah=0|t,this.Al=0|e,this.Bh=0|s,this.Bl=0|i,this.Ch=0|r,this.Cl=0|n,this.Dh=0|o,
this.Dl=0|h,this.Eh=0|a,this.El=0|l,this.Fh=0|c,this.Fl=0|u,this.Gh=0|d,this.Gl=0|f,this.Hh=0|b,this.Hl=0|g}process(t,e){for(let s=0;s<16;s++,e+=4)j[s]=t.getUint32(e),M[s]=t.getUint32(e+=4);for(let t=16;t<80;t++){const e=0|j[t-15],s=0|M[t-15],i=F.rotrSH(e,s,1)^F.rotrSH(e,s,8)^F.shrSH(e,s,7),r=F.rotrSL(e,s,1)^F.rotrSL(e,s,8)^F.shrSL(e,s,7),n=0|j[t-2],o=0|M[t-2],h=F.rotrSH(n,o,19)^F.rotrBH(n,o,61)^F.shrSH(n,o,6),a=F.rotrSL(n,o,19)^F.rotrBL(n,o,61)^F.shrSL(n,o,6),l=F.add4L(r,a,M[t-7],M[t-16]),c=F.add4H(l,i,h,j[t-7],j[t-16]);j[t]=0|c,M[t]=0|l}let{Ah:s,Al:i,Bh:r,Bl:n,Ch:o,Cl:h,Dh:a,Dl:l,Eh:c,El:u,Fh:d,Fl:f,Gh:b,Gl:g,Hh:p,Hl:w}=this;for(let t=0;t<80;t++){const e=F.rotrSH(c,u,14)^F.rotrSH(c,u,18)^F.rotrBH(c,u,41),y=F.rotrSL(c,u,14)^F.rotrSL(c,u,18)^F.rotrBL(c,u,41),x=c&d^~c&b,A=u&f^~u&g,m=F.add5L(w,y,A,P[t],M[t]),H=F.add5H(m,p,e,x,G[t],j[t]),L=0|m,I=F.rotrSH(s,i,28)^F.rotrBH(s,i,34)^F.rotrBH(s,i,39),S=F.rotrSL(s,i,28)^F.rotrBL(s,i,34)^F.rotrBL(s,i,39),B=s&r^s&o^r&o,E=i&n^i&h^n&h;p=0|b,w=0|g,b=0|d,g=0|f,d=0|c,f=0|u,({h:c,l:u}=F.add(0|a,0|l,0|H,0|L)),a=0|o,l=0|h,o=0|r,h=0|n,r=0|s,n=0|i;const U=F.add3L(L,S,E);s=F.add3H(U,H,I,B),i=0|U}({h:s,l:i}=F.add(0|this.Ah,0|this.Al,0|s,0|i)),({h:r,l:n}=F.add(0|this.Bh,0|this.Bl,0|r,0|n)),({h:o,l:h}=F.add(0|this.Ch,0|this.Cl,0|o,0|h)),({h:a,l}=F.add(0|this.Dh,0|this.Dl,0|a,0|l)),({h:c,l:u}=F.add(0|this.Eh,0|this.El,0|c,0|u)),({h:d,l:f}=F.add(0|this.Fh,0|this.Fl,0|d,0|f)),({h:b,l:g}=F.add(0|this.Gh,0|this.Gl,0|b,0|g)),({h:p,l:w}=F.add(0|this.Hh,0|this.Hl,0|p,0|w)),this.set(s,i,r,n,o,h,a,l,c,u,d,f,b,g,p,w)}roundClean(){j.fill(0),M.fill(0)}destroy(){this.buffer.fill(0),this.set(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)}constructor(){super(128,64,16,!1),this.Ah=1779033703,this.Al=-205731576,this.Bh=-1150833019,this.Bl=-2067093701,this.Ch=1013904242,this.Cl=-23791573,this.Dh=-1521486534,this.Dl=1595750129,this.Eh=1359893119,this.El=-1377402159,this.Fh=-1694144372,this.Fl=725511199,this.Gh=528734635,this.Gl=-79577749,this.Hh=1541459225,this.Hl=327033209}}class N extends R{constructor(){super(),
this.Ah=-876896931,this.Al=-1056596264,this.Bh=1654270250,this.Bl=914150663,this.Ch=-1856437926,this.Cl=812702999,this.Dh=355462360,this.Dl=-150054599,this.Eh=1731405415,this.El=-4191439,this.Fh=-1900787065,this.Fl=1750603025,this.Gh=-619958771,this.Gl=1694076839,this.Hh=1203062813,this.Hl=-1090891868,this.outputLen=48}}const X=d((()=>new R)),V=d((()=>new N)),Z=[],z=[],J=[],K=BigInt(0),Q=BigInt(1),W=BigInt(2),Y=BigInt(7),q=BigInt(256),tt=BigInt(113);for(let t=0,e=Q,s=1,i=0;t<24;t++){[s,i]=[i,(2*s+3*i)%5],Z.push(2*(5*i+s)),z.push((t+1)*(t+2)/2%64);let r=K;for(let t=0;t<7;t++)e=(e<<Q^(e>>Y)*tt)%q,e&W&&(r^=Q<<(Q<<BigInt(t))-Q);J.push(r)}const[et,st]=k(J,!0),it=(t,e,s)=>s>32?D(t,e,s):T(t,e,s),rt=(t,e,s)=>s>32?_(t,e,s):$(t,e,s);class nt extends u{keccak(){a||l(this.state32),function(t,e=24){const s=new Uint32Array(10);for(let i=24-e;i<24;i++){for(let e=0;e<10;e++)s[e]=t[e]^t[e+10]^t[e+20]^t[e+30]^t[e+40];for(let e=0;e<10;e+=2){const i=(e+8)%10,r=(e+2)%10,n=s[r],o=s[r+1],h=it(n,o,1)^s[i],a=rt(n,o,1)^s[i+1];for(let s=0;s<50;s+=10)t[e+s]^=h,t[e+s+1]^=a}let e=t[2],r=t[3];for(let s=0;s<24;s++){const i=z[s],n=it(e,r,i),o=rt(e,r,i),h=Z[s];e=t[h],r=t[h+1],t[h]=n,t[h+1]=o}for(let e=0;e<50;e+=10){for(let i=0;i<10;i++)s[i]=t[e+i];for(let i=0;i<10;i++)t[e+i]^=~s[(i+2)%10]&s[(i+4)%10]}t[0]^=et[i],t[1]^=st[i]}s.fill(0)}(this.state32,this.rounds),a||l(this.state32),this.posOut=0,this.pos=0}update(t){i(this);const{blockLen:e,state:s}=this,r=(t=c(t)).length;for(let i=0;i<r;){const n=Math.min(e-this.pos,r-i);for(let e=0;e<n;e++)s[this.pos++]^=t[i++];this.pos===e&&this.keccak()}return this}finish(){if(this.finished)return;this.finished=!0;const{state:t,suffix:e,pos:s,blockLen:i}=this;t[s]^=e,128&e&&s===i-1&&this.keccak(),t[i-1]^=128,this.keccak()}writeInto(t){i(this,!1),s(t),this.finish();const e=this.state,{blockLen:r}=this;for(let s=0,i=t.length;s<i;){this.posOut>=r&&this.keccak();const n=Math.min(r-this.posOut,i-s);t.set(e.subarray(this.posOut,this.posOut+n),s),this.posOut+=n,s+=n}return t}xofInto(t){
if(!this.enableXOF)throw new Error("XOF is not possible for this instance");return this.writeInto(t)}xof(t){return e(t),this.xofInto(new Uint8Array(t))}digestInto(t){if(r(t,this),this.finished)throw new Error("digest() was already called");return this.writeInto(t),this.destroy(),t}digest(){return this.digestInto(new Uint8Array(this.outputLen))}destroy(){this.destroyed=!0,this.state.fill(0)}_cloneInto(t){const{blockLen:e,suffix:s,outputLen:i,rounds:r,enableXOF:n}=this;return t||(t=new nt(e,s,i,n,r)),t.state32.set(this.state32),t.pos=this.pos,t.posOut=this.posOut,t.finished=this.finished,t.rounds=r,t.suffix=s,t.outputLen=i,t.enableXOF=n,t.destroyed=this.destroyed,t}constructor(t,s,i,r=!1,n=24){if(super(),this.blockLen=t,this.suffix=s,this.outputLen=i,this.enableXOF=r,this.rounds=n,this.pos=0,this.posOut=0,this.finished=!1,this.destroyed=!1,e(i),0>=this.blockLen||this.blockLen>=200)throw new Error("Sha3 supports only keccak-f1600 function");var o;this.state=new Uint8Array(200),this.state32=(o=this.state,new Uint32Array(o.buffer,o.byteOffset,Math.floor(o.byteLength/4)))}}const ot=(t,e,s)=>d((()=>new nt(e,t,s))),ht=ot(6,144,28),at=ot(6,136,32),lt=ot(6,104,48),ct=ot(6,72,64),ut=(()=>{if("object"==typeof globalThis)return globalThis;Object.defineProperty(Object.prototype,"__GLOBALTHIS__",{get(){return this},configurable:!0});try{if("undefined"!=typeof __GLOBALTHIS__)return __GLOBALTHIS__}finally{delete Object.prototype.__GLOBALTHIS__}return"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:void 0})(),dt={SHA1:m,SHA224:U,SHA256:E,SHA384:V,SHA512:X,"SHA3-224":ht,"SHA3-256":at,"SHA3-384":lt,"SHA3-512":ct},ft=t=>{switch(!0){case/^(?:SHA-?1|SSL3-SHA1)$/i.test(t):return"SHA1";case/^SHA(?:2?-)?224$/i.test(t):return"SHA224";case/^SHA(?:2?-)?256$/i.test(t):return"SHA256";case/^SHA(?:2?-)?384$/i.test(t):return"SHA384";case/^SHA(?:2?-)?512$/i.test(t):return"SHA512";case/^SHA3-224$/i.test(t):return"SHA3-224";case/^SHA3-256$/i.test(t):return"SHA3-256";case/^SHA3-384$/i.test(t):
return"SHA3-384";case/^SHA3-512$/i.test(t):return"SHA3-512";default:throw new TypeError(`Unknown hash algorithm: ${t}`)}},bt="ABCDEFGHIJKLMNOPQRSTUVWXYZ234567",gt=t=>{let e=(t=t.replace(/ /g,"")).length;for(;"="===t[e-1];)--e;t=(e<t.length?t.substring(0,e):t).toUpperCase();const s=new ArrayBuffer(5*t.length/8|0),i=new Uint8Array(s);let r=0,n=0,o=0;for(let e=0;e<t.length;e++){const s=bt.indexOf(t[e]);if(-1===s)throw new TypeError(`Invalid character found: ${t[e]}`);n=n<<5|s,r+=5,r>=8&&(r-=8,i[o++]=n>>>r)}return i},pt=t=>{let e=0,s=0,i="";for(let r=0;r<t.length;r++)for(s=s<<8|t[r],e+=8;e>=5;)i+=bt[s>>>e-5&31],e-=5;return e>0&&(i+=bt[s<<5-e&31]),i},wt=t=>{t=t.replace(/ /g,"");const e=new ArrayBuffer(t.length/2),s=new Uint8Array(e);for(let e=0;e<t.length;e+=2)s[e/2]=parseInt(t.substring(e,e+2),16);return s},yt=t=>{let e="";for(let s=0;s<t.length;s++){const i=t[s].toString(16);1===i.length&&(e+="0"),e+=i}return e.toUpperCase()},xt=t=>{const e=new ArrayBuffer(t.length),s=new Uint8Array(e);for(let e=0;e<t.length;e++)s[e]=255&t.charCodeAt(e);return s},At=t=>{let e="";for(let s=0;s<t.length;s++)e+=String.fromCharCode(t[s]);return e},mt=ut.TextEncoder?new ut.TextEncoder:null,Ht=ut.TextDecoder?new ut.TextDecoder:null,Lt=t=>{if(!mt)throw new Error("Encoding API not available");return mt.encode(t)},It=t=>{if(!Ht)throw new Error("Encoding API not available");return Ht.decode(t)};class St{static fromLatin1(t){return new St({buffer:xt(t).buffer})}static fromUTF8(t){return new St({buffer:Lt(t).buffer})}static fromBase32(t){return new St({buffer:gt(t).buffer})}static fromHex(t){return new St({buffer:wt(t).buffer})}get buffer(){return this.bytes.buffer}get latin1(){return Object.defineProperty(this,"latin1",{enumerable:!0,writable:!1,configurable:!1,value:At(this.bytes)}),this.latin1}get utf8(){return Object.defineProperty(this,"utf8",{enumerable:!0,writable:!1,configurable:!1,value:It(this.bytes)}),this.utf8}get base32(){return Object.defineProperty(this,"base32",{enumerable:!0,writable:!1,configurable:!1,value:pt(this.bytes)}),
this.base32}get hex(){return Object.defineProperty(this,"hex",{enumerable:!0,writable:!1,configurable:!1,value:yt(this.bytes)}),this.hex}constructor({buffer:t,size:e=20}={}){this.bytes=void 0===t?(t=>{if(ut.crypto?.getRandomValues)return ut.crypto.getRandomValues(new Uint8Array(t));throw new Error("Cryptography API not available")})(e):new Uint8Array(t),Object.defineProperty(this,"bytes",{enumerable:!0,writable:!1,configurable:!1,value:this.bytes})}}class Bt{static get defaults(){return{issuer:"",label:"OTPAuth",issuerInLabel:!0,algorithm:"SHA1",digits:6,counter:0,window:1}}static generate({secret:t,algorithm:e=Bt.defaults.algorithm,digits:s=Bt.defaults.digits,counter:i=Bt.defaults.counter}){const r=((t,e,s)=>{if(b){const i=dt[t]??dt[ft(t)];return b(i,e,s)}throw new Error("Missing HMAC function")})(e,t.bytes,(t=>{const e=new ArrayBuffer(8),s=new Uint8Array(e);let i=t;for(let t=7;t>=0&&0!==i;t--)s[t]=255&i,i-=s[t],i/=256;return s})(i)),n=15&r[r.byteLength-1];return(((127&r[n])<<24|(255&r[n+1])<<16|(255&r[n+2])<<8|255&r[n+3])%10**s).toString().padStart(s,"0")}generate({counter:t=this.counter++}={}){return Bt.generate({secret:this.secret,algorithm:this.algorithm,digits:this.digits,counter:t})}static validate({token:t,secret:e,algorithm:s,digits:i=Bt.defaults.digits,counter:r=Bt.defaults.counter,window:n=Bt.defaults.window}){if(t.length!==i)return null;let o=null;const h=n=>{const h=Bt.generate({secret:e,algorithm:s,digits:i,counter:n});((t,e)=>{{if(t.length!==e.length)throw new TypeError("Input strings must have the same length");let s=-1,i=0;for(;++s<t.length;)i|=t.charCodeAt(s)^e.charCodeAt(s);return 0===i}})(t,h)&&(o=n-r)};h(r);for(let t=1;t<=n&&null===o&&(h(r-t),null===o)&&(h(r+t),null===o);++t);return o}validate({token:t,counter:e=this.counter,window:s}){return Bt.validate({token:t,secret:this.secret,algorithm:this.algorithm,digits:this.digits,counter:e,window:s})}toString(){const t=encodeURIComponent
;return"otpauth://hotp/"+(this.issuer.length>0?this.issuerInLabel?`${t(this.issuer)}:${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?`)+`secret=${t(this.secret.base32)}&`+`algorithm=${t(this.algorithm)}&`+`digits=${t(this.digits)}&`+`counter=${t(this.counter)}`}constructor({issuer:t=Bt.defaults.issuer,label:e=Bt.defaults.label,issuerInLabel:s=Bt.defaults.issuerInLabel,secret:i=new St,algorithm:r=Bt.defaults.algorithm,digits:n=Bt.defaults.digits,counter:o=Bt.defaults.counter}={}){this.issuer=t,this.label=e,this.issuerInLabel=s,this.secret="string"==typeof i?St.fromBase32(i):i,this.algorithm=ft(r),this.digits=n,this.counter=o}}class Et{static get defaults(){return{issuer:"",label:"OTPAuth",issuerInLabel:!0,algorithm:"SHA1",digits:6,period:30,window:1}}static counter({period:t=Et.defaults.period,timestamp:e=Date.now()}={}){return Math.floor(e/1e3/t)}counter({timestamp:t=Date.now()}={}){return Et.counter({period:this.period,timestamp:t})}static remaining({period:t=Et.defaults.period,timestamp:e=Date.now()}={}){return 1e3*t-e%(1e3*t)}remaining({timestamp:t=Date.now()}={}){return Et.remaining({period:this.period,timestamp:t})}static generate({secret:t,algorithm:e,digits:s,period:i=Et.defaults.period,timestamp:r=Date.now()}){return Bt.generate({secret:t,algorithm:e,digits:s,counter:Et.counter({period:i,timestamp:r})})}generate({timestamp:t=Date.now()}={}){return Et.generate({secret:this.secret,algorithm:this.algorithm,digits:this.digits,period:this.period,timestamp:t})}static validate({token:t,secret:e,algorithm:s,digits:i,period:r=Et.defaults.period,timestamp:n=Date.now(),window:o}){return Bt.validate({token:t,secret:e,algorithm:s,digits:i,counter:Et.counter({period:r,timestamp:n}),window:o})}validate({token:t,timestamp:e,window:s}){return Et.validate({token:t,secret:this.secret,algorithm:this.algorithm,digits:this.digits,period:this.period,timestamp:e,window:s})}toString(){const t=encodeURIComponent
;return"otpauth://totp/"+(this.issuer.length>0?this.issuerInLabel?`${t(this.issuer)}:${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?`)+`secret=${t(this.secret.base32)}&`+`algorithm=${t(this.algorithm)}&`+`digits=${t(this.digits)}&`+`period=${t(this.period)}`}constructor({issuer:t=Et.defaults.issuer,label:e=Et.defaults.label,issuerInLabel:s=Et.defaults.issuerInLabel,secret:i=new St,algorithm:r=Et.defaults.algorithm,digits:n=Et.defaults.digits,period:o=Et.defaults.period}={}){this.issuer=t,this.label=e,this.issuerInLabel=s,this.secret="string"==typeof i?St.fromBase32(i):i,this.algorithm=ft(r),this.digits=n,this.period=o}}const Ut=/^otpauth:\/\/([ht]otp)\/(.+)\?([A-Z0-9.~_-]+=[^?&]*(?:&[A-Z0-9.~_-]+=[^?&]*)*)$/i,Ct=/^[2-7A-Z]+=*$/i,Ot=/^SHA(?:1|224|256|384|512|3-224|3-256|3-384|3-512)$/i,vt=/^[+-]?\d+$/,kt=/^\+?[1-9]\d*$/;t.HOTP=Bt,t.Secret=St,t.TOTP=Et,t.URI=class{static parse(t){let e;try{e=t.match(Ut)}catch(t){}if(!Array.isArray(e))throw new URIError("Invalid URI format");const s=e[1].toLowerCase(),i=e[2].split(/(?::|%3A) *(.+)/i,2).map(decodeURIComponent),r=e[3].split("&").reduce(((t,e)=>{const s=e.split(/=(.*)/,2).map(decodeURIComponent),i=s[0].toLowerCase(),r=s[1],n=t;return n[i]=r,n}),{});let n;const o={};if("hotp"===s){if(n=Bt,void 0===r.counter||!vt.test(r.counter))throw new TypeError("Missing or invalid 'counter' parameter");o.counter=parseInt(r.counter,10)}else{if("totp"!==s)throw new TypeError("Unknown OTP type");if(n=Et,void 0!==r.period){if(!kt.test(r.period))throw new TypeError("Invalid 'period' parameter");o.period=parseInt(r.period,10)}}if(void 0!==r.issuer&&(o.issuer=r.issuer),2===i.length?(o.label=i[1],void 0===o.issuer||""===o.issuer?o.issuer=i[0]:""===i[0]&&(o.issuerInLabel=!1)):(o.label=i[0],void 0!==o.issuer&&""!==o.issuer&&(o.issuerInLabel=!1)),void 0===r.secret||!Ct.test(r.secret))throw new TypeError("Missing or invalid 'secret' parameter");if(o.secret=r.secret,void 0!==r.algorithm){
if(!Ot.test(r.algorithm))throw new TypeError("Invalid 'algorithm' parameter");o.algorithm=r.algorithm}if(void 0!==r.digits){if(!kt.test(r.digits))throw new TypeError("Invalid 'digits' parameter");o.digits=parseInt(r.digits,10)}return new n(o)}static stringify(t){if(t instanceof Bt||t instanceof Et)return t.toString();throw new TypeError("Invalid 'HOTP/TOTP' object")}},t.version="9.4.0"}));
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).OTPAuth={})}(this,function(t){"use strict";function e(t){if(!Number.isSafeInteger(t)||t<0)throw new Error("positive integer expected, got "+t)}function s(t,...e){if(!((s=t)instanceof Uint8Array||ArrayBuffer.isView(s)&&"Uint8Array"===s.constructor.name))throw new Error("Uint8Array expected");var s;if(e.length>0&&!e.includes(t.length))throw new Error("Uint8Array expected of length "+e+", got length="+t.length)}function i(t,e=!0){if(t.destroyed)throw new Error("Hash instance has been destroyed");if(e&&t.finished)throw new Error("Hash#digest() has already been called")}function r(t,e){s(t);const i=e.outputLen;if(t.length<i)throw new Error("digestInto() expects output buffer of length at least "+i)}function n(...t){for(let e=0;e<t.length;e++)t[e].fill(0)}function o(t){return new DataView(t.buffer,t.byteOffset,t.byteLength)}function h(t,e){return t<<32-e|t>>>e}function a(t,e){return t<<e|t>>>32-e>>>0}function c(t){return t<<24&4278190080|t<<8&16711680|t>>>8&65280|t>>>24&255}const l=(()=>68===new Uint8Array(new Uint32Array([287454020]).buffer)[0])()?t=>t:function(t){for(let e=0;e<t.length;e++)t[e]=c(t[e]);return t};function u(t){return"string"==typeof t&&(t=function(t){if("string"!=typeof t)throw new Error("string expected");return new Uint8Array((new TextEncoder).encode(t))}(t)),s(t),t}class f{}function d(t){const e=e=>t().update(u(e)).digest(),s=t();return e.outputLen=s.outputLen,e.blockLen=s.blockLen,e.create=()=>t(),e}class b extends f{update(t){return i(this),this.iHash.update(t),this}digestInto(t){i(this),s(t,this.outputLen),this.finished=!0,this.iHash.digestInto(t),this.oHash.update(t),this.oHash.digestInto(t),this.destroy()}digest(){const t=new Uint8Array(this.oHash.outputLen);return this.digestInto(t),t}_cloneInto(t){t||(t=Object.create(Object.getPrototypeOf(this),{}))
;const{oHash:e,iHash:s,finished:i,destroyed:r,blockLen:n,outputLen:o}=this;return t.finished=i,t.destroyed=r,t.blockLen=n,t.outputLen=o,t.oHash=e._cloneInto(t.oHash),t.iHash=s._cloneInto(t.iHash),t}clone(){return this._cloneInto()}destroy(){this.destroyed=!0,this.oHash.destroy(),this.iHash.destroy()}constructor(t,s){super(),this.finished=!1,this.destroyed=!1,function(t){if("function"!=typeof t||"function"!=typeof t.create)throw new Error("Hash should be wrapped by utils.createHasher");e(t.outputLen),e(t.blockLen)}(t);const i=u(s);if(this.iHash=t.create(),"function"!=typeof this.iHash.update)throw new Error("Expected instance of class which extends utils.Hash");this.blockLen=this.iHash.blockLen,this.outputLen=this.iHash.outputLen;const r=this.blockLen,o=new Uint8Array(r);o.set(i.length>r?t.create().update(i).digest():i);for(let t=0;t<o.length;t++)o[t]^=54;this.iHash.update(o),this.oHash=t.create();for(let t=0;t<o.length;t++)o[t]^=106;this.oHash.update(o),n(o)}}const g=(t,e,s)=>new b(t,e).update(s).digest();function p(t,e,s){return t&e^~t&s}function w(t,e,s){return t&e^t&s^e&s}g.create=(t,e)=>new b(t,e);class y extends f{update(t){i(this),s(t=u(t));const{view:e,buffer:r,blockLen:n}=this,h=t.length;for(let s=0;s<h;){const i=Math.min(n-this.pos,h-s);if(i===n){const e=o(t);for(;n<=h-s;s+=n)this.process(e,s);continue}r.set(t.subarray(s,s+i),this.pos),this.pos+=i,s+=i,this.pos===n&&(this.process(e,0),this.pos=0)}return this.length+=t.length,this.roundClean(),this}digestInto(t){i(this),r(t,this),this.finished=!0;const{buffer:e,view:s,blockLen:h,isLE:a}=this;let{pos:c}=this;e[c++]=128,n(this.buffer.subarray(c)),this.padOffset>h-c&&(this.process(s,0),c=0);for(let t=c;t<h;t++)e[t]=0;!function(t,e,s,i){if("function"==typeof t.setBigUint64)return t.setBigUint64(e,s,i);const r=BigInt(32),n=BigInt(4294967295),o=Number(s>>r&n),h=Number(s&n),a=i?4:0,c=i?0:4;t.setUint32(e+a,o,i),t.setUint32(e+c,h,i)}(s,h-8,BigInt(8*this.length),a),this.process(s,0);const l=o(t),u=this.outputLen
;if(u%4)throw new Error("_sha2: outputLen should be aligned to 32bit");const f=u/4,d=this.get();if(f>d.length)throw new Error("_sha2: outputLen bigger than state");for(let t=0;t<f;t++)l.setUint32(4*t,d[t],a)}digest(){const{buffer:t,outputLen:e}=this;this.digestInto(t);const s=t.slice(0,e);return this.destroy(),s}_cloneInto(t){t||(t=new this.constructor),t.set(...this.get());const{blockLen:e,buffer:s,length:i,finished:r,destroyed:n,pos:o}=this;return t.destroyed=n,t.finished=r,t.length=i,t.pos=o,i%e&&t.buffer.set(s),t}clone(){return this._cloneInto()}constructor(t,e,s,i){super(),this.finished=!1,this.length=0,this.pos=0,this.destroyed=!1,this.blockLen=t,this.outputLen=e,this.padOffset=s,this.isLE=i,this.buffer=new Uint8Array(t),this.view=o(this.buffer)}}const x=Uint32Array.from([1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225]),m=Uint32Array.from([3238371032,914150663,812702999,4144912697,4290775857,1750603025,1694076839,3204075428]),A=Uint32Array.from([3418070365,3238371032,1654270250,914150663,2438529370,812702999,355462360,4144912697,1731405415,4290775857,2394180231,1750603025,3675008525,1694076839,1203062813,3204075428]),H=Uint32Array.from([1779033703,4089235720,3144134277,2227873595,1013904242,4271175723,2773480762,1595750129,1359893119,2917565137,2600822924,725511199,528734635,4215389547,1541459225,327033209]),I=Uint32Array.from([1732584193,4023233417,2562383102,271733878,3285377520]),L=new Uint32Array(80);class E extends y{get(){const{A:t,B:e,C:s,D:i,E:r}=this;return[t,e,s,i,r]}set(t,e,s,i,r){this.A=0|t,this.B=0|e,this.C=0|s,this.D=0|i,this.E=0|r}process(t,e){for(let s=0;s<16;s++,e+=4)L[s]=t.getUint32(e,!1);for(let t=16;t<80;t++)L[t]=a(L[t-3]^L[t-8]^L[t-14]^L[t-16],1);let{A:s,B:i,C:r,D:n,E:o}=this;for(let t=0;t<80;t++){let e,h;t<20?(e=p(i,r,n),h=1518500249):t<40?(e=i^r^n,h=1859775393):t<60?(e=w(i,r,n),h=2400959708):(e=i^r^n,h=3395469782);const c=a(s,5)+e+o+h+L[t]|0;o=n,n=r,r=a(i,30),i=s,s=c}s=s+this.A|0,i=i+this.B|0,r=r+this.C|0,n=n+this.D|0,o=o+this.E|0,
this.set(s,i,r,n,o)}roundClean(){n(L)}destroy(){this.set(0,0,0,0,0),n(this.buffer)}constructor(){super(64,20,8,!1),this.A=0|I[0],this.B=0|I[1],this.C=0|I[2],this.D=0|I[3],this.E=0|I[4]}}const U=d(()=>new E),B=BigInt(2**32-1),S=BigInt(32);function O(t,e=!1){return e?{h:Number(t&B),l:Number(t>>S&B)}:{h:0|Number(t>>S&B),l:0|Number(t&B)}}function C(t,e=!1){const s=t.length;let i=new Uint32Array(s),r=new Uint32Array(s);for(let n=0;n<s;n++){const{h:s,l:o}=O(t[n],e);[i[n],r[n]]=[s,o]}return[i,r]}const v=(t,e,s)=>t>>>s,k=(t,e,s)=>t<<32-s|e>>>s,$=(t,e,s)=>t>>>s|e<<32-s,T=(t,e,s)=>t<<32-s|e>>>s,D=(t,e,s)=>t<<64-s|e>>>s-32,_=(t,e,s)=>t>>>s-32|e<<64-s;function F(t,e,s,i){const r=(e>>>0)+(i>>>0);return{h:t+s+(r/2**32|0)|0,l:0|r}}const G=(t,e,s)=>(t>>>0)+(e>>>0)+(s>>>0),P=(t,e,s,i)=>e+s+i+(t/2**32|0)|0,j=(t,e,s,i)=>(t>>>0)+(e>>>0)+(s>>>0)+(i>>>0),M=(t,e,s,i,r)=>e+s+i+r+(t/2**32|0)|0,R=(t,e,s,i,r)=>(t>>>0)+(e>>>0)+(s>>>0)+(i>>>0)+(r>>>0),N=(t,e,s,i,r,n)=>e+s+i+r+n+(t/2**32|0)|0,X=Uint32Array.from([1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298]),V=new Uint32Array(64);class Z extends y{get(){const{A:t,B:e,C:s,D:i,E:r,F:n,G:o,H:h}=this;return[t,e,s,i,r,n,o,h]}set(t,e,s,i,r,n,o,h){this.A=0|t,this.B=0|e,this.C=0|s,this.D=0|i,this.E=0|r,this.F=0|n,this.G=0|o,this.H=0|h}process(t,e){for(let s=0;s<16;s++,e+=4)V[s]=t.getUint32(e,!1);for(let t=16;t<64;t++){
const e=V[t-15],s=V[t-2],i=h(e,7)^h(e,18)^e>>>3,r=h(s,17)^h(s,19)^s>>>10;V[t]=r+V[t-7]+i+V[t-16]|0}let{A:s,B:i,C:r,D:n,E:o,F:a,G:c,H:l}=this;for(let t=0;t<64;t++){const e=l+(h(o,6)^h(o,11)^h(o,25))+p(o,a,c)+X[t]+V[t]|0,u=(h(s,2)^h(s,13)^h(s,22))+w(s,i,r)|0;l=c,c=a,a=o,o=n+e|0,n=r,r=i,i=s,s=e+u|0}s=s+this.A|0,i=i+this.B|0,r=r+this.C|0,n=n+this.D|0,o=o+this.E|0,a=a+this.F|0,c=c+this.G|0,l=l+this.H|0,this.set(s,i,r,n,o,a,c,l)}roundClean(){n(V)}destroy(){this.set(0,0,0,0,0,0,0,0),n(this.buffer)}constructor(t=32){super(64,t,8,!1),this.A=0|x[0],this.B=0|x[1],this.C=0|x[2],this.D=0|x[3],this.E=0|x[4],this.F=0|x[5],this.G=0|x[6],this.H=0|x[7]}}class z extends Z{constructor(){super(28),this.A=0|m[0],this.B=0|m[1],this.C=0|m[2],this.D=0|m[3],this.E=0|m[4],this.F=0|m[5],this.G=0|m[6],this.H=0|m[7]}}
const J=(()=>C(["0x428a2f98d728ae22","0x7137449123ef65cd","0xb5c0fbcfec4d3b2f","0xe9b5dba58189dbbc","0x3956c25bf348b538","0x59f111f1b605d019","0x923f82a4af194f9b","0xab1c5ed5da6d8118","0xd807aa98a3030242","0x12835b0145706fbe","0x243185be4ee4b28c","0x550c7dc3d5ffb4e2","0x72be5d74f27b896f","0x80deb1fe3b1696b1","0x9bdc06a725c71235","0xc19bf174cf692694","0xe49b69c19ef14ad2","0xefbe4786384f25e3","0x0fc19dc68b8cd5b5","0x240ca1cc77ac9c65","0x2de92c6f592b0275","0x4a7484aa6ea6e483","0x5cb0a9dcbd41fbd4","0x76f988da831153b5","0x983e5152ee66dfab","0xa831c66d2db43210","0xb00327c898fb213f","0xbf597fc7beef0ee4","0xc6e00bf33da88fc2","0xd5a79147930aa725","0x06ca6351e003826f","0x142929670a0e6e70","0x27b70a8546d22ffc","0x2e1b21385c26c926","0x4d2c6dfc5ac42aed","0x53380d139d95b3df","0x650a73548baf63de","0x766a0abb3c77b2a8","0x81c2c92e47edaee6","0x92722c851482353b","0xa2bfe8a14cf10364","0xa81a664bbc423001","0xc24b8b70d0f89791","0xc76c51a30654be30","0xd192e819d6ef5218","0xd69906245565a910","0xf40e35855771202a","0x106aa07032bbd1b8","0x19a4c116b8d2d0c8","0x1e376c085141ab53","0x2748774cdf8eeb99","0x34b0bcb5e19b48a8","0x391c0cb3c5c95a63","0x4ed8aa4ae3418acb","0x5b9cca4f7763e373","0x682e6ff3d6b2b8a3","0x748f82ee5defb2fc","0x78a5636f43172f60","0x84c87814a1f0ab72","0x8cc702081a6439ec","0x90befffa23631e28","0xa4506cebde82bde9","0xbef9a3f7b2c67915","0xc67178f2e372532b","0xca273eceea26619c","0xd186b8c721c0c207","0xeada7dd6cde0eb1e","0xf57d4f7fee6ed178","0x06f067aa72176fba","0x0a637dc5a2c898a6","0x113f9804bef90dae","0x1b710b35131c471b","0x28db77f523047d84","0x32caab7b40c72493","0x3c9ebe0a15c9bebc","0x431d67c49c100d4c","0x4cc5d4becb3e42b6","0x597f299cfc657e2a","0x5fcb6fab3ad6faec","0x6c44198c4a475817"].map(t=>BigInt(t))))(),K=(()=>J[0])(),Q=(()=>J[1])(),W=new Uint32Array(80),Y=new Uint32Array(80);class q extends y{get(){const{Ah:t,Al:e,Bh:s,Bl:i,Ch:r,Cl:n,Dh:o,Dl:h,Eh:a,El:c,Fh:l,Fl:u,Gh:f,Gl:d,Hh:b,Hl:g}=this;return[t,e,s,i,r,n,o,h,a,c,l,u,f,d,b,g]}set(t,e,s,i,r,n,o,h,a,c,l,u,f,d,b,g){this.Ah=0|t,this.Al=0|e,this.Bh=0|s,this.Bl=0|i,this.Ch=0|r,
this.Cl=0|n,this.Dh=0|o,this.Dl=0|h,this.Eh=0|a,this.El=0|c,this.Fh=0|l,this.Fl=0|u,this.Gh=0|f,this.Gl=0|d,this.Hh=0|b,this.Hl=0|g}process(t,e){for(let s=0;s<16;s++,e+=4)W[s]=t.getUint32(e),Y[s]=t.getUint32(e+=4);for(let t=16;t<80;t++){const e=0|W[t-15],s=0|Y[t-15],i=$(e,s,1)^$(e,s,8)^v(e,0,7),r=T(e,s,1)^T(e,s,8)^k(e,s,7),n=0|W[t-2],o=0|Y[t-2],h=$(n,o,19)^D(n,o,61)^v(n,0,6),a=T(n,o,19)^_(n,o,61)^k(n,o,6),c=j(r,a,Y[t-7],Y[t-16]),l=M(c,i,h,W[t-7],W[t-16]);W[t]=0|l,Y[t]=0|c}let{Ah:s,Al:i,Bh:r,Bl:n,Ch:o,Cl:h,Dh:a,Dl:c,Eh:l,El:u,Fh:f,Fl:d,Gh:b,Gl:g,Hh:p,Hl:w}=this;for(let t=0;t<80;t++){const e=$(l,u,14)^$(l,u,18)^D(l,u,41),y=T(l,u,14)^T(l,u,18)^_(l,u,41),x=l&f^~l&b,m=R(w,y,u&d^~u&g,Q[t],Y[t]),A=N(m,p,e,x,K[t],W[t]),H=0|m,I=$(s,i,28)^D(s,i,34)^D(s,i,39),L=T(s,i,28)^_(s,i,34)^_(s,i,39),E=s&r^s&o^r&o,U=i&n^i&h^n&h;p=0|b,w=0|g,b=0|f,g=0|d,f=0|l,d=0|u,({h:l,l:u}=F(0|a,0|c,0|A,0|H)),a=0|o,c=0|h,o=0|r,h=0|n,r=0|s,n=0|i;const B=G(H,L,U);s=P(B,A,I,E),i=0|B}({h:s,l:i}=F(0|this.Ah,0|this.Al,0|s,0|i)),({h:r,l:n}=F(0|this.Bh,0|this.Bl,0|r,0|n)),({h:o,l:h}=F(0|this.Ch,0|this.Cl,0|o,0|h)),({h:a,l:c}=F(0|this.Dh,0|this.Dl,0|a,0|c)),({h:l,l:u}=F(0|this.Eh,0|this.El,0|l,0|u)),({h:f,l:d}=F(0|this.Fh,0|this.Fl,0|f,0|d)),({h:b,l:g}=F(0|this.Gh,0|this.Gl,0|b,0|g)),({h:p,l:w}=F(0|this.Hh,0|this.Hl,0|p,0|w)),this.set(s,i,r,n,o,h,a,c,l,u,f,d,b,g,p,w)}roundClean(){n(W,Y)}destroy(){n(this.buffer),this.set(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)}constructor(t=64){super(128,t,16,!1),this.Ah=0|H[0],this.Al=0|H[1],this.Bh=0|H[2],this.Bl=0|H[3],this.Ch=0|H[4],this.Cl=0|H[5],this.Dh=0|H[6],this.Dl=0|H[7],this.Eh=0|H[8],this.El=0|H[9],this.Fh=0|H[10],this.Fl=0|H[11],this.Gh=0|H[12],this.Gl=0|H[13],this.Hh=0|H[14],this.Hl=0|H[15]}}class tt extends q{constructor(){super(48),this.Ah=0|A[0],this.Al=0|A[1],this.Bh=0|A[2],this.Bl=0|A[3],this.Ch=0|A[4],this.Cl=0|A[5],this.Dh=0|A[6],this.Dl=0|A[7],this.Eh=0|A[8],this.El=0|A[9],this.Fh=0|A[10],this.Fl=0|A[11],this.Gh=0|A[12],this.Gl=0|A[13],this.Hh=0|A[14],this.Hl=0|A[15]}}
const et=d(()=>new Z),st=d(()=>new z),it=d(()=>new q),rt=d(()=>new tt),nt=BigInt(0),ot=BigInt(1),ht=BigInt(2),at=BigInt(7),ct=BigInt(256),lt=BigInt(113),ut=[],ft=[],dt=[];for(let t=0,e=ot,s=1,i=0;t<24;t++){[s,i]=[i,(2*s+3*i)%5],ut.push(2*(5*i+s)),ft.push((t+1)*(t+2)/2%64);let r=nt;for(let t=0;t<7;t++)e=(e<<ot^(e>>at)*lt)%ct,e&ht&&(r^=ot<<(ot<<BigInt(t))-ot);dt.push(r)}const bt=C(dt,!0),gt=bt[0],pt=bt[1],wt=(t,e,s)=>s>32?((t,e,s)=>e<<s-32|t>>>64-s)(t,e,s):((t,e,s)=>t<<s|e>>>32-s)(t,e,s),yt=(t,e,s)=>s>32?((t,e,s)=>t<<s-32|e>>>64-s)(t,e,s):((t,e,s)=>e<<s|t>>>32-s)(t,e,s);class xt extends f{clone(){return this._cloneInto()}keccak(){l(this.state32),function(t,e=24){const s=new Uint32Array(10);for(let i=24-e;i<24;i++){for(let e=0;e<10;e++)s[e]=t[e]^t[e+10]^t[e+20]^t[e+30]^t[e+40];for(let e=0;e<10;e+=2){const i=(e+8)%10,r=(e+2)%10,n=s[r],o=s[r+1],h=wt(n,o,1)^s[i],a=yt(n,o,1)^s[i+1];for(let s=0;s<50;s+=10)t[e+s]^=h,t[e+s+1]^=a}let e=t[2],r=t[3];for(let s=0;s<24;s++){const i=ft[s],n=wt(e,r,i),o=yt(e,r,i),h=ut[s];e=t[h],r=t[h+1],t[h]=n,t[h+1]=o}for(let e=0;e<50;e+=10){for(let i=0;i<10;i++)s[i]=t[e+i];for(let i=0;i<10;i++)t[e+i]^=~s[(i+2)%10]&s[(i+4)%10]}t[0]^=gt[i],t[1]^=pt[i]}n(s)}(this.state32,this.rounds),l(this.state32),this.posOut=0,this.pos=0}update(t){i(this),s(t=u(t));const{blockLen:e,state:r}=this,n=t.length;for(let s=0;s<n;){const i=Math.min(e-this.pos,n-s);for(let e=0;e<i;e++)r[this.pos++]^=t[s++];this.pos===e&&this.keccak()}return this}finish(){if(this.finished)return;this.finished=!0;const{state:t,suffix:e,pos:s,blockLen:i}=this;t[s]^=e,128&e&&s===i-1&&this.keccak(),t[i-1]^=128,this.keccak()}writeInto(t){i(this,!1),s(t),this.finish();const e=this.state,{blockLen:r}=this;for(let s=0,i=t.length;s<i;){this.posOut>=r&&this.keccak();const n=Math.min(r-this.posOut,i-s);t.set(e.subarray(this.posOut,this.posOut+n),s),this.posOut+=n,s+=n}return t}xofInto(t){if(!this.enableXOF)throw new Error("XOF is not possible for this instance");return this.writeInto(t)}xof(t){return e(t),this.xofInto(new Uint8Array(t))}
digestInto(t){if(r(t,this),this.finished)throw new Error("digest() was already called");return this.writeInto(t),this.destroy(),t}digest(){return this.digestInto(new Uint8Array(this.outputLen))}destroy(){this.destroyed=!0,n(this.state)}_cloneInto(t){const{blockLen:e,suffix:s,outputLen:i,rounds:r,enableXOF:n}=this;return t||(t=new xt(e,s,i,n,r)),t.state32.set(this.state32),t.pos=this.pos,t.posOut=this.posOut,t.finished=this.finished,t.rounds=r,t.suffix=s,t.outputLen=i,t.enableXOF=n,t.destroyed=this.destroyed,t}constructor(t,s,i,r=!1,n=24){if(super(),this.pos=0,this.posOut=0,this.finished=!1,this.destroyed=!1,this.enableXOF=!1,this.blockLen=t,this.suffix=s,this.outputLen=i,this.enableXOF=r,this.rounds=n,e(i),!(0<t&&t<200))throw new Error("only keccak-f1600 function is supported");var o;this.state=new Uint8Array(200),this.state32=(o=this.state,new Uint32Array(o.buffer,o.byteOffset,Math.floor(o.byteLength/4)))}}const mt=(t,e,s)=>d(()=>new xt(e,t,s)),At=(()=>mt(6,144,28))(),Ht=(()=>mt(6,136,32))(),It=(()=>mt(6,104,48))(),Lt=(()=>mt(6,72,64))(),Et=(()=>{if("object"==typeof globalThis)return globalThis;Object.defineProperty(Object.prototype,"__GLOBALTHIS__",{get(){return this},configurable:!0});try{if("undefined"!=typeof __GLOBALTHIS__)return __GLOBALTHIS__}finally{delete Object.prototype.__GLOBALTHIS__}return"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:void 0})(),Ut={SHA1:U,SHA224:st,SHA256:et,SHA384:rt,SHA512:it,"SHA3-224":At,"SHA3-256":Ht,"SHA3-384":It,"SHA3-512":Lt},Bt=t=>{switch(!0){case/^(?:SHA-?1|SSL3-SHA1)$/i.test(t):return"SHA1";case/^SHA(?:2?-)?224$/i.test(t):return"SHA224";case/^SHA(?:2?-)?256$/i.test(t):return"SHA256";case/^SHA(?:2?-)?384$/i.test(t):return"SHA384";case/^SHA(?:2?-)?512$/i.test(t):return"SHA512";case/^SHA3-224$/i.test(t):return"SHA3-224";case/^SHA3-256$/i.test(t):return"SHA3-256";case/^SHA3-384$/i.test(t):return"SHA3-384";case/^SHA3-512$/i.test(t):return"SHA3-512";default:throw new TypeError(`Unknown hash algorithm: ${t}`)}
},St="ABCDEFGHIJKLMNOPQRSTUVWXYZ234567",Ot=t=>{let e=(t=t.replace(/ /g,"")).length;for(;"="===t[e-1];)--e;t=(e<t.length?t.substring(0,e):t).toUpperCase();const s=new ArrayBuffer(5*t.length/8|0),i=new Uint8Array(s);let r=0,n=0,o=0;for(let e=0;e<t.length;e++){const s=St.indexOf(t[e]);if(-1===s)throw new TypeError(`Invalid character found: ${t[e]}`);n=n<<5|s,r+=5,r>=8&&(r-=8,i[o++]=n>>>r)}return i},Ct=t=>{let e=0,s=0,i="";for(let r=0;r<t.length;r++)for(s=s<<8|t[r],e+=8;e>=5;)i+=St[s>>>e-5&31],e-=5;return e>0&&(i+=St[s<<5-e&31]),i},vt=t=>{t=t.replace(/ /g,"");const e=new ArrayBuffer(t.length/2),s=new Uint8Array(e);for(let e=0;e<t.length;e+=2)s[e/2]=parseInt(t.substring(e,e+2),16);return s},kt=t=>{let e="";for(let s=0;s<t.length;s++){const i=t[s].toString(16);1===i.length&&(e+="0"),e+=i}return e.toUpperCase()},$t=t=>{const e=new ArrayBuffer(t.length),s=new Uint8Array(e);for(let e=0;e<t.length;e++)s[e]=255&t.charCodeAt(e);return s},Tt=t=>{let e="";for(let s=0;s<t.length;s++)e+=String.fromCharCode(t[s]);return e},Dt=Et.TextEncoder?new Et.TextEncoder:null,_t=Et.TextDecoder?new Et.TextDecoder:null,Ft=t=>{if(!Dt)throw new Error("Encoding API not available");return Dt.encode(t)},Gt=t=>{if(!_t)throw new Error("Encoding API not available");return _t.decode(t)};class Pt{static fromLatin1(t){return new Pt({buffer:$t(t).buffer})}static fromUTF8(t){return new Pt({buffer:Ft(t).buffer})}static fromBase32(t){return new Pt({buffer:Ot(t).buffer})}static fromHex(t){return new Pt({buffer:vt(t).buffer})}get buffer(){return this.bytes.buffer}get latin1(){return Object.defineProperty(this,"latin1",{enumerable:!0,writable:!1,configurable:!1,value:Tt(this.bytes)}),this.latin1}get utf8(){return Object.defineProperty(this,"utf8",{enumerable:!0,writable:!1,configurable:!1,value:Gt(this.bytes)}),this.utf8}get base32(){return Object.defineProperty(this,"base32",{enumerable:!0,writable:!1,configurable:!1,value:Ct(this.bytes)}),this.base32}get hex(){return Object.defineProperty(this,"hex",{enumerable:!0,writable:!1,configurable:!1,
value:kt(this.bytes)}),this.hex}constructor({buffer:t,size:e=20}={}){this.bytes=void 0===t?(t=>{if(Et.crypto?.getRandomValues)return Et.crypto.getRandomValues(new Uint8Array(t));throw new Error("Cryptography API not available")})(e):new Uint8Array(t),Object.defineProperty(this,"bytes",{enumerable:!0,writable:!1,configurable:!1,value:this.bytes})}}class jt{static get defaults(){return{issuer:"",label:"OTPAuth",issuerInLabel:!0,algorithm:"SHA1",digits:6,counter:0,window:1}}static generate({secret:t,algorithm:e=jt.defaults.algorithm,digits:s=jt.defaults.digits,counter:i=jt.defaults.counter}){const r=((t,e,s)=>{if(g){const i=Ut[t]??Ut[Bt(t)];return g(i,e,s)}throw new Error("Missing HMAC function")})(e,t.bytes,(t=>{const e=new ArrayBuffer(8),s=new Uint8Array(e);let i=t;for(let t=7;t>=0&&0!==i;t--)s[t]=255&i,i-=s[t],i/=256;return s})(i)),n=15&r[r.byteLength-1];return(((127&r[n])<<24|(255&r[n+1])<<16|(255&r[n+2])<<8|255&r[n+3])%10**s).toString().padStart(s,"0")}generate({counter:t=this.counter++}={}){return jt.generate({secret:this.secret,algorithm:this.algorithm,digits:this.digits,counter:t})}static validate({token:t,secret:e,algorithm:s,digits:i=jt.defaults.digits,counter:r=jt.defaults.counter,window:n=jt.defaults.window}){if(t.length!==i)return null;let o=null;const h=n=>{const h=jt.generate({secret:e,algorithm:s,digits:i,counter:n});((t,e)=>{{if(t.length!==e.length)throw new TypeError("Input strings must have the same length");let s=-1,i=0;for(;++s<t.length;)i|=t.charCodeAt(s)^e.charCodeAt(s);return 0===i}})(t,h)&&(o=n-r)};h(r);for(let t=1;t<=n&&null===o&&(h(r-t),null===o)&&(h(r+t),null===o);++t);return o}validate({token:t,counter:e=this.counter,window:s}){return jt.validate({token:t,secret:this.secret,algorithm:this.algorithm,digits:this.digits,counter:e,window:s})}toString(){const t=encodeURIComponent
;return"otpauth://hotp/"+(this.issuer.length>0?this.issuerInLabel?`${t(this.issuer)}:${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?`)+`secret=${t(this.secret.base32)}&`+`algorithm=${t(this.algorithm)}&`+`digits=${t(this.digits)}&`+`counter=${t(this.counter)}`}constructor({issuer:t=jt.defaults.issuer,label:e=jt.defaults.label,issuerInLabel:s=jt.defaults.issuerInLabel,secret:i=new Pt,algorithm:r=jt.defaults.algorithm,digits:n=jt.defaults.digits,counter:o=jt.defaults.counter}={}){this.issuer=t,this.label=e,this.issuerInLabel=s,this.secret="string"==typeof i?Pt.fromBase32(i):i,this.algorithm=Bt(r),this.digits=n,this.counter=o}}class Mt{static get defaults(){return{issuer:"",label:"OTPAuth",issuerInLabel:!0,algorithm:"SHA1",digits:6,period:30,window:1}}static counter({period:t=Mt.defaults.period,timestamp:e=Date.now()}={}){return Math.floor(e/1e3/t)}counter({timestamp:t=Date.now()}={}){return Mt.counter({period:this.period,timestamp:t})}static remaining({period:t=Mt.defaults.period,timestamp:e=Date.now()}={}){return 1e3*t-e%(1e3*t)}remaining({timestamp:t=Date.now()}={}){return Mt.remaining({period:this.period,timestamp:t})}static generate({secret:t,algorithm:e,digits:s,period:i=Mt.defaults.period,timestamp:r=Date.now()}){return jt.generate({secret:t,algorithm:e,digits:s,counter:Mt.counter({period:i,timestamp:r})})}generate({timestamp:t=Date.now()}={}){return Mt.generate({secret:this.secret,algorithm:this.algorithm,digits:this.digits,period:this.period,timestamp:t})}static validate({token:t,secret:e,algorithm:s,digits:i,period:r=Mt.defaults.period,timestamp:n=Date.now(),window:o}){return jt.validate({token:t,secret:e,algorithm:s,digits:i,counter:Mt.counter({period:r,timestamp:n}),window:o})}validate({token:t,timestamp:e,window:s}){return Mt.validate({token:t,secret:this.secret,algorithm:this.algorithm,digits:this.digits,period:this.period,timestamp:e,window:s})}toString(){const t=encodeURIComponent
;return"otpauth://totp/"+(this.issuer.length>0?this.issuerInLabel?`${t(this.issuer)}:${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?issuer=${t(this.issuer)}&`:`${t(this.label)}?`)+`secret=${t(this.secret.base32)}&`+`algorithm=${t(this.algorithm)}&`+`digits=${t(this.digits)}&`+`period=${t(this.period)}`}constructor({issuer:t=Mt.defaults.issuer,label:e=Mt.defaults.label,issuerInLabel:s=Mt.defaults.issuerInLabel,secret:i=new Pt,algorithm:r=Mt.defaults.algorithm,digits:n=Mt.defaults.digits,period:o=Mt.defaults.period}={}){this.issuer=t,this.label=e,this.issuerInLabel=s,this.secret="string"==typeof i?Pt.fromBase32(i):i,this.algorithm=Bt(r),this.digits=n,this.period=o}}const Rt=/^otpauth:\/\/([ht]otp)\/(.+)\?([A-Z0-9.~_-]+=[^?&]*(?:&[A-Z0-9.~_-]+=[^?&]*)*)$/i,Nt=/^[2-7A-Z]+=*$/i,Xt=/^SHA(?:1|224|256|384|512|3-224|3-256|3-384|3-512)$/i,Vt=/^[+-]?\d+$/,Zt=/^\+?[1-9]\d*$/;t.HOTP=jt,t.Secret=Pt,t.TOTP=Mt,t.URI=class{static parse(t){let e;try{e=t.match(Rt)}catch(t){}if(!Array.isArray(e))throw new URIError("Invalid URI format");const s=e[1].toLowerCase(),i=e[2].split(/(?::|%3A) *(.+)/i,2).map(decodeURIComponent),r=e[3].split("&").reduce((t,e)=>{const s=e.split(/=(.*)/,2).map(decodeURIComponent),i=s[0].toLowerCase(),r=s[1],n=t;return n[i]=r,n},{});let n;const o={};if("hotp"===s){if(n=jt,void 0===r.counter||!Vt.test(r.counter))throw new TypeError("Missing or invalid 'counter' parameter");o.counter=parseInt(r.counter,10)}else{if("totp"!==s)throw new TypeError("Unknown OTP type");if(n=Mt,void 0!==r.period){if(!Zt.test(r.period))throw new TypeError("Invalid 'period' parameter");o.period=parseInt(r.period,10)}}if(void 0!==r.issuer&&(o.issuer=r.issuer),2===i.length?(o.label=i[1],void 0===o.issuer||""===o.issuer?o.issuer=i[0]:""===i[0]&&(o.issuerInLabel=!1)):(o.label=i[0],void 0!==o.issuer&&""!==o.issuer&&(o.issuerInLabel=!1)),void 0===r.secret||!Nt.test(r.secret))throw new TypeError("Missing or invalid 'secret' parameter");if(o.secret=r.secret,void 0!==r.algorithm){
if(!Xt.test(r.algorithm))throw new TypeError("Invalid 'algorithm' parameter");o.algorithm=r.algorithm}if(void 0!==r.digits){if(!Zt.test(r.digits))throw new TypeError("Invalid 'digits' parameter");o.digits=parseInt(r.digits,10)}return new n(o)}static stringify(t){if(t instanceof jt||t instanceof Mt)return t.toString();throw new TypeError("Invalid 'HOTP/TOTP' object")}},t.version="9.4.1"});
//# sourceMappingURL=otpauth.umd.min.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,10 @@
package controller
import (
"net/http"
"github.com/mhsanaei/3x-ui/v2/web/service"
"github.com/mhsanaei/3x-ui/v2/web/session"
"github.com/gin-gonic/gin"
)
@ -21,11 +24,21 @@ func NewAPIController(g *gin.RouterGroup) *APIController {
return a
}
// checkAPIAuth is a middleware that returns 404 for unauthenticated API requests
// to hide the existence of API endpoints from unauthorized users
func (a *APIController) checkAPIAuth(c *gin.Context) {
if !session.IsLogin(c) {
c.AbortWithStatus(http.StatusNotFound)
return
}
c.Next()
}
// initRouter sets up the API routes for inbounds, server, and other endpoints.
func (a *APIController) initRouter(g *gin.RouterGroup) {
// Main API group
api := g.Group("/panel/api")
api.Use(a.checkLogin)
api.Use(a.checkAPIAuth)
// Inbounds API
inbounds := api.Group("/inbounds")

View file

@ -39,8 +39,9 @@ func NewIndexController(g *gin.RouterGroup) *IndexController {
// initRouter sets up the routes for index, login, logout, and two-factor authentication.
func (a *IndexController) initRouter(g *gin.RouterGroup) {
g.GET("/", a.index)
g.POST("/login", a.login)
g.GET("/logout", a.logout)
g.POST("/login", a.login)
g.POST("/getTwoFactorEnable", a.getTwoFactorEnable)
}

View file

@ -8,8 +8,6 @@ import (
type XUIController struct {
BaseController
inboundController *InboundController
serverController *ServerController
settingController *SettingController
xraySettingController *XraySettingController
}
@ -31,8 +29,6 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
g.GET("/settings", a.settings)
g.GET("/xray", a.xraySettings)
a.inboundController = NewInboundController(g)
a.serverController = NewServerController(g)
a.settingController = NewSettingController(g)
a.xraySettingController = NewXraySettingController(g)
}

View file

@ -74,7 +74,31 @@ type AllSetting struct {
SubJsonFragment string `json:"subJsonFragment" form:"subJsonFragment"` // JSON subscription fragment configuration
SubJsonNoises string `json:"subJsonNoises" form:"subJsonNoises"` // JSON subscription noise configuration
SubJsonMux string `json:"subJsonMux" form:"subJsonMux"` // JSON subscription mux configuration
SubJsonRules string `json:"subJsonRules" form:"subJsonRules"` // JSON subscription routing rules
SubJsonRules string `json:"subJsonRules" form:"subJsonRules"`
// LDAP settings
LdapEnable bool `json:"ldapEnable" form:"ldapEnable"`
LdapHost string `json:"ldapHost" form:"ldapHost"`
LdapPort int `json:"ldapPort" form:"ldapPort"`
LdapUseTLS bool `json:"ldapUseTLS" form:"ldapUseTLS"`
LdapBindDN string `json:"ldapBindDN" form:"ldapBindDN"`
LdapPassword string `json:"ldapPassword" form:"ldapPassword"`
LdapBaseDN string `json:"ldapBaseDN" form:"ldapBaseDN"`
LdapUserFilter string `json:"ldapUserFilter" form:"ldapUserFilter"`
LdapUserAttr string `json:"ldapUserAttr" form:"ldapUserAttr"` // e.g., mail or uid
LdapVlessField string `json:"ldapVlessField" form:"ldapVlessField"`
LdapSyncCron string `json:"ldapSyncCron" form:"ldapSyncCron"`
// Generic flag configuration
LdapFlagField string `json:"ldapFlagField" form:"ldapFlagField"`
LdapTruthyValues string `json:"ldapTruthyValues" form:"ldapTruthyValues"`
LdapInvertFlag bool `json:"ldapInvertFlag" form:"ldapInvertFlag"`
LdapInboundTags string `json:"ldapInboundTags" form:"ldapInboundTags"`
LdapAutoCreate bool `json:"ldapAutoCreate" form:"ldapAutoCreate"`
LdapAutoDelete bool `json:"ldapAutoDelete" form:"ldapAutoDelete"`
LdapDefaultTotalGB int `json:"ldapDefaultTotalGB" form:"ldapDefaultTotalGB"`
LdapDefaultExpiryDays int `json:"ldapDefaultExpiryDays" form:"ldapDefaultExpiryDays"`
LdapDefaultLimitIP int `json:"ldapDefaultLimitIP" form:"ldapDefaultLimitIP"`
// JSON subscription routing rules
}
// CheckValid validates all settings in the AllSetting struct, checking IP addresses, ports, SSL certificates, and other configuration values.

View file

@ -28,7 +28,7 @@
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.port" }}'>
<a-input-number v-model.number="inbound.port" :min="1" :max="65531"></a-input-number>
<a-input-number v-model.number="inbound.port" :min="1" :max="65535"></a-input-number>
</a-form-item>
<a-form-item>
@ -52,7 +52,8 @@
<br v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0">
<span v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0">
<strong>{{ i18n "pages.inbounds.lastReset" }}:</strong>
<span v-if="datepicker == 'gregorian'">[[ moment(dbInbound.lastTrafficResetTime).format('YYYY-MM-DD HH:mm:ss') ]]</span>
<span v-if="datepicker == 'gregorian'">[[
moment(dbInbound.lastTrafficResetTime).format('YYYY-MM-DD HH:mm:ss') ]]</span>
<span v-else>[[ DateUtil.convertToJalalian(moment(dbInbound.lastTrafficResetTime)) ]]</span>
</span>
</template>

View file

@ -3,12 +3,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 icon="plus" v-if="externalProxy" type="primary" :style="{ marginLeft: '10px' }" size="small" @click="inbound.stream.externalProxy.push({forceTls: 'same', dest: '', port: 443, remark: ''})"></a-button>
<a-button icon="plus" v-if="externalProxy" type="primary" :style="{ marginLeft: '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 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>
@ -17,7 +19,7 @@
</template>
<a-input :style="{ width: '30%' }" 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-input-number :style="{ width: '15%' }" v-model.number="row.port" min="1" max="65535"></a-input-number>
</a-tooltip>
<a-input :style="{ width: '30%', top: '0' }" v-model.trim="row.remark" placeholder='{{ i18n "remark" }}'>
<template slot="addonAfter">

View file

@ -4,7 +4,7 @@
{{ template "page/body_start" .}}
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' login-app'">
<transition name="list" appear>
<a-layout-content class="under min-h-0">
<a-layout-content class="under min-h-0">
<div class="waves-header">
<div class="waves-inner-header"></div>
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
@ -20,7 +20,7 @@
</g>
</svg>
</div>
<a-row type="flex" justify="center" align="middle" class="h-100 overflow-y-auto overflow-x-hidden">
<a-row type="flex" justify="center" align="middle" class="h-100 overflow-y-auto overflow-x-hidden">
<a-col :xs="22" :sm="12" :md="10" :lg="8" :xl="6" :xxl="5" id="login" class="my-3rem">
<template v-if="!loadingStates.fetched">
<div class="text-center">
@ -35,8 +35,8 @@
<a-space direction="vertical" :size="10">
<a-theme-switch-login></a-theme-switch-login>
<span>{{ i18n "pages.settings.language" }}</span>
<a-select ref="selectLang" class="w-100" v-model="lang"
@change="LanguageManager.setLanguage(lang)" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select ref="selectLang" class="w-100" v-model="lang" @change="LanguageManager.setLanguage(lang)"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="l.value" label="English" v-for="l in LanguageManager.supportedLanguages">
<span role="img" aria-label="l.name" v-text="l.icon"></span>
&nbsp;&nbsp;<span v-text="l.name"></span>
@ -68,7 +68,7 @@
</a-input>
</a-form-item>
<a-form-item>
<a-input-password autocomplete="password" name="password" v-model.trim="user.password"
<a-input-password autocomplete="current-password" name="password" v-model.trim="user.password"
placeholder='{{ i18n "password" }}' required>
<a-icon slot="prefix" type="lock" class="fs-1rem"></a-icon>
</a-input-password>
@ -81,7 +81,8 @@
</a-form-item>
<a-form-item>
<a-row justify="center" class="centered">
<div class="wave-btn-bg wave-btn-bg-cl h-50px mt-1rem" :style="loadingStates.spinning ? 'width: 52px' : 'display: inline-block'">
<div class="wave-btn-bg wave-btn-bg-cl h-50px mt-1rem"
:style="loadingStates.spinning ? 'width: 52px' : 'display: inline-block'">
<a-button class="ant-btn-primary-login" type="primary" :loading="loadingStates.spinning"
:icon="loadingStates.spinning ? 'poweroff' : undefined" html-type="submit">
[[ loadingStates.spinning ? '' : '{{ i18n "login" }}' ]]
@ -107,17 +108,11 @@
el: '#app',
data: {
themeSwitcher,
loadingStates: {
fetched: false,
spinning: false
},
user: {
username: "",
password: "",
twoFactorCode: ""
},
loadingStates: { fetched: false, spinning: false },
user: { username: "", password: "", twoFactorCode: "" },
twoFactorEnable: false,
lang: ""
lang: "",
animationStarted: false
},
async mounted() {
this.lang = LanguageManager.getLanguage();
@ -126,65 +121,52 @@
methods: {
async login() {
this.loadingStates.spinning = true;
const msg = await HttpUtil.post('/login', this.user);
if (msg.success) {
location.href = basePath + 'panel/';
}
this.loadingStates.spinning = false;
},
async getTwoFactorEnable() {
const msg = await HttpUtil.post('/getTwoFactorEnable');
if (msg.success) {
this.twoFactorEnable = msg.obj;
this.loadingStates.fetched = true;
this.$nextTick(() => {
if (!this.animationStarted) {
this.animationStarted = true;
this.initHeadline();
}
});
return msg.obj;
}
},
initHeadline() {
const animationDelay = 2000;
const headlines = this.$el.querySelectorAll('.headline');
headlines.forEach((headline) => {
const first = headline.querySelector('.is-visible');
if (!first) return;
setTimeout(() => this.hideWord(first, animationDelay), animationDelay);
});
},
hideWord(word, delay) {
const nextWord = this.takeNext(word);
this.switchWord(word, nextWord);
setTimeout(() => this.hideWord(nextWord, delay), delay);
},
takeNext(word) {
return word.nextElementSibling || word.parentElement.firstElementChild;
},
switchWord(oldWord, newWord) {
oldWord.classList.remove('is-visible');
oldWord.classList.add('is-hidden');
newWord.classList.remove('is-hidden');
newWord.classList.add('is-visible');
}
},
});
document.addEventListener("DOMContentLoaded", function () {
var animationDelay = 2000;
initHeadline();
function initHeadline() {
animateHeadline(document.querySelectorAll('.headline'));
}
function animateHeadline(headlines) {
var duration = animationDelay;
headlines.forEach(function (headline) {
setTimeout(function () {
hideWord(headline.querySelector('.is-visible'));
}, duration);
});
}
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');
}
});
const pm_input_selector = 'input.ant-input, textarea.ant-input';
const pm_strip_props = [
'background',

View file

@ -7,12 +7,13 @@
<a-input v-model.trim="dnsModal.dnsServer.address"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.port" }}'>
<a-input-number v-model.number="dnsModal.dnsServer.port" :min="1" :max="65531"></a-input-number>
<a-input-number v-model.number="dnsModal.dnsServer.port" :min="1" :max="65535"></a-input-number>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.dns.strategy" }}'>
<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 ['UseSystem', 'UseIP', 'UseIPv4', 'UseIPv6']"> [[ l ]] </a-select-option>
<a-select-option :value="l" :label="l" v-for="l in ['UseSystem', 'UseIP', 'UseIPv4', 'UseIPv6']"> [[ l ]]
</a-select-option>
</a-select>
</a-form-item>
<a-divider :style="{ margin: '5px 0' }"></a-divider>

View file

@ -9,19 +9,20 @@
<a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}'>
<transition name="list" appear>
<a-alert type="error" v-if="confAlerts.length>0 && loadingStates.fetched" :style="{ marginBottom: '10px' }"
message='{{ i18n "secAlertTitle" }}'
color="red"
show-icon closable>
message='{{ i18n "secAlertTitle" }}' color="red" show-icon closable>
<template slot="description">
<b>{{ i18n "secAlertConf" }}</b>
<ul><li v-for="a in confAlerts">[[ a ]]</li></ul>
<ul>
<li v-for="a in confAlerts">[[ a ]]</li>
</ul>
</template>
</a-alert>
</transition>
<transition name="list" appear>
<template>
<a-row v-if="!loadingStates.fetched">
<a-card :style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
<a-card
:style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
<a-spin tip='{{ i18n "loading" }}'></a-spin>
</a-card>
</a-row>
@ -31,17 +32,19 @@
<a-row :style="{ display: 'flex', flexWrap: 'wrap', alignItems: 'center' }">
<a-col :xs="24" :sm="10" :style="{ padding: '4px' }">
<a-space direction="horizontal">
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.settings.save" }}</a-button>
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button>
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n
"pages.settings.save" }}</a-button>
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n
"pages.settings.restartPanel" }}</a-button>
</a-space>
</a-col>
<a-col :xs="24" :sm="14">
<template>
<div>
<a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200"></a-back-top>
<a-back-top :target="() => document.getElementById('content-layout')"
visibility-height="200"></a-back-top>
<a-alert type="warning" :style="{ float: 'right', width: 'fit-content' }"
message='{{ i18n "pages.settings.infoDesc" }}'
show-icon>
message='{{ i18n "pages.settings.infoDesc" }}' show-icon>
</a-alert>
</div>
</template>
@ -119,6 +122,7 @@
saveBtnDisable: true,
user: {},
lang: LanguageManager.getLanguage(),
inboundOptions: [],
remarkModels: { i: 'Inbound', e: 'Email', o: 'Other' },
remarkSeparators: [' ', '-', '_', '@', ':', '~', '|', ',', '.', '/'],
datepickerList: [{ name: 'Gregorian (Standard)', value: 'gregorian' }, { name: 'Jalalian (شمسی)', value: 'jalalian' }],
@ -131,7 +135,8 @@
fragment: {
packets: "tlshello",
length: "100-200",
interval: "10-20"
interval: "10-20",
maxSplit: "300-400"
}
},
streamSettings: {
@ -242,6 +247,17 @@
this.saveBtnDisable = true;
}
},
async loadInboundTags() {
const msg = await HttpUtil.get("/panel/api/inbounds/list");
if (msg && msg.success && Array.isArray(msg.obj)) {
this.inboundOptions = msg.obj.map(ib => ({
label: `${ib.tag} (${ib.protocol}@${ib.port})`,
value: ib.tag,
}));
} else {
this.inboundOptions = [];
}
},
async updateAllSetting() {
this.loading(true);
const msg = await HttpUtil.post("/panel/setting/update", this.allSetting);
@ -368,6 +384,15 @@
},
},
computed: {
ldapInboundTagList: {
get: function () {
const csv = this.allSetting.ldapInboundTags || "";
return csv.length ? csv.split(',').map(s => s.trim()).filter(Boolean) : [];
},
set: function (list) {
this.allSetting.ldapInboundTags = Array.isArray(list) ? list.join(',') : '';
}
},
fragment: {
get: function () { return this.allSetting?.subJsonFragment != ""; },
set: function (v) {
@ -404,6 +429,16 @@
}
}
},
fragmentMaxSplit: {
get: function () { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).settings.fragment.maxSplit : ""; },
set: function (v) {
if (v != "") {
newFragment = JSON.parse(this.allSetting.subJsonFragment);
newFragment.settings.fragment.maxSplit = v;
this.allSetting.subJsonFragment = JSON.stringify(newFragment);
}
}
},
noises: {
get() {
return this.allSetting?.subJsonNoises != "";
@ -534,7 +569,7 @@
},
async mounted() {
await this.getAllSetting();
await this.loadInboundTags();
while (true) {
await PromiseUtil.sleep(1000);
this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);

View file

@ -39,7 +39,7 @@
<template #title>{{ i18n "pages.settings.panelPort"}}</template>
<template #description>{{ i18n "pages.settings.panelPortDesc"}}</template>
<template #control>
<a-input-number :min="1" :min="65531" v-model="allSetting.webPort" :style="{ width: '100%' }"></a-input>
<a-input-number :min="1" :min="65535" v-model="allSetting.webPort" :style="{ width: '100%' }"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
@ -137,7 +137,8 @@
<template #title>{{ i18n "pages.settings.datepicker"}}</template>
<template #description>{{ i18n "pages.settings.datepickerDescription"}}</template>
<template #control>
<a-select :style="{ width: '100%' }" :dropdown-class-name="themeSwitcher.currentTheme" v-model="datepicker">
<a-select :style="{ width: '100%' }" :dropdown-class-name="themeSwitcher.currentTheme"
v-model="datepicker">
<a-select-option v-for="item in datepickerList" :value="item.value">
<span v-text="item.name"></span>
</a-select-option>
@ -145,5 +146,135 @@
</template>
</a-setting-list-item>
</a-collapse-panel>
<a-collapse-panel key="6" header='LDAP'>
<a-setting-list-item paddings="small">
<template #title>Enable LDAP sync</template>
<template #control>
<a-switch v-model="allSetting.ldapEnable"></a-switch>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>LDAP Host</template>
<template #control>
<a-input type="text" v-model="allSetting.ldapHost"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>LDAP Port</template>
<template #control>
<a-input-number :min="1" :max="65535" v-model="allSetting.ldapPort" :style="{ width: '100%' }"></a-input-number>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Use TLS (LDAPS)</template>
<template #control>
<a-switch v-model="allSetting.ldapUseTLS"></a-switch>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Bind DN</template>
<template #control>
<a-input type="text" v-model="allSetting.ldapBindDN"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Password</template>
<template #control>
<a-input type="password" v-model="allSetting.ldapPassword"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Base DN</template>
<template #control>
<a-input type="text" v-model="allSetting.ldapBaseDN"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>User filter</template>
<template #control>
<a-input type="text" v-model="allSetting.ldapUserFilter"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>User attribute (username/email)</template>
<template #control>
<a-input type="text" v-model="allSetting.ldapUserAttr"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>VLESS flag attribute</template>
<template #control>
<a-input type="text" v-model="allSetting.ldapVlessField"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Generic flag attribute (optional)</template>
<template #description>If set, overrides VLESS flag; e.g. shadowInactive</template>
<template #control>
<a-input type="text" v-model="allSetting.ldapFlagField"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Truthy values</template>
<template #description>Comma-separated; default: true,1,yes,on</template>
<template #control>
<a-input type="text" v-model="allSetting.ldapTruthyValues"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Invert flag</template>
<template #description>Enable when attribute means disabled (e.g., shadowInactive)</template>
<template #control>
<a-switch v-model="allSetting.ldapInvertFlag"></a-switch>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Sync schedule</template>
<template #description>cron-like string, e.g. @every 1m</template>
<template #control>
<a-input type="text" v-model="allSetting.ldapSyncCron"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Inbound tags</template>
<template #description>Select inbounds to manage (auto create/delete)</template>
<template #control>
<a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme" :style="{ width: '100%' }" v-model="ldapInboundTagList">
<a-select-option v-for="opt in inboundOptions" :key="opt.value" :value="opt.value">[[ opt.label ]]</a-select-option>
</a-select>
<div v-if="inboundOptions.length==0" style="margin-top:6px;color:#999">No inbounds found. Please create one in Inbounds.</div>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Auto create clients</template>
<template #control>
<a-switch v-model="allSetting.ldapAutoCreate"></a-switch>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Auto delete clients</template>
<template #control>
<a-switch v-model="allSetting.ldapAutoDelete"></a-switch>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Default total (GB)</template>
<template #control>
<a-input-number :min="0" v-model="allSetting.ldapDefaultTotalGB" :style="{ width: '100%' }"></a-input-number>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Default expiry (days)</template>
<template #control>
<a-input-number :min="0" v-model="allSetting.ldapDefaultExpiryDays" :style="{ width: '100%' }"></a-input-number>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Default Limit IP</template>
<template #control>
<a-input-number :min="0" v-model="allSetting.ldapDefaultLimitIP" :style="{ width: '100%' }"></a-input-number>
</template>
</a-setting-list-item>
</a-collapse-panel>
</a-collapse>
{{end}}

View file

@ -40,7 +40,7 @@
<template #title>{{ i18n "pages.settings.subPort"}}</template>
<template #description>{{ i18n "pages.settings.subPortDesc"}}</template>
<template #control>
<a-input-number v-model="allSetting.subPort" :min="1" :min="65531"
<a-input-number v-model="allSetting.subPort" :min="1" :min="65535"
:style="{ width: '100%' }"></a-input-number>
</template>
</a-setting-list-item>
@ -48,13 +48,10 @@
<template #title>{{ i18n "pages.settings.subPath"}}</template>
<template #description>{{ i18n "pages.settings.subPathDesc"}}</template>
<template #control>
<a-input
type="text"
v-model="allSetting.subPath"
<a-input type="text" v-model="allSetting.subPath"
@input="allSetting.subPath = ((typeof $event === 'string' ? $event : ($event && $event.target ? $event.target.value : '')) || '').replace(/[:*]/g, '')"
@blur="allSetting.subPath = (p => { p = p || '/'; if (!p.startsWith('/')) p='/' + p; if (!p.endsWith('/')) p += '/'; return p.replace(/\/+/g,'/'); })(allSetting.subPath)"
placeholder="/sub/"
></a-input>
placeholder="/sub/"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">

View file

@ -5,13 +5,10 @@
<template #title>{{ i18n "pages.settings.subPath"}}</template>
<template #description>{{ i18n "pages.settings.subPathDesc"}}</template>
<template #control>
<a-input
type="text"
v-model="allSetting.subJsonPath"
<a-input type="text" v-model="allSetting.subJsonPath"
@input="allSetting.subJsonPath = ((typeof $event === 'string' ? $event : ($event && $event.target ? $event.target.value : '')) || '').replace(/[:*]/g, '')"
@blur="allSetting.subJsonPath = (p => { p = p || '/'; if (!p.startsWith('/')) p='/' + p; if (!p.endsWith('/')) p += '/'; return p.replace(/\/+/g,'/'); })(allSetting.subJsonPath)"
placeholder="/json/"
></a-input>
placeholder="/json/"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
@ -53,6 +50,12 @@
<a-input type="text" v-model="fragmentInterval" placeholder="10-20"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>MaxSplit</template>
<template #control>
<a-input type="text" v-model="fragmentMaxSplit" placeholder="300-400"></a-input>
</template>
</a-setting-list-item>
</a-collapse-panel>
</a-collapse>
</a-list-item>
@ -74,7 +77,8 @@
<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', 'hex']" :key="p">
<a-select-option :value="p" :label="p" v-for="p in ['rand', 'base64', 'str', 'hex']"
:key="p">
<span>[[ p ]]</span>
</a-select-option>
</a-select>

View file

@ -218,6 +218,8 @@
<a-menu-item key="android-npvtunnel"
@click="copy(app.subUrl)">NPV
Tunnel</a-menu-item>
<a-menu-item key="android-happ"
@click="open('happ://add/' + encodeURIComponent(app.subUrl))">Happ</a-menu-item>
</a-menu>
</a-dropdown>
</a-col>
@ -244,6 +246,8 @@
@click="copy(npvtunUrl)">NPV
Tunnel
</a-menu-item>
<a-menu-item key="ios-happ"
@click="open(happUrl)">Happ</a-menu-item>
</a-menu>
</a-dropdown>
</a-col>

View file

@ -12,13 +12,14 @@
<a-layout-content>
<a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}'>
<transition name="list" appear>
<a-alert type="error" v-if="showAlert && loadingStates.fetched" :style="{ marginBottom: '10px' }" message='{{ i18n "secAlertTitle" }}'
color="red" description='{{ i18n "secAlertSsl" }}' show-icon closable>
<a-alert type="error" v-if="showAlert && loadingStates.fetched" :style="{ marginBottom: '10px' }"
message='{{ i18n "secAlertTitle" }}' color="red" description='{{ i18n "secAlertSsl" }}' show-icon closable>
</a-alert>
</transition>
<transition name="list" appear>
<a-row v-if="!loadingStates.fetched">
<a-card :style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
<a-card
:style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
<a-spin tip='{{ i18n "loading" }}'></a-spin>
</a-card>
</a-row>
@ -37,7 +38,8 @@
<a-popover v-if="restartResult" :overlay-class-name="themeSwitcher.currentTheme">
<span slot="title">{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span>
<template slot="content">
<span :style="{ maxWidth: '400px' }" v-for="line in restartResult.split('\n')">[[ line ]]</span>
<span :style="{ maxWidth: '400px' }" v-for="line in restartResult.split('\n')">[[ line
]]</span>
</template>
<a-icon type="question-circle"></a-icon>
</a-popover>
@ -537,6 +539,7 @@
serverObj = o.settings.vnext;
break;
case Protocols.VLESS:
return [o.settings?.address + ':' + o.settings?.port];
case Protocols.HTTP:
case Protocols.Socks:
case Protocols.Shadowsocks:

421
web/job/ldap_sync_job.go Normal file
View file

@ -0,0 +1,421 @@
package job
import (
"time"
"strings"
"github.com/mhsanaei/3x-ui/v2/database/model"
"github.com/mhsanaei/3x-ui/v2/logger"
ldaputil "github.com/mhsanaei/3x-ui/v2/util/ldap"
"github.com/mhsanaei/3x-ui/v2/web/service"
"strconv"
"github.com/google/uuid"
)
var DefaultTruthyValues = []string{"true", "1", "yes", "on"}
type LdapSyncJob struct {
settingService service.SettingService
inboundService service.InboundService
xrayService service.XrayService
}
// --- Helper functions for mustGet ---
func mustGetString(fn func() (string, error)) string {
v, err := fn()
if err != nil {
panic(err)
}
return v
}
func mustGetInt(fn func() (int, error)) int {
v, err := fn()
if err != nil {
panic(err)
}
return v
}
func mustGetBool(fn func() (bool, error)) bool {
v, err := fn()
if err != nil {
panic(err)
}
return v
}
func mustGetStringOr(fn func() (string, error), fallback string) string {
v, err := fn()
if err != nil || v == "" {
return fallback
}
return v
}
func NewLdapSyncJob() *LdapSyncJob {
return new(LdapSyncJob)
}
func (j *LdapSyncJob) Run() {
logger.Info("LDAP sync job started")
enabled, err := j.settingService.GetLdapEnable()
if err != nil || !enabled {
logger.Warning("LDAP disabled or failed to fetch flag")
return
}
// --- LDAP fetch ---
cfg := ldaputil.Config{
Host: mustGetString(j.settingService.GetLdapHost),
Port: mustGetInt(j.settingService.GetLdapPort),
UseTLS: mustGetBool(j.settingService.GetLdapUseTLS),
BindDN: mustGetString(j.settingService.GetLdapBindDN),
Password: mustGetString(j.settingService.GetLdapPassword),
BaseDN: mustGetString(j.settingService.GetLdapBaseDN),
UserFilter: mustGetString(j.settingService.GetLdapUserFilter),
UserAttr: mustGetString(j.settingService.GetLdapUserAttr),
FlagField: mustGetStringOr(j.settingService.GetLdapFlagField, mustGetString(j.settingService.GetLdapVlessField)),
TruthyVals: splitCsv(mustGetString(j.settingService.GetLdapTruthyValues)),
Invert: mustGetBool(j.settingService.GetLdapInvertFlag),
}
flags, err := ldaputil.FetchVlessFlags(cfg)
if err != nil {
logger.Warning("LDAP fetch failed:", err)
return
}
logger.Infof("Fetched %d LDAP flags", len(flags))
// --- Load all inbounds and all clients once ---
inboundTags := splitCsv(mustGetString(j.settingService.GetLdapInboundTags))
inbounds, err := j.inboundService.GetAllInbounds()
if err != nil {
logger.Warning("Failed to get inbounds:", err)
return
}
allClients := map[string]*model.Client{} // email -> client
inboundMap := map[string]*model.Inbound{} // tag -> inbound
for _, ib := range inbounds {
inboundMap[ib.Tag] = ib
clients, _ := j.inboundService.GetClients(ib)
for i := range clients {
allClients[clients[i].Email] = &clients[i]
}
}
// --- Prepare batch operations ---
autoCreate := mustGetBool(j.settingService.GetLdapAutoCreate)
defGB := mustGetInt(j.settingService.GetLdapDefaultTotalGB)
defExpiryDays := mustGetInt(j.settingService.GetLdapDefaultExpiryDays)
defLimitIP := mustGetInt(j.settingService.GetLdapDefaultLimitIP)
clientsToCreate := map[string][]model.Client{} // tag -> []new clients
clientsToEnable := map[string][]string{} // tag -> []email
clientsToDisable := map[string][]string{} // tag -> []email
for email, allowed := range flags {
exists := allClients[email] != nil
for _, tag := range inboundTags {
if !exists && allowed && autoCreate {
newClient := j.buildClient(inboundMap[tag], email, defGB, defExpiryDays, defLimitIP)
clientsToCreate[tag] = append(clientsToCreate[tag], newClient)
} else if exists {
if allowed && !allClients[email].Enable {
clientsToEnable[tag] = append(clientsToEnable[tag], email)
} else if !allowed && allClients[email].Enable {
clientsToDisable[tag] = append(clientsToDisable[tag], email)
}
}
}
}
// --- Execute batch create ---
for tag, newClients := range clientsToCreate {
if len(newClients) == 0 {
continue
}
payload := &model.Inbound{Id: inboundMap[tag].Id}
payload.Settings = j.clientsToJSON(newClients)
if _, err := j.inboundService.AddInboundClient(payload); err != nil {
logger.Warningf("Failed to add clients for tag %s: %v", tag, err)
} else {
logger.Infof("LDAP auto-create: %d clients for %s", len(newClients), tag)
j.xrayService.SetToNeedRestart()
}
}
// --- Execute enable/disable batch ---
for tag, emails := range clientsToEnable {
j.batchSetEnable(inboundMap[tag], emails, true)
}
for tag, emails := range clientsToDisable {
j.batchSetEnable(inboundMap[tag], emails, false)
}
// --- Auto delete clients not in LDAP ---
autoDelete := mustGetBool(j.settingService.GetLdapAutoDelete)
if autoDelete {
ldapEmailSet := map[string]struct{}{}
for e := range flags {
ldapEmailSet[e] = struct{}{}
}
for _, tag := range inboundTags {
j.deleteClientsNotInLDAP(tag, ldapEmailSet)
}
}
}
func splitCsv(s string) []string {
if s == "" {
return DefaultTruthyValues
}
parts := strings.Split(s, ",")
out := make([]string, 0, len(parts))
for _, p := range parts {
v := strings.TrimSpace(p)
if v != "" {
out = append(out, v)
}
}
return out
}
// buildClient creates a new client for auto-create
func (j *LdapSyncJob) buildClient(ib *model.Inbound, email string, defGB, defExpiryDays, defLimitIP int) model.Client {
c := model.Client{
Email: email,
Enable: true,
LimitIP: defLimitIP,
TotalGB: int64(defGB),
}
if defExpiryDays > 0 {
c.ExpiryTime = time.Now().Add(time.Duration(defExpiryDays) * 24 * time.Hour).UnixMilli()
}
switch ib.Protocol {
case model.Trojan, model.Shadowsocks:
c.Password = uuid.NewString()
default:
c.ID = uuid.NewString()
}
return c
}
// batchSetEnable enables/disables clients in batch through a single call
func (j *LdapSyncJob) batchSetEnable(ib *model.Inbound, emails []string, enable bool) {
if len(emails) == 0 {
return
}
// Prepare JSON for mass update
clients := make([]model.Client, 0, len(emails))
for _, email := range emails {
clients = append(clients, model.Client{
Email: email,
Enable: enable,
})
}
payload := &model.Inbound{
Id: ib.Id,
Settings: j.clientsToJSON(clients),
}
// Use a single AddInboundClient call to update enable
if _, err := j.inboundService.AddInboundClient(payload); err != nil {
logger.Warningf("Batch set enable failed for inbound %s: %v", ib.Tag, err)
return
}
logger.Infof("Batch set enable=%v for %d clients in inbound %s", enable, len(emails), ib.Tag)
j.xrayService.SetToNeedRestart()
}
// deleteClientsNotInLDAP deletes clients not in LDAP using batches and a single restart
func (j *LdapSyncJob) deleteClientsNotInLDAP(inboundTag string, ldapEmails map[string]struct{}) {
inbounds, err := j.inboundService.GetAllInbounds()
if err != nil {
logger.Warning("Failed to get inbounds for deletion:", err)
return
}
batchSize := 50 // clients in 1 batch
restartNeeded := false
for _, ib := range inbounds {
if ib.Tag != inboundTag {
continue
}
clients, err := j.inboundService.GetClients(ib)
if err != nil {
logger.Warningf("Failed to get clients for inbound %s: %v", ib.Tag, err)
continue
}
// Collect clients for deletion
toDelete := []model.Client{}
for _, c := range clients {
if _, ok := ldapEmails[c.Email]; !ok {
toDelete = append(toDelete, c)
}
}
if len(toDelete) == 0 {
continue
}
// Delete in batches
for i := 0; i < len(toDelete); i += batchSize {
end := i + batchSize
if end > len(toDelete) {
end = len(toDelete)
}
batch := toDelete[i:end]
for _, c := range batch {
var clientKey string
switch ib.Protocol {
case model.Trojan:
clientKey = c.Password
case model.Shadowsocks:
clientKey = c.Email
default: // vless/vmess
clientKey = c.ID
}
if _, err := j.inboundService.DelInboundClient(ib.Id, clientKey); err != nil {
logger.Warningf("Failed to delete client %s from inbound id=%d(tag=%s): %v",
c.Email, ib.Id, ib.Tag, err)
} else {
logger.Infof("Deleted client %s from inbound id=%d(tag=%s)",
c.Email, ib.Id, ib.Tag)
// do not restart here
restartNeeded = true
}
}
}
}
// One time after all batches
if restartNeeded {
j.xrayService.SetToNeedRestart()
logger.Info("Xray restart scheduled after batch deletion")
}
}
// clientsToJSON serializes an array of clients to JSON
func (j *LdapSyncJob) clientsToJSON(clients []model.Client) string {
b := strings.Builder{}
b.WriteString("{\"clients\":[")
for i, c := range clients {
if i > 0 {
b.WriteString(",")
}
b.WriteString(j.clientToJSON(c))
}
b.WriteString("]}")
return b.String()
}
// ensureClientExists adds client with defaults to inbound tag if not present
func (j *LdapSyncJob) ensureClientExists(inboundTag string, email string, defGB int, defExpiryDays int, defLimitIP int) {
inbounds, err := j.inboundService.GetAllInbounds()
if err != nil {
logger.Warning("ensureClientExists: get inbounds failed:", err)
return
}
var target *model.Inbound
for _, ib := range inbounds {
if ib.Tag == inboundTag {
target = ib
break
}
}
if target == nil {
logger.Debugf("ensureClientExists: inbound tag %s not found", inboundTag)
return
}
// check if email already exists in this inbound
clients, err := j.inboundService.GetClients(target)
if err == nil {
for _, c := range clients {
if c.Email == email {
return
}
}
}
// build new client according to protocol
newClient := model.Client{
Email: email,
Enable: true,
LimitIP: defLimitIP,
TotalGB: int64(defGB),
}
if defExpiryDays > 0 {
newClient.ExpiryTime = time.Now().Add(time.Duration(defExpiryDays) * 24 * time.Hour).UnixMilli()
}
switch target.Protocol {
case model.Trojan:
newClient.Password = uuid.NewString()
case model.Shadowsocks:
newClient.Password = uuid.NewString()
default: // VMESS/VLESS and others using ID
newClient.ID = uuid.NewString()
}
// prepare inbound payload with only the new client
payload := &model.Inbound{Id: target.Id}
payload.Settings = `{"clients":[` + j.clientToJSON(newClient) + `]}`
if _, err := j.inboundService.AddInboundClient(payload); err != nil {
logger.Warning("ensureClientExists: add client failed:", err)
} else {
j.xrayService.SetToNeedRestart()
logger.Infof("LDAP auto-create: %s in %s", email, inboundTag)
}
}
// clientToJSON serializes minimal client fields to JSON object string without extra deps
func (j *LdapSyncJob) clientToJSON(c model.Client) string {
// construct minimal JSON manually to avoid importing json for simple case
b := strings.Builder{}
b.WriteString("{")
if c.ID != "" {
b.WriteString("\"id\":\"")
b.WriteString(c.ID)
b.WriteString("\",")
}
if c.Password != "" {
b.WriteString("\"password\":\"")
b.WriteString(c.Password)
b.WriteString("\",")
}
b.WriteString("\"email\":\"")
b.WriteString(c.Email)
b.WriteString("\",")
b.WriteString("\"enable\":")
if c.Enable {
b.WriteString("true")
} else {
b.WriteString("false")
}
b.WriteString(",")
b.WriteString("\"limitIp\":")
b.WriteString(strconv.Itoa(c.LimitIP))
b.WriteString(",")
b.WriteString("\"totalGB\":")
b.WriteString(strconv.FormatInt(c.TotalGB, 10))
if c.ExpiryTime > 0 {
b.WriteString(",\"expiryTime\":")
b.WriteString(strconv.FormatInt(c.ExpiryTime, 10))
}
b.WriteString("}")
return b.String()
}

View file

@ -37,13 +37,19 @@ func (j *PeriodicTrafficResetJob) Run() {
resetCount := 0
for _, inbound := range inbounds {
if err := j.inboundService.ResetAllClientTraffics(inbound.Id); err != nil {
logger.Warning("Failed to reset traffic for inbound", inbound.Id, ":", err)
continue
resetInboundErr := j.inboundService.ResetAllTraffics()
if resetInboundErr != nil {
logger.Warning("Failed to reset traffic for inbound", inbound.Id, ":", resetInboundErr)
}
resetCount++
logger.Infof("Reset traffic for inbound %d (%s)", inbound.Id, inbound.Remark)
resetClientErr := j.inboundService.ResetAllClientTraffics(inbound.Id)
if resetClientErr != nil {
logger.Warning("Failed to reset traffic for all users of inbound", inbound.Id, ":", resetClientErr)
}
if resetInboundErr == nil && resetClientErr == nil {
resetCount++
}
}
if resetCount > 0 {

View file

@ -35,6 +35,25 @@ func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {
if err != nil && err != gorm.ErrRecordNotFound {
return nil, err
}
// Enrich client stats with UUID/SubId from inbound settings
for _, inbound := range inbounds {
clients, _ := s.GetClients(inbound)
if len(clients) == 0 || len(inbound.ClientStats) == 0 {
continue
}
// Build a map email -> client
cMap := make(map[string]model.Client, len(clients))
for _, c := range clients {
cMap[strings.ToLower(c.Email)] = c
}
for i := range inbound.ClientStats {
email := strings.ToLower(inbound.ClientStats[i].Email)
if c, ok := cMap[email]; ok {
inbound.ClientStats[i].UUID = c.ID
inbound.ClientStats[i].SubId = c.SubID
}
}
}
return inbounds, nil
}
@ -47,6 +66,24 @@ func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) {
if err != nil && err != gorm.ErrRecordNotFound {
return nil, err
}
// Enrich client stats with UUID/SubId from inbound settings
for _, inbound := range inbounds {
clients, _ := s.GetClients(inbound)
if len(clients) == 0 || len(inbound.ClientStats) == 0 {
continue
}
cMap := make(map[string]model.Client, len(clients))
for _, c := range clients {
cMap[strings.ToLower(c.Email)] = c
}
for i := range inbound.ClientStats {
email := strings.ToLower(inbound.ClientStats[i].Email)
if c, ok := cMap[email]; ok {
inbound.ClientStats[i].UUID = c.ID
inbound.ClientStats[i].SubId = c.SubID
}
}
}
return inbounds, nil
}
@ -1532,6 +1569,23 @@ func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, bo
return !clientOldEnabled, needRestart, nil
}
// SetClientEnableByEmail sets client enable state to desired value; returns (changed, needRestart, error)
func (s *InboundService) SetClientEnableByEmail(clientEmail string, enable bool) (bool, bool, error) {
current, err := s.checkIsEnabledByEmail(clientEmail)
if err != nil {
return false, false, err
}
if current == enable {
return false, false, nil
}
newEnabled, needRestart, err := s.ToggleClientEnableByEmail(clientEmail)
if err != nil {
return false, needRestart, err
}
return newEnabled == enable, needRestart, nil
}
func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int) (bool, error) {
_, inbound, err := s.GetClientInboundByEmail(clientEmail)
if err != nil {

View file

@ -110,6 +110,7 @@ type ServerService struct {
mu sync.Mutex
lastCPUTimes cpu.TimesStat
hasLastCPUSample bool
hasNativeCPUSample bool
emaCPU float64
cpuHistory []CPUSample
cachedCpuSpeedMhz float64
@ -432,23 +433,27 @@ func (s *ServerService) AppendCpuSample(t time.Time, v float64) {
}
func (s *ServerService) sampleCPUUtilization() (float64, error) {
// Prefer native Windows API to avoid external deps for CPU percent
if runtime.GOOS == "windows" {
if pct, err := sys.CPUPercentRaw(); err == nil {
s.mu.Lock()
// Smooth with EMA
const alpha = 0.3
if s.emaCPU == 0 {
s.emaCPU = pct
} else {
s.emaCPU = alpha*pct + (1-alpha)*s.emaCPU
}
val := s.emaCPU
// Try native platform-specific CPU implementation first (Windows, Linux, macOS)
if pct, err := sys.CPUPercentRaw(); err == nil {
s.mu.Lock()
// First call to native method returns 0 (initializes baseline)
if !s.hasNativeCPUSample {
s.hasNativeCPUSample = true
s.mu.Unlock()
return val, nil
return 0, nil
}
// If native call fails, fall back to gopsutil times
// Smooth with EMA
const alpha = 0.3
if s.emaCPU == 0 {
s.emaCPU = pct
} else {
s.emaCPU = alpha*pct + (1-alpha)*s.emaCPU
}
val := s.emaCPU
s.mu.Unlock()
return val, nil
}
// If native call fails, fall back to gopsutil times
// Read aggregate CPU times (all CPUs combined)
times, err := cpu.Times(false)
if err != nil {
@ -471,17 +476,16 @@ func (s *ServerService) sampleCPUUtilization() (float64, error) {
}
// Compute busy and total deltas
// Note: Guest and GuestNice times are already included in User and Nice respectively,
// so we exclude them to avoid double-counting (Linux kernel accounting)
idleDelta := cur.Idle - s.lastCPUTimes.Idle
// Sum of busy deltas (exclude Idle)
busyDelta := (cur.User - s.lastCPUTimes.User) +
(cur.System - s.lastCPUTimes.System) +
(cur.Nice - s.lastCPUTimes.Nice) +
(cur.Iowait - s.lastCPUTimes.Iowait) +
(cur.Irq - s.lastCPUTimes.Irq) +
(cur.Softirq - s.lastCPUTimes.Softirq) +
(cur.Steal - s.lastCPUTimes.Steal) +
(cur.Guest - s.lastCPUTimes.Guest) +
(cur.GuestNice - s.lastCPUTimes.GuestNice)
(cur.Steal - s.lastCPUTimes.Steal)
totalDelta := busyDelta + idleDelta

View file

@ -73,6 +73,27 @@ var defaultValueMap = map[string]string{
"warp": "",
"externalTrafficInformEnable": "false",
"externalTrafficInformURI": "",
// LDAP defaults
"ldapEnable": "false",
"ldapHost": "",
"ldapPort": "389",
"ldapUseTLS": "false",
"ldapBindDN": "",
"ldapPassword": "",
"ldapBaseDN": "",
"ldapUserFilter": "(objectClass=person)",
"ldapUserAttr": "mail",
"ldapVlessField": "vless_enabled",
"ldapSyncCron": "@every 1m",
"ldapFlagField": "",
"ldapTruthyValues": "true,1,yes,on",
"ldapInvertFlag": "false",
"ldapInboundTags": "",
"ldapAutoCreate": "false",
"ldapAutoDelete": "false",
"ldapDefaultTotalGB": "0",
"ldapDefaultExpiryDays": "0",
"ldapDefaultLimitIP": "0",
}
// SettingService provides business logic for application settings management.
@ -542,6 +563,87 @@ func (s *SettingService) GetIpLimitEnable() (bool, error) {
return (accessLogPath != "none" && accessLogPath != ""), nil
}
// LDAP exported getters
func (s *SettingService) GetLdapEnable() (bool, error) {
return s.getBool("ldapEnable")
}
func (s *SettingService) GetLdapHost() (string, error) {
return s.getString("ldapHost")
}
func (s *SettingService) GetLdapPort() (int, error) {
return s.getInt("ldapPort")
}
func (s *SettingService) GetLdapUseTLS() (bool, error) {
return s.getBool("ldapUseTLS")
}
func (s *SettingService) GetLdapBindDN() (string, error) {
return s.getString("ldapBindDN")
}
func (s *SettingService) GetLdapPassword() (string, error) {
return s.getString("ldapPassword")
}
func (s *SettingService) GetLdapBaseDN() (string, error) {
return s.getString("ldapBaseDN")
}
func (s *SettingService) GetLdapUserFilter() (string, error) {
return s.getString("ldapUserFilter")
}
func (s *SettingService) GetLdapUserAttr() (string, error) {
return s.getString("ldapUserAttr")
}
func (s *SettingService) GetLdapVlessField() (string, error) {
return s.getString("ldapVlessField")
}
func (s *SettingService) GetLdapSyncCron() (string, error) {
return s.getString("ldapSyncCron")
}
func (s *SettingService) GetLdapFlagField() (string, error) {
return s.getString("ldapFlagField")
}
func (s *SettingService) GetLdapTruthyValues() (string, error) {
return s.getString("ldapTruthyValues")
}
func (s *SettingService) GetLdapInvertFlag() (bool, error) {
return s.getBool("ldapInvertFlag")
}
func (s *SettingService) GetLdapInboundTags() (string, error) {
return s.getString("ldapInboundTags")
}
func (s *SettingService) GetLdapAutoCreate() (bool, error) {
return s.getBool("ldapAutoCreate")
}
func (s *SettingService) GetLdapAutoDelete() (bool, error) {
return s.getBool("ldapAutoDelete")
}
func (s *SettingService) GetLdapDefaultTotalGB() (int, error) {
return s.getInt("ldapDefaultTotalGB")
}
func (s *SettingService) GetLdapDefaultExpiryDays() (int, error) {
return s.getInt("ldapDefaultExpiryDays")
}
func (s *SettingService) GetLdapDefaultLimitIP() (int, error) {
return s.getInt("ldapDefaultLimitIP")
}
func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
if err := allSetting.CheckValid(); err != nil {
return err

View file

@ -46,8 +46,8 @@ var (
hashStorage *global.HashStorage
// Performance improvements
messageWorkerPool chan struct{} // Semaphore for limiting concurrent message processing
optimizedHTTPClient *http.Client // HTTP client with connection pooling and timeouts
messageWorkerPool chan struct{} // Semaphore for limiting concurrent message processing
optimizedHTTPClient *http.Client // HTTP client with connection pooling and timeouts
// Simple cache for frequently accessed data
statusCache struct {
@ -359,7 +359,7 @@ func (t *Tgbot) OnReceive() {
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
// Use goroutine with worker pool for concurrent command processing
go func() {
messageWorkerPool <- struct{}{} // Acquire worker
messageWorkerPool <- struct{}{} // Acquire worker
defer func() { <-messageWorkerPool }() // Release worker
delete(userStates, message.Chat.ID)
@ -371,7 +371,7 @@ func (t *Tgbot) OnReceive() {
botHandler.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error {
// Use goroutine with worker pool for concurrent callback processing
go func() {
messageWorkerPool <- struct{}{} // Acquire worker
messageWorkerPool <- struct{}{} // Acquire worker
defer func() { <-messageWorkerPool }() // Release worker
delete(userStates, query.Message.GetChat().ID)

View file

@ -7,7 +7,7 @@ import (
"github.com/mhsanaei/3x-ui/v2/database/model"
"github.com/mhsanaei/3x-ui/v2/logger"
"github.com/mhsanaei/3x-ui/v2/util/crypto"
ldaputil "github.com/mhsanaei/3x-ui/v2/util/ldap"
"github.com/xlzd/gotp"
"gorm.io/gorm"
)
@ -49,9 +49,38 @@ func (s *UserService) CheckUser(username string, password string, twoFactorCode
return nil
}
if !crypto.CheckPasswordHash(user.Password, password) {
return nil
}
// If LDAP enabled and local password check fails, attempt LDAP auth
if !crypto.CheckPasswordHash(user.Password, password) {
ldapEnabled, _ := s.settingService.GetLdapEnable()
if !ldapEnabled {
return nil
}
host, _ := s.settingService.GetLdapHost()
port, _ := s.settingService.GetLdapPort()
useTLS, _ := s.settingService.GetLdapUseTLS()
bindDN, _ := s.settingService.GetLdapBindDN()
ldapPass, _ := s.settingService.GetLdapPassword()
baseDN, _ := s.settingService.GetLdapBaseDN()
userFilter, _ := s.settingService.GetLdapUserFilter()
userAttr, _ := s.settingService.GetLdapUserAttr()
cfg := ldaputil.Config{
Host: host,
Port: port,
UseTLS: useTLS,
BindDN: bindDN,
Password: ldapPass,
BaseDN: baseDN,
UserFilter: userFilter,
UserAttr: userAttr,
}
ok, err := ldaputil.AuthenticateUser(cfg, username, password)
if err != nil || !ok {
return nil
}
// On successful LDAP auth, continue 2FA checks below
}
twoFactorEnable, err := s.settingService.GetTwoFactorEnable()
if err != nil {

View file

@ -99,7 +99,7 @@
[pages.login]
"hello" = "Привет!"
"title" = "Приветствие!"
"title" = "Добро пожаловать!"
"loginAgain" = "Сессия истекла. Войдите в систему снова"
[pages.login.toasts]
@ -648,7 +648,7 @@
"ips" = "🔢 IP-адреса:\r\n{{ .IPs }}\r\n"
"serverUpTime" = "⏳ Время работы сервера: {{ .UpTime }} {{ .Unit }}\r\n"
"serverLoad" = "📈 Нагрузка сервера: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
"serverMemory" = "📋 Диск сервера: {{ .Current }}/{{ .Total }}\r\n"
"serverMemory" = "📋 ОЗУ сервера: {{ .Current }}/{{ .Total }}\r\n"
"tcpCount" = "🔹 Количество TCP-соединений: {{ .Count }}\r\n"
"udpCount" = "🔸 Количество UDP-соединений: {{ .Count }}\r\n"
"traffic" = "🚦 Трафик: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"

View file

@ -95,10 +95,9 @@ type Server struct {
httpServer *http.Server
listener net.Listener
index *controller.IndexController
server *controller.ServerController
panel *controller.XUIController
api *controller.APIController
index *controller.IndexController
panel *controller.XUIController
api *controller.APIController
xrayService service.XrayService
settingService service.SettingService
@ -264,10 +263,19 @@ func (s *Server) initRouter() (*gin.Engine, error) {
g := engine.Group(basePath)
s.index = controller.NewIndexController(g)
s.server = controller.NewServerController(g)
s.panel = controller.NewXUIController(g)
s.api = controller.NewAPIController(g)
// Chrome DevTools endpoint for debugging web apps
engine.GET("/.well-known/appspecific/com.chrome.devtools.json", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{})
})
// Add a catch-all route to handle undefined paths and return 404
engine.NoRoute(func(c *gin.Context) {
c.AbortWithStatus(http.StatusNotFound)
})
return engine, nil
}
@ -311,6 +319,17 @@ func (s *Server) startTask() {
// Run once a month, midnight, first of month
s.cron.AddJob("@monthly", job.NewPeriodicTrafficResetJob("monthly"))
// LDAP sync scheduling
if ldapEnabled, _ := s.settingService.GetLdapEnable(); ldapEnabled {
runtime, err := s.settingService.GetLdapSyncCron()
if err != nil || runtime == "" {
runtime = "@every 1m"
}
j := job.NewLdapSyncJob()
// job has zero-value services with method receivers that read settings on demand
s.cron.AddJob(runtime, j)
}
// Make a traffic condition every day, 8:30
var entry cron.EntryID
isTgbotenabled, err := s.settingService.GetTgbotEnabled()

13
x-ui.rc Normal file
View file

@ -0,0 +1,13 @@
#!/sbin/openrc-run
command="/usr/local/x-ui/x-ui"
command_background=true
pidfile="/run/x-ui.pid"
description="x-ui Service"
procname="x-ui"
depend() {
need net
}
start_pre(){
cd /usr/local/x-ui
}

265
x-ui.sh
View file

@ -85,7 +85,7 @@ install() {
}
update() {
confirm "This function will forcefully reinstall the latest version, and the data will not be lost. Do you want to continue?" "y"
confirm "This function will update all x-ui components to the latest version, and the data will not be lost. Do you want to continue?" "y"
if [[ $? != 0 ]]; then
LOGE "Cancelled"
if [[ $# == 0 ]]; then
@ -93,7 +93,7 @@ update() {
fi
return 0
fi
bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/main/install.sh)
bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/main/update.sh)
if [[ $? == 0 ]]; then
LOGI "Update is complete, Panel has automatically restarted "
before_show_menu
@ -153,11 +153,19 @@ uninstall() {
fi
return 0
fi
systemctl stop x-ui
systemctl disable x-ui
rm /etc/systemd/system/x-ui.service -f
systemctl daemon-reload
systemctl reset-failed
if [[ $release == "alpine" ]]; then
rc-service x-ui stop
rc-update del x-ui
rm /etc/init.d/x-ui -f
else
systemctl stop x-ui
systemctl disable x-ui
rm /etc/systemd/system/x-ui.service -f
systemctl daemon-reload
systemctl reset-failed
fi
rm /etc/x-ui/ -rf
rm /usr/local/x-ui/ -rf
@ -181,9 +189,9 @@ reset_user() {
fi
read -rp "Please set the login username [default is a random username]: " config_account
[[ -z $config_account ]] && config_account=$(date +%s%N | md5sum | cut -c 1-8)
[[ -z $config_account ]] && config_account=$(gen_random_string 10)
read -rp "Please set the login password [default is a random password]: " config_password
[[ -z $config_password ]] && config_password=$(date +%s%N | md5sum | cut -c 1-8)
[[ -z $config_password ]] && config_password=$(gen_random_string 18)
read -rp "Do you want to disable currently configured two-factor authentication? (y/n): " twoFactorConfirm
if [[ $twoFactorConfirm != "y" && $twoFactorConfirm != "Y" ]]; then
@ -286,7 +294,11 @@ start() {
echo ""
LOGI "Panel is running, No need to start again, If you need to restart, please select restart"
else
systemctl start x-ui
if [[ $release == "alpine" ]]; then
rc-service x-ui start
else
systemctl start x-ui
fi
sleep 2
check_status
if [[ $? == 0 ]]; then
@ -307,7 +319,11 @@ stop() {
echo ""
LOGI "Panel stopped, No need to stop again!"
else
systemctl stop x-ui
if [[ $release == "alpine" ]]; then
rc-service x-ui stop
else
systemctl stop x-ui
fi
sleep 2
check_status
if [[ $? == 1 ]]; then
@ -323,7 +339,11 @@ stop() {
}
restart() {
systemctl restart x-ui
if [[ $release == "alpine" ]]; then
rc-service x-ui restart
else
systemctl restart x-ui
fi
sleep 2
check_status
if [[ $? == 0 ]]; then
@ -337,14 +357,22 @@ restart() {
}
status() {
systemctl status x-ui -l
if [[ $release == "alpine" ]]; then
rc-service x-ui status
else
systemctl status x-ui -l
fi
if [[ $# == 0 ]]; then
before_show_menu
fi
}
enable() {
systemctl enable x-ui
if [[ $release == "alpine" ]]; then
rc-update add x-ui
else
systemctl enable x-ui
fi
if [[ $? == 0 ]]; then
LOGI "x-ui Set to boot automatically on startup successfully"
else
@ -357,7 +385,11 @@ enable() {
}
disable() {
systemctl disable x-ui
if [[ $release == "alpine" ]]; then
rc-update del x-ui
else
systemctl disable x-ui
fi
if [[ $? == 0 ]]; then
LOGI "x-ui Autostart Cancelled successfully"
else
@ -370,32 +402,54 @@ disable() {
}
show_log() {
echo -e "${green}\t1.${plain} Debug Log"
echo -e "${green}\t2.${plain} Clear All logs"
echo -e "${green}\t0.${plain} Back to Main Menu"
read -rp "Choose an option: " choice
if [[ $release == "alpine" ]]; then
echo -e "${green}\t1.${plain} Debug Log"
echo -e "${green}\t0.${plain} Back to Main Menu"
read -rp "Choose an option: " choice
case "$choice" in
0)
show_menu
;;
1)
journalctl -u x-ui -e --no-pager -f -p debug
if [[ $# == 0 ]]; then
before_show_menu
fi
;;
2)
sudo journalctl --rotate
sudo journalctl --vacuum-time=1s
echo "All Logs cleared."
restart
;;
*)
echo -e "${red}Invalid option. Please select a valid number.${plain}\n"
show_log
;;
esac
case "$choice" in
0)
show_menu
;;
1)
grep -F 'x-ui[' /var/log/messages
if [[ $# == 0 ]]; then
before_show_menu
fi
;;
*)
echo -e "${red}Invalid option. Please select a valid number.${plain}\n"
show_log
;;
esac
else
echo -e "${green}\t1.${plain} Debug Log"
echo -e "${green}\t2.${plain} Clear All logs"
echo -e "${green}\t0.${plain} Back to Main Menu"
read -rp "Choose an option: " choice
case "$choice" in
0)
show_menu
;;
1)
journalctl -u x-ui -e --no-pager -f -p debug
if [[ $# == 0 ]]; then
before_show_menu
fi
;;
2)
sudo journalctl --rotate
sudo journalctl --vacuum-time=1s
echo "All Logs cleared."
restart
;;
*)
echo -e "${red}Invalid option. Please select a valid number.${plain}\n"
show_log
;;
esac
fi
}
bbr_menu() {
@ -464,6 +518,12 @@ enable_bbr() {
arch | manjaro | parch)
pacman -Sy --noconfirm ca-certificates
;;
opensuse-tumbleweed | opensuse-leap)
zypper refresh && zypper -q install -y ca-certificates
;;
alpine)
apk add ca-certificates
;;
*)
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
exit 1
@ -500,23 +560,42 @@ update_shell() {
# 0: running, 1: not running, 2: not installed
check_status() {
if [[ ! -f /etc/systemd/system/x-ui.service ]]; then
return 2
fi
temp=$(systemctl status x-ui | grep Active | awk '{print $3}' | cut -d "(" -f2 | cut -d ")" -f1)
if [[ "${temp}" == "running" ]]; then
return 0
if [[ $release == "alpine" ]]; then
if [[ ! -f /etc/init.d/x-ui ]]; then
return 2
fi
if [[ $(rc-service x-ui status | grep -F 'status: started' -c) == 1 ]]; then
return 0
else
return 1
fi
else
return 1
if [[ ! -f /etc/systemd/system/x-ui.service ]]; then
return 2
fi
temp=$(systemctl status x-ui | grep Active | awk '{print $3}' | cut -d "(" -f2 | cut -d ")" -f1)
if [[ "${temp}" == "running" ]]; then
return 0
else
return 1
fi
fi
}
check_enabled() {
temp=$(systemctl is-enabled x-ui)
if [[ "${temp}" == "enabled" ]]; then
return 0
if [[ $release == "alpine" ]]; then
if [[ $(rc-update show | grep -F 'x-ui' | grep default -c) == 1 ]]; then
return 0
else
return 1
fi
else
return 1
temp=$(systemctl is-enabled x-ui)
if [[ "${temp}" == "enabled" ]]; then
return 0
else
return 1
fi
fi
}
@ -798,7 +877,11 @@ update_geo() {
show_menu
;;
1)
systemctl stop x-ui
if [[ $release == "alpine" ]]; then
rc-service x-ui stop
else
systemctl stop x-ui
fi
rm -f geoip.dat geosite.dat
wget -N https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
wget -N https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
@ -806,7 +889,11 @@ update_geo() {
restart
;;
2)
systemctl stop x-ui
if [[ $release == "alpine" ]]; then
rc-service x-ui stop
else
systemctl stop x-ui
fi
rm -f geoip_IR.dat geosite_IR.dat
wget -O geoip_IR.dat -N https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
wget -O geosite_IR.dat -N https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
@ -814,7 +901,11 @@ update_geo() {
restart
;;
3)
systemctl stop x-ui
if [[ $release == "alpine" ]]; then
rc-service x-ui stop
else
systemctl stop x-ui
fi
rm -f geoip_RU.dat geosite_RU.dat
wget -O geoip_RU.dat -N https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
wget -O geosite_RU.dat -N https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
@ -985,6 +1076,12 @@ ssl_cert_issue() {
arch | manjaro | parch)
pacman -Sy --noconfirm socat
;;
opensuse-tumbleweed | opensuse-leap)
zypper refresh && zypper -q install -y socat
;;
alpine)
apk add socat
;;
*)
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
exit 1
@ -1335,7 +1432,11 @@ iplimit_main() {
read -rp "Please enter new Ban Duration in Minutes [default 30]: " NUM
if [[ $NUM =~ ^[0-9]+$ ]]; then
create_iplimit_jails ${NUM}
systemctl restart fail2ban
if [[ $release == "alpine" ]]; then
rc-service fail2ban restart
else
systemctl restart fail2ban
fi
else
echo -e "${red}${NUM} is not a number! Please, try again.${plain}"
fi
@ -1388,7 +1489,11 @@ iplimit_main() {
iplimit_main
;;
9)
systemctl restart fail2ban
if [[ $release == "alpine" ]]; then
rc-service fail2ban restart
else
systemctl restart fail2ban
fi
iplimit_main
;;
10)
@ -1436,6 +1541,9 @@ install_iplimit() {
arch | manjaro | parch)
pacman -Syu --noconfirm fail2ban
;;
alpine)
apk add fail2ban
;;
*)
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
exit 1
@ -1472,12 +1580,21 @@ install_iplimit() {
create_iplimit_jails
# Launching fail2ban
if ! systemctl is-active --quiet fail2ban; then
systemctl start fail2ban
if [[ $release == "alpine" ]]; then
if [[ $(rc-service fail2ban status | grep -F 'status: started' -c) == 0 ]]; then
rc-service fail2ban start
else
rc-service fail2ban restart
fi
rc-update add fail2ban
else
systemctl restart fail2ban
if ! systemctl is-active --quiet fail2ban; then
systemctl start fail2ban
else
systemctl restart fail2ban
fi
systemctl enable fail2ban
fi
systemctl enable fail2ban
echo -e "${green}IP Limit installed and configured successfully!${plain}\n"
before_show_menu
@ -1493,13 +1610,21 @@ remove_iplimit() {
rm -f /etc/fail2ban/filter.d/3x-ipl.conf
rm -f /etc/fail2ban/action.d/3x-ipl.conf
rm -f /etc/fail2ban/jail.d/3x-ipl.conf
systemctl restart fail2ban
if [[ $release == "alpine" ]]; then
rc-service fail2ban restart
else
systemctl restart fail2ban
fi
echo -e "${green}IP Limit removed successfully!${plain}\n"
before_show_menu
;;
2)
rm -rf /etc/fail2ban
systemctl stop fail2ban
if [[ $release == "alpine" ]]; then
rc-service fail2ban stop
else
systemctl stop fail2ban
fi
case "${release}" in
ubuntu | debian | armbian)
apt-get remove -y fail2ban
@ -1517,6 +1642,9 @@ remove_iplimit() {
arch | manjaro | parch)
pacman -Rns --noconfirm fail2ban
;;
alpine)
apk del fail2ban
;;
*)
echo -e "${red}Unsupported operating system. Please uninstall Fail2ban manually.${plain}\n"
exit 1
@ -1540,9 +1668,16 @@ show_banlog() {
echo -e "${green}Checking ban logs...${plain}\n"
if ! systemctl is-active --quiet fail2ban; then
echo -e "${red}Fail2ban service is not running!${plain}\n"
return 1
if [[ $release == "alpine" ]]; then
if [[ $(rc-service fail2ban status | grep -F 'status: started' -c) == 0 ]]; then
echo -e "${red}Fail2ban service is not running!${plain}\n"
return 1
fi
else
if ! systemctl is-active --quiet fail2ban; then
echo -e "${red}Fail2ban service is not running!${plain}\n"
return 1
fi
fi
if [[ -f "$system_log" ]]; then