mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-03-19 09:05:49 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38d87230d3 | ||
|
|
f0f98c7122 | ||
|
|
554981d9d3 | ||
|
|
a08f1c6c13 | ||
|
|
7f7ae0c547 | ||
|
|
60abeaad66 | ||
|
|
a6d0100381 | ||
|
|
6767f76ccf | ||
|
|
e4add73c9e | ||
|
|
ff72090e1a | ||
|
|
a3e1bd59df | ||
|
|
5bbb48a8fd | ||
|
|
ee84d585f9 | ||
|
|
7b03346cfc | ||
|
|
258b08fff3 | ||
|
|
a2097ad062 | ||
|
|
52fdf5d429 | ||
|
|
34d8885075 | ||
|
|
5740996436 | ||
|
|
874aae8080 | ||
|
|
842fae18d7 | ||
|
|
ccd223aeea | ||
|
|
96b8fe472c | ||
|
|
59b695ba83 | ||
|
|
159b85f979 | ||
|
|
3ec5b3589f | ||
|
|
2b1d3e7347 | ||
|
|
37f0880f8f | ||
|
|
5b796672e9 | ||
|
|
3fa0da38c9 | ||
|
|
8eb1225734 | ||
|
|
e5c0fe3edf | ||
|
|
f4057989f5 |
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "github-actions" # See documentation for possible values
|
||||||
|
directory: "/" # Location of package manifests
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
14
.github/workflows/docker.yml
vendored
14
.github/workflows/docker.yml
vendored
@@ -15,13 +15,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v6
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
hsanaeii/3x-ui
|
hsanaeii/3x-ui
|
||||||
@@ -32,28 +32,28 @@ jobs:
|
|||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v4
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
with:
|
with:
|
||||||
install: true
|
install: true
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to GHCR
|
- name: Login to GHCR
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
|
|||||||
64
.github/workflows/release.yml
vendored
64
.github/workflows/release.yml
vendored
@@ -2,11 +2,9 @@ name: Release 3X-UI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- '**'
|
||||||
tags:
|
tags:
|
||||||
- "v*.*.*"
|
- "v*.*.*"
|
||||||
paths:
|
paths:
|
||||||
@@ -20,9 +18,48 @@ on:
|
|||||||
- 'x-ui.service.debian'
|
- 'x-ui.service.debian'
|
||||||
- 'x-ui.service.arch'
|
- 'x-ui.service.arch'
|
||||||
- 'x-ui.service.rhel'
|
- 'x-ui.service.rhel'
|
||||||
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze Go code
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Check formatting
|
||||||
|
run: |
|
||||||
|
unformatted=$(gofmt -l .)
|
||||||
|
if [ -n "$unformatted" ]; then
|
||||||
|
echo "These files are not gofmt-formatted:"
|
||||||
|
echo "$unformatted"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run go vet
|
||||||
|
run: go vet ./...
|
||||||
|
|
||||||
|
- name: Run staticcheck
|
||||||
|
uses: dominikh/staticcheck-action@v1
|
||||||
|
with:
|
||||||
|
version: "latest"
|
||||||
|
install-go: false
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: go test -race -shuffle=on ./...
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
needs: analyze
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
strategy:
|
strategy:
|
||||||
@@ -38,7 +75,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v6
|
||||||
@@ -133,19 +170,17 @@ jobs:
|
|||||||
run: tar -zcvf x-ui-linux-${{ matrix.platform }}.tar.gz x-ui
|
run: tar -zcvf x-ui-linux-${{ matrix.platform }}.tar.gz x-ui
|
||||||
|
|
||||||
- name: Upload files to Artifacts
|
- name: Upload files to Artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: x-ui-linux-${{ matrix.platform }}
|
name: x-ui-linux-${{ matrix.platform }}
|
||||||
path: ./x-ui-linux-${{ matrix.platform }}.tar.gz
|
path: ./x-ui-linux-${{ matrix.platform }}.tar.gz
|
||||||
|
|
||||||
- name: Upload files to GH release
|
- name: Upload files to GH release
|
||||||
uses: svenstaro/upload-release-action@v2
|
uses: svenstaro/upload-release-action@v2
|
||||||
if: |
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||||
(github.event_name == 'release' && github.event.action == 'published') ||
|
|
||||||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
|
||||||
with:
|
with:
|
||||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
tag: ${{ github.ref }}
|
tag: ${{ github.ref_name }}
|
||||||
file: x-ui-linux-${{ matrix.platform }}.tar.gz
|
file: x-ui-linux-${{ matrix.platform }}.tar.gz
|
||||||
asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz
|
asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz
|
||||||
overwrite: true
|
overwrite: true
|
||||||
@@ -156,6 +191,7 @@ jobs:
|
|||||||
# =================================
|
# =================================
|
||||||
build-windows:
|
build-windows:
|
||||||
name: Build for Windows
|
name: Build for Windows
|
||||||
|
needs: analyze
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
strategy:
|
strategy:
|
||||||
@@ -165,7 +201,7 @@ jobs:
|
|||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v6
|
||||||
@@ -230,19 +266,17 @@ jobs:
|
|||||||
Compress-Archive -Path .\x-ui -DestinationPath "x-ui-windows-amd64.zip"
|
Compress-Archive -Path .\x-ui -DestinationPath "x-ui-windows-amd64.zip"
|
||||||
|
|
||||||
- name: Upload files to Artifacts
|
- name: Upload files to Artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: x-ui-windows-amd64
|
name: x-ui-windows-amd64
|
||||||
path: ./x-ui-windows-amd64.zip
|
path: ./x-ui-windows-amd64.zip
|
||||||
|
|
||||||
- name: Upload files to GH release
|
- name: Upload files to GH release
|
||||||
uses: svenstaro/upload-release-action@v2
|
uses: svenstaro/upload-release-action@v2
|
||||||
if: |
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||||
(github.event_name == 'release' && github.event.action == 'published') ||
|
|
||||||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
|
||||||
with:
|
with:
|
||||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
tag: ${{ github.ref }}
|
tag: ${{ github.ref_name }}
|
||||||
file: x-ui-windows-amd64.zip
|
file: x-ui-windows-amd64.zip
|
||||||
asset_name: x-ui-windows-amd64.zip
|
asset_name: x-ui-windows-amd64.zip
|
||||||
overwrite: true
|
overwrite: true
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# ========================================================
|
# ========================================================
|
||||||
# Stage: Builder
|
# Stage: Builder
|
||||||
# ========================================================
|
# ========================================================
|
||||||
FROM golang:1.25-alpine AS builder
|
FROM golang:1.26-alpine AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
2.8.10
|
2.8.11
|
||||||
25
go.mod
25
go.mod
@@ -1,22 +1,22 @@
|
|||||||
module github.com/mhsanaei/3x-ui/v2
|
module github.com/mhsanaei/3x-ui/v2
|
||||||
|
|
||||||
go 1.25.7
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-contrib/gzip v1.2.5
|
github.com/gin-contrib/gzip v1.2.5
|
||||||
github.com/gin-contrib/sessions v1.0.4
|
github.com/gin-contrib/sessions v1.0.4
|
||||||
github.com/gin-gonic/gin v1.11.0
|
github.com/gin-gonic/gin v1.12.0
|
||||||
github.com/go-ldap/ldap/v3 v3.4.12
|
github.com/go-ldap/ldap/v3 v3.4.12
|
||||||
github.com/goccy/go-json v0.10.5
|
github.com/goccy/go-json v0.10.5
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/mymmrac/telego v1.6.0
|
github.com/mymmrac/telego v1.7.0
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.6.1
|
github.com/nicksnyder/go-i18n/v2 v2.6.1
|
||||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4
|
github.com/pelletier/go-toml/v2 v2.2.4
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/shirou/gopsutil/v4 v4.26.1
|
github.com/shirou/gopsutil/v4 v4.26.2
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
github.com/valyala/fasthttp v1.69.0
|
github.com/valyala/fasthttp v1.69.0
|
||||||
github.com/xlzd/gotp v0.1.0
|
github.com/xlzd/gotp v0.1.0
|
||||||
@@ -25,7 +25,7 @@ require (
|
|||||||
golang.org/x/crypto v0.48.0
|
golang.org/x/crypto v0.48.0
|
||||||
golang.org/x/sys v0.41.0
|
golang.org/x/sys v0.41.0
|
||||||
golang.org/x/text v0.34.0
|
golang.org/x/text v0.34.0
|
||||||
google.golang.org/grpc v1.78.0
|
google.golang.org/grpc v1.79.1
|
||||||
gorm.io/driver/sqlite v1.6.0
|
gorm.io/driver/sqlite v1.6.0
|
||||||
gorm.io/gorm v1.31.1
|
gorm.io/gorm v1.31.1
|
||||||
)
|
)
|
||||||
@@ -39,7 +39,7 @@ require (
|
|||||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||||
github.com/cloudflare/circl v1.6.3 // indirect
|
github.com/cloudflare/circl v1.6.3 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/ebitengine/purego v0.9.1 // indirect
|
github.com/ebitengine/purego v0.10.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // 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-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||||
@@ -60,7 +60,7 @@ require (
|
|||||||
github.com/klauspost/compress v1.18.4 // indirect
|
github.com/klauspost/compress v1.18.4 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.34 // indirect
|
github.com/mattn/go-sqlite3 v1.14.34 // indirect
|
||||||
github.com/miekg/dns v1.1.72 // indirect
|
github.com/miekg/dns v1.1.72 // indirect
|
||||||
@@ -72,29 +72,30 @@ require (
|
|||||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||||
github.com/refraction-networking/utls v1.8.2 // indirect
|
github.com/refraction-networking/utls v1.8.2 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
github.com/sagernet/sing v0.7.18 // indirect
|
github.com/sagernet/sing v0.8.1 // indirect
|
||||||
github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
|
github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fastjson v1.6.7 // indirect
|
github.com/valyala/fastjson v1.6.10 // indirect
|
||||||
github.com/vishvananda/netlink v1.3.1 // indirect
|
github.com/vishvananda/netlink v1.3.1 // indirect
|
||||||
github.com/vishvananda/netns v0.0.5 // indirect
|
github.com/vishvananda/netns v0.0.5 // indirect
|
||||||
github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237 // indirect
|
github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||||
golang.org/x/arch v0.24.0 // indirect
|
golang.org/x/arch v0.24.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20260209203927-2842357ff358 // indirect
|
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
|
||||||
golang.org/x/mod v0.33.0 // indirect
|
golang.org/x/mod v0.33.0 // indirect
|
||||||
golang.org/x/net v0.50.0 // indirect
|
golang.org/x/net v0.51.0 // indirect
|
||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/time v0.14.0 // indirect
|
golang.org/x/time v0.14.0 // indirect
|
||||||
golang.org/x/tools v0.42.0 // indirect
|
golang.org/x/tools v0.42.0 // indirect
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
|
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect
|
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect
|
||||||
lukechampine.com/blake3 v1.4.1 // indirect
|
lukechampine.com/blake3 v1.4.1 // indirect
|
||||||
|
|||||||
68
go.sum
68
go.sum
@@ -14,6 +14,8 @@ github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uS
|
|||||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
@@ -21,8 +23,8 @@ github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gE
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
|
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
|
||||||
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
|
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
|
||||||
@@ -33,8 +35,8 @@ github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kb
|
|||||||
github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs=
|
github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs=
|
||||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
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-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.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||||
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 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-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 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
|
||||||
@@ -115,8 +117,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM=
|
||||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
|
github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
|
||||||
@@ -128,8 +130,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/mymmrac/telego v1.6.0 h1:Zc8rgyHozvd/7ZgyrigyHdAF9koHYMfilYfyB6wlFC0=
|
github.com/mymmrac/telego v1.7.0 h1:yRO/l00tFGG4nY66ufUKb4ARqv7qx9+LsjQv/b0NEyo=
|
||||||
github.com/mymmrac/telego v1.6.0/go.mod h1:xt6ZWA8zi8KmuzryE1ImEdl9JSwjHNpM4yhC7D8hU4Y=
|
github.com/mymmrac/telego v1.7.0/go.mod h1:pdLV346EgVuq7Xrh3kMggeBiazeHhsdEoK0RTEOPXRM=
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ=
|
github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ=
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA=
|
github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA=
|
||||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
|
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
|
||||||
@@ -154,12 +156,12 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
|||||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
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/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/sagernet/sing v0.7.18 h1:iZHkaru1/MoHugx3G+9S3WG4owMewKO/KvieE2Pzk4E=
|
github.com/sagernet/sing v0.8.1 h1:Li+zg4xdiMsvdX4j50TPqmSG8LF/TB9US2qlAN40izU=
|
||||||
github.com/sagernet/sing v0.7.18/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
github.com/sagernet/sing v0.8.1/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 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM=
|
||||||
github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8=
|
github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8=
|
||||||
github.com/shirou/gopsutil/v4 v4.26.1 h1:TOkEyriIXk2HX9d4isZJtbjXbEjf5qyKPAzbzY0JWSo=
|
github.com/shirou/gopsutil/v4 v4.26.2 h1:X8i6sicvUFih4BmYIGT1m2wwgw2VG9YgrDTi7cIRGUI=
|
||||||
github.com/shirou/gopsutil/v4 v4.26.1/go.mod h1:medLI9/UNAb0dOI9Q3/7yWSqKkj00u+1tgY8nvv41pc=
|
github.com/shirou/gopsutil/v4 v4.26.2/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
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/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=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
@@ -185,8 +187,8 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
|
|||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||||
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
||||||
github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM=
|
github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4=
|
||||||
github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
|
||||||
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||||
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
||||||
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
||||||
@@ -201,18 +203,20 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
|
|||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
@@ -225,12 +229,12 @@ golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
|
|||||||
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
golang.org/x/exp v0.0.0-20260209203927-2842357ff358 h1:kpfSV7uLwKJbFSEgNhWzGSL47NDSF/5pYYQw1V0ub6c=
|
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
|
||||||
golang.org/x/exp v0.0.0-20260209203927-2842357ff358/go.mod h1:R3t0oliuryB5eenPWl3rrQxwnNM3WTwnsRZZiXLAAW8=
|
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
||||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -253,10 +257,10 @@ 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=
|
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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
||||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ is_port_in_use() {
|
|||||||
install_base() {
|
install_base() {
|
||||||
case "${release}" in
|
case "${release}" in
|
||||||
ubuntu | debian | armbian)
|
ubuntu | debian | armbian)
|
||||||
apt-get update && apt-get install -y -q curl tar tzdata socat ca-certificates
|
apt-get update && apt-get install -y -q cron curl tar tzdata socat ca-certificates
|
||||||
;;
|
;;
|
||||||
fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
|
fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
|
||||||
dnf -y update && dnf install -y -q curl tar tzdata socat ca-certificates
|
dnf -y update && dnf install -y -q curl tar tzdata socat ca-certificates
|
||||||
@@ -654,8 +654,11 @@ config_after_install() {
|
|||||||
)
|
)
|
||||||
local server_ip=""
|
local server_ip=""
|
||||||
for ip_address in "${URL_lists[@]}"; do
|
for ip_address in "${URL_lists[@]}"; do
|
||||||
server_ip=$(curl -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]')
|
local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2>/dev/null)
|
||||||
if [[ -n "${server_ip}" ]]; then
|
local http_code=$(echo "$response" | tail -n1)
|
||||||
|
local ip_result=$(echo "$response" | head -n-1 | tr -d '[:space:]')
|
||||||
|
if [[ "${http_code}" == "200" && -n "${ip_result}" ]]; then
|
||||||
|
server_ip="${ip_result}"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|||||||
9
main.go
9
main.go
@@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"github.com/mhsanaei/3x-ui/v2/sub"
|
"github.com/mhsanaei/3x-ui/v2/sub"
|
||||||
"github.com/mhsanaei/3x-ui/v2/util/crypto"
|
"github.com/mhsanaei/3x-ui/v2/util/crypto"
|
||||||
|
"github.com/mhsanaei/3x-ui/v2/util/sys"
|
||||||
"github.com/mhsanaei/3x-ui/v2/web"
|
"github.com/mhsanaei/3x-ui/v2/web"
|
||||||
"github.com/mhsanaei/3x-ui/v2/web/global"
|
"github.com/mhsanaei/3x-ui/v2/web/global"
|
||||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
@@ -70,7 +71,7 @@ func runWebServer() {
|
|||||||
|
|
||||||
sigCh := make(chan os.Signal, 1)
|
sigCh := make(chan os.Signal, 1)
|
||||||
// Trap shutdown signals
|
// Trap shutdown signals
|
||||||
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM)
|
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, sys.SIGUSR1)
|
||||||
for {
|
for {
|
||||||
sig := <-sigCh
|
sig := <-sigCh
|
||||||
|
|
||||||
@@ -108,6 +109,12 @@ func runWebServer() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Println("Sub server restarted successfully.")
|
log.Println("Sub server restarted successfully.")
|
||||||
|
case sys.SIGUSR1:
|
||||||
|
logger.Info("Received USR1 signal, restarting xray-core...")
|
||||||
|
err := server.RestartXray()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Failed to restart xray-core:", err)
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// --- FIX FOR TELEGRAM BOT CONFLICT (409) on full shutdown ---
|
// --- FIX FOR TELEGRAM BOT CONFLICT (409) on full shutdown ---
|
||||||
|
|||||||
@@ -687,8 +687,11 @@ config_after_update() {
|
|||||||
)
|
)
|
||||||
local server_ip=""
|
local server_ip=""
|
||||||
for ip_address in "${URL_lists[@]}"; do
|
for ip_address in "${URL_lists[@]}"; do
|
||||||
server_ip=$(${curl_bin} -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]')
|
local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2>/dev/null)
|
||||||
if [[ -n "${server_ip}" ]]; then
|
local http_code=$(echo "$response" | tail -n1)
|
||||||
|
local ip_result=$(echo "$response" | head -n-1 | tr -d '[:space:]')
|
||||||
|
if [[ "${http_code}" == "200" && -n "${ip_result}" ]]; then
|
||||||
|
server_ip="${ip_result}"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -7,11 +7,14 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"github.com/shirou/gopsutil/v4/net"
|
"github.com/shirou/gopsutil/v4/net"
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var SIGUSR1 = syscall.SIGUSR1
|
||||||
|
|
||||||
func GetTCPCount() (int, error) {
|
func GetTCPCount() (int, error) {
|
||||||
stats, err := net.Connections("tcp")
|
stats, err := net.Connections("tcp")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -12,8 +12,11 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var SIGUSR1 = syscall.SIGUSR1
|
||||||
|
|
||||||
func getLinesNum(filename string) (int, error) {
|
func getLinesNum(filename string) (int, error) {
|
||||||
file, err := os.Open(filename)
|
file, err := os.Open(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import (
|
|||||||
"github.com/shirou/gopsutil/v4/net"
|
"github.com/shirou/gopsutil/v4/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var SIGUSR1 = syscall.Signal(0)
|
||||||
|
|
||||||
// GetConnectionCount returns the number of active connections for the specified protocol ("tcp" or "udp").
|
// GetConnectionCount returns the number of active connections for the specified protocol ("tcp" or "udp").
|
||||||
func GetConnectionCount(proto string) (int, error) {
|
func GetConnectionCount(proto string) (int, error) {
|
||||||
if proto != "tcp" && proto != "udp" {
|
if proto != "tcp" && proto != "udp" {
|
||||||
|
|||||||
@@ -144,7 +144,7 @@
|
|||||||
return this.app.subUrl;
|
return this.app.subUrl;
|
||||||
},
|
},
|
||||||
happUrl() {
|
happUrl() {
|
||||||
return `happ://add/${encodeURIComponent(this.app.subUrl)}`;
|
return `happ://add/${this.app.subUrl}`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/v2/database/model"
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
@@ -193,6 +194,37 @@ func (a *InboundController) getClientIps(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prefer returning a normalized string list for consistent UI rendering
|
||||||
|
type ipWithTimestamp struct {
|
||||||
|
IP string `json:"ip"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var ipsWithTime []ipWithTimestamp
|
||||||
|
if err := json.Unmarshal([]byte(ips), &ipsWithTime); err == nil && len(ipsWithTime) > 0 {
|
||||||
|
formatted := make([]string, 0, len(ipsWithTime))
|
||||||
|
for _, item := range ipsWithTime {
|
||||||
|
if item.IP == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if item.Timestamp > 0 {
|
||||||
|
ts := time.Unix(item.Timestamp, 0).Local().Format("2006-01-02 15:04:05")
|
||||||
|
formatted = append(formatted, fmt.Sprintf("%s (%s)", item.IP, ts))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
formatted = append(formatted, item.IP)
|
||||||
|
}
|
||||||
|
jsonObj(c, formatted, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldIps []string
|
||||||
|
if err := json.Unmarshal([]byte(ips), &oldIps); err == nil && len(oldIps) > 0 {
|
||||||
|
jsonObj(c, oldIps, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If parsing fails, return as string
|
||||||
jsonObj(c, ips, nil)
|
jsonObj(c, ips, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
@@ -71,14 +72,22 @@ func (a *IndexController) login(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user := a.userService.CheckUser(form.Username, form.Password, form.TwoFactorCode)
|
user, checkErr := a.userService.CheckUser(form.Username, form.Password, form.TwoFactorCode)
|
||||||
timeStr := time.Now().Format("2006-01-02 15:04:05")
|
timeStr := time.Now().Format("2006-01-02 15:04:05")
|
||||||
safeUser := template.HTMLEscapeString(form.Username)
|
safeUser := template.HTMLEscapeString(form.Username)
|
||||||
safePass := template.HTMLEscapeString(form.Password)
|
safePass := template.HTMLEscapeString(form.Password)
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
logger.Warningf("wrong username: \"%s\", password: \"%s\", IP: \"%s\"", safeUser, safePass, getRemoteIp(c))
|
logger.Warningf("wrong username: \"%s\", password: \"%s\", IP: \"%s\"", safeUser, safePass, getRemoteIp(c))
|
||||||
a.tgbot.UserLoginNotify(safeUser, safePass, getRemoteIp(c), timeStr, 0)
|
|
||||||
|
notifyPass := safePass
|
||||||
|
|
||||||
|
if checkErr != nil && checkErr.Error() == "invalid 2fa code" {
|
||||||
|
translatedError := a.tgbot.I18nBot("tgbot.messages.2faFailed")
|
||||||
|
notifyPass = fmt.Sprintf("*** (%s)", translatedError)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.tgbot.UserLoginNotify(safeUser, notifyPass, getRemoteIp(c), timeStr, 0)
|
||||||
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
|
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -612,7 +612,7 @@
|
|||||||
</a-divider>
|
</a-divider>
|
||||||
<a-form-item label='Type'>
|
<a-form-item label='Type'>
|
||||||
<a-select v-model="mask.type"
|
<a-select v-model="mask.type"
|
||||||
@change="(type) => mask.settings = mask._getDefaultSettings(type, {})"
|
@change="(type) => { mask.settings = mask._getDefaultSettings(type, {}); if(outbound.stream.network === 'kcp') { outbound.stream.kcp.mtu = type === 'xdns' ? 900 : 1350; } }"
|
||||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||||
<!-- Salamander for Hysteria2 only -->
|
<!-- Salamander for Hysteria2 only -->
|
||||||
<a-select-option v-if="outbound.protocol === Protocols.Hysteria"
|
<a-select-option v-if="outbound.protocol === Protocols.Hysteria"
|
||||||
@@ -643,9 +643,9 @@
|
|||||||
<a-select-option v-if="outbound.stream.network === 'kcp'"
|
<a-select-option v-if="outbound.stream.network === 'kcp'"
|
||||||
value="mkcp-original">
|
value="mkcp-original">
|
||||||
mKCP Original</a-select-option>
|
mKCP Original</a-select-option>
|
||||||
<!-- xDNS for TCP/WS/HTTPUpgrade/XHTTP -->
|
<!-- xDNS for TCP/WS/HTTPUpgrade/XHTTP/KCP -->
|
||||||
<a-select-option
|
<a-select-option
|
||||||
v-if="['tcp', 'ws', 'httpupgrade', 'xhttp'].includes(outbound.stream.network)"
|
v-if="['tcp', 'ws', 'httpupgrade', 'xhttp', 'kcp'].includes(outbound.stream.network)"
|
||||||
value="xdns">
|
value="xdns">
|
||||||
xDNS (Experimental)</a-select-option>
|
xDNS (Experimental)</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
</a-divider>
|
</a-divider>
|
||||||
<a-form-item label='Type'>
|
<a-form-item label='Type'>
|
||||||
<a-select v-model="mask.type"
|
<a-select v-model="mask.type"
|
||||||
@change="(type) => mask.settings = mask._getDefaultSettings(type, {})"
|
@change="(type) => { mask.settings = mask._getDefaultSettings(type, {}); if(inbound.stream.network === 'kcp') { inbound.stream.kcp.mtu = type === 'xdns' ? 900 : 1350; } }"
|
||||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||||
<!-- mKCP-specific masks -->
|
<!-- mKCP-specific masks -->
|
||||||
<a-select-option v-if="inbound.stream.network === 'kcp'"
|
<a-select-option v-if="inbound.stream.network === 'kcp'"
|
||||||
@@ -48,9 +48,9 @@
|
|||||||
<a-select-option v-if="inbound.stream.network === 'kcp'"
|
<a-select-option v-if="inbound.stream.network === 'kcp'"
|
||||||
value="xicmp">
|
value="xicmp">
|
||||||
xICMP (Experimental)</a-select-option>
|
xICMP (Experimental)</a-select-option>
|
||||||
<!-- xDNS for TCP/WS/HTTPUpgrade/XHTTP -->
|
<!-- xDNS for TCP/WS/HTTPUpgrade/XHTTP/KCP -->
|
||||||
<a-select-option
|
<a-select-option
|
||||||
v-if="['tcp', 'ws', 'httpupgrade', 'xhttp'].includes(inbound.stream.network)"
|
v-if="['tcp', 'ws', 'httpupgrade', 'xhttp', 'kcp'].includes(inbound.stream.network)"
|
||||||
value="xdns">
|
value="xdns">
|
||||||
xDNS (Experimental)</a-select-option>
|
xDNS (Experimental)</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
|
|||||||
@@ -260,15 +260,31 @@
|
|||||||
v-if="app.ipLimitEnable && infoModal.clientSettings.limitIp > 0">
|
v-if="app.ipLimitEnable && infoModal.clientSettings.limitIp > 0">
|
||||||
<td>{{ i18n "pages.inbounds.IPLimitlog" }}</td>
|
<td>{{ i18n "pages.inbounds.IPLimitlog" }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a-tag>[[ infoModal.clientIps ]]</a-tag>
|
<div
|
||||||
<a-icon type="sync" :spin="refreshing" @click="refreshIPs"
|
style="max-height: 150px; overflow-y: auto; text-align: left;">
|
||||||
:style="{ margin: '0 5px' }"></a-icon>
|
<div
|
||||||
<a-tooltip :title="[[ dbInbound.address ]]">
|
v-if="infoModal.clientIpsArray && infoModal.clientIpsArray.length > 0">
|
||||||
<template slot="title">
|
<a-tag
|
||||||
<span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span>
|
v-for="(ipInfo, idx) in infoModal.clientIpsArray"
|
||||||
</template>
|
:key="idx"
|
||||||
<a-icon type="delete" @click="clearClientIps"></a-icon>
|
color="blue"
|
||||||
</a-tooltip>
|
style="margin: 2px 0; display: block; font-family: monospace; font-size: 11px;">
|
||||||
|
[[ formatIpInfo(ipInfo) ]]
|
||||||
|
</a-tag>
|
||||||
|
</div>
|
||||||
|
<a-tag v-else>[[ infoModal.clientIps || 'No IP Record'
|
||||||
|
]]</a-tag>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 5px;">
|
||||||
|
<a-icon type="sync" :spin="refreshing" @click="refreshIPs"
|
||||||
|
:style="{ margin: '0 5px' }"></a-icon>
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
<span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span>
|
||||||
|
</template>
|
||||||
|
<a-icon type="delete" @click="clearClientIps"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -542,12 +558,73 @@
|
|||||||
<script>
|
<script>
|
||||||
function refreshIPs(email) {
|
function refreshIPs(email) {
|
||||||
return HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`).then((msg) => {
|
return HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`).then((msg) => {
|
||||||
if (msg.success) {
|
if (!msg.success) {
|
||||||
try {
|
return { text: 'No IP Record', array: [] };
|
||||||
return JSON.parse(msg.obj).join(', ');
|
}
|
||||||
} catch (e) {
|
|
||||||
return msg.obj;
|
const formatIpRecord = (record) => {
|
||||||
|
if (record == null) {
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
|
if (typeof record === 'string' || typeof record === 'number') {
|
||||||
|
return String(record);
|
||||||
|
}
|
||||||
|
const ip = record.ip || record.IP || '';
|
||||||
|
const timestamp = record.timestamp || record.Timestamp || 0;
|
||||||
|
if (!ip) {
|
||||||
|
return String(record);
|
||||||
|
}
|
||||||
|
if (!timestamp) {
|
||||||
|
return String(ip);
|
||||||
|
}
|
||||||
|
const date = new Date(Number(timestamp) * 1000);
|
||||||
|
const timeStr = date
|
||||||
|
.toLocaleString('en-GB', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
})
|
||||||
|
.replace(',', '');
|
||||||
|
return `${ip} (${timeStr})`;
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
let ips = msg.obj;
|
||||||
|
// If msg.obj is a string, try to parse it
|
||||||
|
if (typeof ips === 'string') {
|
||||||
|
try {
|
||||||
|
ips = JSON.parse(ips);
|
||||||
|
} catch (e) {
|
||||||
|
return { text: String(ips), array: [String(ips)] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize single object response to array
|
||||||
|
if (ips && !Array.isArray(ips) && typeof ips === 'object') {
|
||||||
|
ips = [ips];
|
||||||
|
}
|
||||||
|
|
||||||
|
// New format or object array
|
||||||
|
if (Array.isArray(ips) && ips.length > 0 && typeof ips[0] === 'object') {
|
||||||
|
const result = ips.map((item) => formatIpRecord(item)).filter(Boolean);
|
||||||
|
return { text: result.join(' | '), array: result };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Old format - simple array of IPs
|
||||||
|
if (Array.isArray(ips) && ips.length > 0) {
|
||||||
|
const result = ips.map((ip) => String(ip));
|
||||||
|
return { text: result.join(', '), array: result };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for any other format
|
||||||
|
return { text: String(ips), array: [String(ips)] };
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
return { text: 'Error loading IPs', array: [] };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -566,6 +643,7 @@
|
|||||||
subLink: '',
|
subLink: '',
|
||||||
subJsonLink: '',
|
subJsonLink: '',
|
||||||
clientIps: '',
|
clientIps: '',
|
||||||
|
clientIpsArray: [],
|
||||||
show(dbInbound, index) {
|
show(dbInbound, index) {
|
||||||
this.index = index;
|
this.index = index;
|
||||||
this.inbound = dbInbound.toInbound();
|
this.inbound = dbInbound.toInbound();
|
||||||
@@ -583,8 +661,9 @@
|
|||||||
].includes(this.inbound.protocol)
|
].includes(this.inbound.protocol)
|
||||||
) {
|
) {
|
||||||
if (app.ipLimitEnable && this.clientSettings.limitIp) {
|
if (app.ipLimitEnable && this.clientSettings.limitIp) {
|
||||||
refreshIPs(this.clientStats.email).then((ips) => {
|
refreshIPs(this.clientStats.email).then((result) => {
|
||||||
this.clientIps = ips;
|
this.clientIps = result.text;
|
||||||
|
this.clientIpsArray = result.array;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -655,6 +734,35 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
formatIpInfo(ipInfo) {
|
||||||
|
if (ipInfo == null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (typeof ipInfo === 'string' || typeof ipInfo === 'number') {
|
||||||
|
return String(ipInfo);
|
||||||
|
}
|
||||||
|
const ip = ipInfo.ip || ipInfo.IP || '';
|
||||||
|
const timestamp = ipInfo.timestamp || ipInfo.Timestamp || 0;
|
||||||
|
if (!ip) {
|
||||||
|
return String(ipInfo);
|
||||||
|
}
|
||||||
|
if (!timestamp) {
|
||||||
|
return String(ip);
|
||||||
|
}
|
||||||
|
const date = new Date(Number(timestamp) * 1000);
|
||||||
|
const timeStr = date
|
||||||
|
.toLocaleString('en-GB', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
})
|
||||||
|
.replace(',', '');
|
||||||
|
return `${ip} (${timeStr})`;
|
||||||
|
},
|
||||||
copy(content) {
|
copy(content) {
|
||||||
ClipboardManager
|
ClipboardManager
|
||||||
.copyText(content)
|
.copyText(content)
|
||||||
@@ -672,8 +780,9 @@
|
|||||||
refreshIPs() {
|
refreshIPs() {
|
||||||
this.refreshing = true;
|
this.refreshing = true;
|
||||||
refreshIPs(this.infoModal.clientStats.email)
|
refreshIPs(this.infoModal.clientStats.email)
|
||||||
.then((ips) => {
|
.then((result) => {
|
||||||
this.infoModal.clientIps = ips;
|
this.infoModal.clientIps = result.text;
|
||||||
|
this.infoModal.clientIpsArray = result.array;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.refreshing = false;
|
this.refreshing = false;
|
||||||
@@ -686,6 +795,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.infoModal.clientIps = 'No IP Record';
|
this.infoModal.clientIps = 'No IP Record';
|
||||||
|
this.infoModal.clientIpsArray = [];
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -206,7 +206,7 @@
|
|||||||
<a-menu-item key="android-npvtunnel" @click="copy(app.subUrl)">NPV
|
<a-menu-item key="android-npvtunnel" @click="copy(app.subUrl)">NPV
|
||||||
Tunnel</a-menu-item>
|
Tunnel</a-menu-item>
|
||||||
<a-menu-item key="android-happ"
|
<a-menu-item key="android-happ"
|
||||||
@click="open('happ://add/' + encodeURIComponent(app.subUrl))">Happ</a-menu-item>
|
@click="open('happ://add/' + app.subUrl)">Happ</a-menu-item>
|
||||||
</a-menu>
|
</a-menu>
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
</a-col>
|
</a-col>
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/v2/database"
|
"github.com/mhsanaei/3x-ui/v2/database"
|
||||||
@@ -319,13 +318,14 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert back to slice and sort by timestamp (newest first)
|
// Convert back to slice and sort by timestamp (oldest first)
|
||||||
|
// This ensures we always protect the original/current connections and ban new excess ones.
|
||||||
allIps := make([]IPWithTimestamp, 0, len(ipMap))
|
allIps := make([]IPWithTimestamp, 0, len(ipMap))
|
||||||
for ip, timestamp := range ipMap {
|
for ip, timestamp := range ipMap {
|
||||||
allIps = append(allIps, IPWithTimestamp{IP: ip, Timestamp: timestamp})
|
allIps = append(allIps, IPWithTimestamp{IP: ip, Timestamp: timestamp})
|
||||||
}
|
}
|
||||||
sort.Slice(allIps, func(i, j int) bool {
|
sort.Slice(allIps, func(i, j int) bool {
|
||||||
return allIps[i].Timestamp > allIps[j].Timestamp // Descending order (newest first)
|
return allIps[i].Timestamp < allIps[j].Timestamp // Ascending order (oldest first)
|
||||||
})
|
})
|
||||||
|
|
||||||
shouldCleanLog := false
|
shouldCleanLog := false
|
||||||
@@ -345,23 +345,17 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
|
|||||||
if len(allIps) > limitIp {
|
if len(allIps) > limitIp {
|
||||||
shouldCleanLog = true
|
shouldCleanLog = true
|
||||||
|
|
||||||
// Keep only the newest IPs (up to limitIp)
|
// Keep the oldest IPs (currently active connections) and ban the new excess ones.
|
||||||
keptIps := allIps[:limitIp]
|
keptIps := allIps[:limitIp]
|
||||||
disconnectedIps := allIps[limitIp:]
|
bannedIps := allIps[limitIp:]
|
||||||
|
|
||||||
// Log the disconnected IPs (old ones)
|
// Log banned IPs in the format fail2ban filters expect: [LIMIT_IP] Email = X || Disconnecting OLD IP = Y || Timestamp = Z
|
||||||
for _, ipTime := range disconnectedIps {
|
for _, ipTime := range bannedIps {
|
||||||
j.disAllowedIps = append(j.disAllowedIps, ipTime.IP)
|
j.disAllowedIps = append(j.disAllowedIps, ipTime.IP)
|
||||||
log.Printf("[LIMIT_IP] Email = %s || Disconnecting OLD IP = %s || Timestamp = %d", clientEmail, ipTime.IP, ipTime.Timestamp)
|
log.Printf("[LIMIT_IP] Email = %s || Disconnecting OLD IP = %s || Timestamp = %d", clientEmail, ipTime.IP, ipTime.Timestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actually disconnect old IPs by temporarily removing and re-adding user
|
// Update database with only the currently active (kept) IPs
|
||||||
// This forces Xray to drop existing connections from old IPs
|
|
||||||
if len(disconnectedIps) > 0 {
|
|
||||||
j.disconnectClientTemporarily(inbound, clientEmail, clients)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update database with only the newest IPs
|
|
||||||
jsonIps, _ := json.Marshal(keptIps)
|
jsonIps, _ := json.Marshal(keptIps)
|
||||||
inboundClientIps.Ips = string(jsonIps)
|
inboundClientIps.Ips = string(jsonIps)
|
||||||
} else {
|
} else {
|
||||||
@@ -378,67 +372,12 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(j.disAllowedIps) > 0 {
|
if len(j.disAllowedIps) > 0 {
|
||||||
logger.Infof("[LIMIT_IP] Client %s: Kept %d newest IPs, disconnected %d old IPs", clientEmail, limitIp, len(j.disAllowedIps))
|
logger.Infof("[LIMIT_IP] Client %s: Kept %d current IPs, queued %d new IPs for fail2ban", clientEmail, limitIp, len(j.disAllowedIps))
|
||||||
}
|
}
|
||||||
|
|
||||||
return shouldCleanLog
|
return shouldCleanLog
|
||||||
}
|
}
|
||||||
|
|
||||||
// disconnectClientTemporarily removes and re-adds a client to force disconnect old connections
|
|
||||||
func (j *CheckClientIpJob) disconnectClientTemporarily(inbound *model.Inbound, clientEmail string, clients []model.Client) {
|
|
||||||
var xrayAPI xray.XrayAPI
|
|
||||||
|
|
||||||
// Get panel settings for API port
|
|
||||||
db := database.GetDB()
|
|
||||||
var apiPort int
|
|
||||||
var apiPortSetting model.Setting
|
|
||||||
if err := db.Where("key = ?", "xrayApiPort").First(&apiPortSetting).Error; err == nil {
|
|
||||||
apiPort, _ = strconv.Atoi(apiPortSetting.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
if apiPort == 0 {
|
|
||||||
apiPort = 10085 // Default API port
|
|
||||||
}
|
|
||||||
|
|
||||||
err := xrayAPI.Init(apiPort)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warningf("[LIMIT_IP] Failed to init Xray API for disconnection: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer xrayAPI.Close()
|
|
||||||
|
|
||||||
// Find the client config
|
|
||||||
var clientConfig map[string]any
|
|
||||||
for _, client := range clients {
|
|
||||||
if client.Email == clientEmail {
|
|
||||||
// Convert client to map for API
|
|
||||||
clientBytes, _ := json.Marshal(client)
|
|
||||||
json.Unmarshal(clientBytes, &clientConfig)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if clientConfig == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove user to disconnect all connections
|
|
||||||
err = xrayAPI.RemoveUser(inbound.Tag, clientEmail)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warningf("[LIMIT_IP] Failed to remove user %s: %v", clientEmail, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait a moment for disconnection to take effect
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// Re-add user to allow new connections
|
|
||||||
err = xrayAPI.AddUser(string(inbound.Protocol), inbound.Tag, clientConfig)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warningf("[LIMIT_IP] Failed to re-add user %s: %v", clientEmail, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *CheckClientIpJob) getInboundByEmail(clientEmail string) (*model.Inbound, error) {
|
func (j *CheckClientIpJob) getInboundByEmail(clientEmail string) (*model.Inbound, error) {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
inbound := &model.Inbound{}
|
inbound := &model.Inbound{}
|
||||||
|
|||||||
@@ -271,10 +271,7 @@ func (j *LdapSyncJob) deleteClientsNotInLDAP(inboundTag string, ldapEmails map[s
|
|||||||
|
|
||||||
// Delete in batches
|
// Delete in batches
|
||||||
for i := 0; i < len(toDelete); i += batchSize {
|
for i := 0; i < len(toDelete); i += batchSize {
|
||||||
end := i + batchSize
|
end := min(i+batchSize, len(toDelete))
|
||||||
if end > len(toDelete) {
|
|
||||||
end = len(toDelete)
|
|
||||||
}
|
|
||||||
batch := toDelete[i:end]
|
batch := toDelete[i:end]
|
||||||
|
|
||||||
for _, c := range batch {
|
for _, c := range batch {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ type SettingService interface {
|
|||||||
|
|
||||||
// InitLocalizer initializes the internationalization system with embedded translation files.
|
// InitLocalizer initializes the internationalization system with embedded translation files.
|
||||||
func InitLocalizer(i18nFS embed.FS, settingService SettingService) error {
|
func InitLocalizer(i18nFS embed.FS, settingService SettingService) error {
|
||||||
// set default bundle to english
|
// set default bundle to English
|
||||||
i18nBundle = i18n.NewBundle(language.MustParse("en-US"))
|
i18nBundle = i18n.NewBundle(language.MustParse("en-US"))
|
||||||
i18nBundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
|
i18nBundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
|
||||||
|
|
||||||
|
|||||||
@@ -2032,7 +2032,6 @@ func (s *InboundService) GetClientTrafficByEmail(email string) (traffic *xray.Cl
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if t != nil && client != nil {
|
if t != nil && client != nil {
|
||||||
t.Enable = client.Enable
|
|
||||||
t.UUID = client.ID
|
t.UUID = client.ID
|
||||||
t.SubId = client.SubID
|
t.SubId = client.SubID
|
||||||
return t, nil
|
return t, nil
|
||||||
@@ -2141,6 +2140,43 @@ func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if InboundClientIps.Ips == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse as new format (with timestamps)
|
||||||
|
type IPWithTimestamp struct {
|
||||||
|
IP string `json:"ip"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var ipsWithTime []IPWithTimestamp
|
||||||
|
err = json.Unmarshal([]byte(InboundClientIps.Ips), &ipsWithTime)
|
||||||
|
|
||||||
|
// If successfully parsed as new format, return with timestamps
|
||||||
|
if err == nil && len(ipsWithTime) > 0 {
|
||||||
|
return InboundClientIps.Ips, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, assume it's old format (simple string array)
|
||||||
|
// Try to parse as simple array and convert to new format
|
||||||
|
var oldIps []string
|
||||||
|
err = json.Unmarshal([]byte(InboundClientIps.Ips), &oldIps)
|
||||||
|
if err == nil && len(oldIps) > 0 {
|
||||||
|
// Convert old format to new format with current timestamp
|
||||||
|
newIpsWithTime := make([]IPWithTimestamp, len(oldIps))
|
||||||
|
for i, ip := range oldIps {
|
||||||
|
newIpsWithTime[i] = IPWithTimestamp{
|
||||||
|
IP: ip,
|
||||||
|
Timestamp: time.Now().Unix(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result, _ := json.Marshal(newIpsWithTime)
|
||||||
|
return string(result), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return as-is if parsing fails
|
||||||
return InboundClientIps.Ips, nil
|
return InboundClientIps.Ips, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -108,7 +108,7 @@ var defaultValueMap = map[string]string{
|
|||||||
// It handles configuration storage, retrieval, and validation for all system settings.
|
// It handles configuration storage, retrieval, and validation for all system settings.
|
||||||
type SettingService struct{}
|
type SettingService struct{}
|
||||||
|
|
||||||
func (s *SettingService) GetDefaultJsonConfig() (any, error) {
|
func (s *SettingService) GetDefaultJSONConfig() (any, error) {
|
||||||
var jsonData any
|
var jsonData any
|
||||||
err := json.Unmarshal([]byte(xrayTemplateConfig), &jsonData)
|
err := json.Unmarshal([]byte(xrayTemplateConfig), &jsonData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -125,7 +125,7 @@ func (s *SettingService) GetAllSetting() (*entity.AllSetting, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
allSetting := &entity.AllSetting{}
|
allSetting := &entity.AllSetting{}
|
||||||
t := reflect.TypeOf(allSetting).Elem()
|
t := reflect.TypeFor[entity.AllSetting]()
|
||||||
v := reflect.ValueOf(allSetting).Elem()
|
v := reflect.ValueOf(allSetting).Elem()
|
||||||
fields := reflect_util.GetFields(t)
|
fields := reflect_util.GetFields(t)
|
||||||
|
|
||||||
@@ -607,7 +607,7 @@ func (s *SettingService) GetIpLimitEnable() (bool, error) {
|
|||||||
return (accessLogPath != "none" && accessLogPath != ""), nil
|
return (accessLogPath != "none" && accessLogPath != ""), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LDAP exported getters
|
// GetLdapEnable returns whether LDAP is enabled.
|
||||||
func (s *SettingService) GetLdapEnable() (bool, error) {
|
func (s *SettingService) GetLdapEnable() (bool, error) {
|
||||||
return s.getBool("ldapEnable")
|
return s.getBool("ldapEnable")
|
||||||
}
|
}
|
||||||
@@ -694,7 +694,7 @@ func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
v := reflect.ValueOf(allSetting).Elem()
|
v := reflect.ValueOf(allSetting).Elem()
|
||||||
t := reflect.TypeOf(allSetting).Elem()
|
t := reflect.TypeFor[entity.AllSetting]()
|
||||||
fields := reflect_util.GetFields(t)
|
fields := reflect_util.GetFields(t)
|
||||||
errs := make([]error, 0)
|
errs := make([]error, 0)
|
||||||
for _, field := range fields {
|
for _, field := range fields {
|
||||||
@@ -719,25 +719,25 @@ func (s *SettingService) GetDefaultXrayConfig() (any, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func extractHostname(host string) string {
|
func extractHostname(host string) string {
|
||||||
h, _, err := net.SplitHostPort(host)
|
h, _, err := net.SplitHostPort(host)
|
||||||
// Err is not nil means host does not contain port
|
// Err is not nil means host does not contain port
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h = host
|
h = host
|
||||||
}
|
}
|
||||||
|
|
||||||
ip := net.ParseIP(h)
|
ip := net.ParseIP(h)
|
||||||
// If it's not an IP, return as is
|
// If it's not an IP, return as is
|
||||||
if ip == nil {
|
if ip == nil {
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's an IPv4, return as is
|
// If it's an IPv4, return as is
|
||||||
if ip.To4() != nil {
|
if ip.To4() != nil {
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
// IPv6 needs bracketing
|
// IPv6 needs bracketing
|
||||||
return "[" + h + "]"
|
return "[" + h + "]"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SettingService) GetDefaultSettings(host string) (any, error) {
|
func (s *SettingService) GetDefaultSettings(host string) (any, error) {
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"embed"
|
"embed"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html"
|
||||||
"io"
|
"io"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
@@ -14,6 +16,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -271,41 +274,78 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createRobustFastHTTPClient creates a fasthttp.Client with proper connection handling
|
||||||
|
func (t *Tgbot) createRobustFastHTTPClient(proxyUrl string) *fasthttp.Client {
|
||||||
|
client := &fasthttp.Client{
|
||||||
|
// Connection timeouts
|
||||||
|
ReadTimeout: 30 * time.Second,
|
||||||
|
WriteTimeout: 30 * time.Second,
|
||||||
|
MaxIdleConnDuration: 60 * time.Second,
|
||||||
|
MaxConnDuration: 0, // unlimited, but controlled by MaxIdleConnDuration
|
||||||
|
MaxIdemponentCallAttempts: 3,
|
||||||
|
ReadBufferSize: 4096,
|
||||||
|
WriteBufferSize: 4096,
|
||||||
|
MaxConnsPerHost: 100,
|
||||||
|
MaxConnWaitTimeout: 10 * time.Second,
|
||||||
|
DisableHeaderNamesNormalizing: false,
|
||||||
|
DisablePathNormalizing: false,
|
||||||
|
// Retry on connection errors
|
||||||
|
RetryIf: func(request *fasthttp.Request) bool {
|
||||||
|
// Retry on connection errors for GET requests
|
||||||
|
return string(request.Header.Method()) == "GET" || string(request.Header.Method()) == "POST"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set proxy if provided
|
||||||
|
if proxyUrl != "" {
|
||||||
|
client.Dial = fasthttpproxy.FasthttpSocksDialer(proxyUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
// NewBot creates a new Telegram bot instance with optional proxy and API server settings.
|
// NewBot creates a new Telegram bot instance with optional proxy and API server settings.
|
||||||
func (t *Tgbot) NewBot(token string, proxyUrl string, apiServerUrl string) (*telego.Bot, error) {
|
func (t *Tgbot) NewBot(token string, proxyUrl string, apiServerUrl string) (*telego.Bot, error) {
|
||||||
if proxyUrl == "" && apiServerUrl == "" {
|
// Validate proxy URL if provided
|
||||||
return telego.NewBot(token)
|
|
||||||
}
|
|
||||||
|
|
||||||
if proxyUrl != "" {
|
if proxyUrl != "" {
|
||||||
if !strings.HasPrefix(proxyUrl, "socks5://") {
|
if !strings.HasPrefix(proxyUrl, "socks5://") {
|
||||||
logger.Warning("Invalid socks5 URL, using default")
|
logger.Warning("Invalid socks5 URL, ignoring proxy")
|
||||||
return telego.NewBot(token)
|
proxyUrl = "" // Clear invalid proxy
|
||||||
|
} else {
|
||||||
|
_, err := url.Parse(proxyUrl)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warningf("Can't parse proxy URL, ignoring proxy: %v", err)
|
||||||
|
proxyUrl = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_, err := url.Parse(proxyUrl)
|
// Validate API server URL if provided
|
||||||
if err != nil {
|
if apiServerUrl != "" {
|
||||||
logger.Warningf("Can't parse proxy URL, using default instance for tgbot: %v", err)
|
if !strings.HasPrefix(apiServerUrl, "http") {
|
||||||
return telego.NewBot(token)
|
logger.Warning("Invalid http(s) URL for API server, using default")
|
||||||
|
apiServerUrl = ""
|
||||||
|
} else {
|
||||||
|
_, err := url.Parse(apiServerUrl)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warningf("Can't parse API server URL, using default: %v", err)
|
||||||
|
apiServerUrl = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return telego.NewBot(token, telego.WithFastHTTPClient(&fasthttp.Client{
|
|
||||||
Dial: fasthttpproxy.FasthttpSocksDialer(proxyUrl),
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.HasPrefix(apiServerUrl, "http") {
|
// Create robust fasthttp client
|
||||||
logger.Warning("Invalid http(s) URL, using default")
|
client := t.createRobustFastHTTPClient(proxyUrl)
|
||||||
return telego.NewBot(token)
|
|
||||||
|
// Build bot options
|
||||||
|
var options []telego.BotOption
|
||||||
|
options = append(options, telego.WithFastHTTPClient(client))
|
||||||
|
|
||||||
|
if apiServerUrl != "" {
|
||||||
|
options = append(options, telego.WithAPIServer(apiServerUrl))
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := url.Parse(apiServerUrl)
|
return telego.NewBot(token, options...)
|
||||||
if err != nil {
|
|
||||||
logger.Warningf("Can't parse API server URL, using default instance for tgbot: %v", err)
|
|
||||||
return telego.NewBot(token)
|
|
||||||
}
|
|
||||||
|
|
||||||
return telego.NewBot(token, telego.WithAPIServer(apiServerUrl))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsRunning checks if the Telegram bot is currently running.
|
// IsRunning checks if the Telegram bot is currently running.
|
||||||
@@ -389,7 +429,7 @@ func (t *Tgbot) decodeQuery(query string) (string, error) {
|
|||||||
// OnReceive starts the message receiving loop for the Telegram bot.
|
// OnReceive starts the message receiving loop for the Telegram bot.
|
||||||
func (t *Tgbot) OnReceive() {
|
func (t *Tgbot) OnReceive() {
|
||||||
params := telego.GetUpdatesParams{
|
params := telego.GetUpdatesParams{
|
||||||
Timeout: 30, // Increased timeout to reduce API calls
|
Timeout: 20, // Reduced timeout to detect connection issues faster
|
||||||
}
|
}
|
||||||
// Strict singleton: never start a second long-polling loop.
|
// Strict singleton: never start a second long-polling loop.
|
||||||
tgBotMutex.Lock()
|
tgBotMutex.Lock()
|
||||||
@@ -407,7 +447,7 @@ func (t *Tgbot) OnReceive() {
|
|||||||
botWG.Add(1)
|
botWG.Add(1)
|
||||||
tgBotMutex.Unlock()
|
tgBotMutex.Unlock()
|
||||||
|
|
||||||
// Get updates channel using the context.
|
// Get updates channel using the context with shorter timeout for better error recovery
|
||||||
updates, _ := bot.UpdatesViaLongPolling(ctx, ¶ms)
|
updates, _ := bot.UpdatesViaLongPolling(ctx, ¶ms)
|
||||||
go func() {
|
go func() {
|
||||||
defer botWG.Done()
|
defer botWG.Done()
|
||||||
@@ -613,7 +653,7 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo
|
|||||||
msg += t.I18nBot("tgbot.commands.help")
|
msg += t.I18nBot("tgbot.commands.help")
|
||||||
msg += t.I18nBot("tgbot.commands.pleaseChoose")
|
msg += t.I18nBot("tgbot.commands.pleaseChoose")
|
||||||
case "start":
|
case "start":
|
||||||
msg += t.I18nBot("tgbot.commands.start", "Firstname=="+message.From.FirstName)
|
msg += t.I18nBot("tgbot.commands.start", "Firstname=="+html.EscapeString(message.From.FirstName))
|
||||||
if isAdmin {
|
if isAdmin {
|
||||||
msg += t.I18nBot("tgbot.commands.welcome", "Hostname=="+hostname)
|
msg += t.I18nBot("tgbot.commands.welcome", "Hostname=="+hostname)
|
||||||
}
|
}
|
||||||
@@ -1886,6 +1926,8 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
|||||||
} else {
|
} else {
|
||||||
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
|
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
|
||||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.successfulOperation"), tu.ReplyKeyboardRemove())
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.successfulOperation"), tu.ReplyKeyboardRemove())
|
||||||
|
t.sendClientIndividualLinks(chatId, client_Email)
|
||||||
|
t.sendClientQRLinks(chatId, client_Email)
|
||||||
}
|
}
|
||||||
case "add_client_submit_enable":
|
case "add_client_submit_enable":
|
||||||
client_Enable = true
|
client_Enable = true
|
||||||
@@ -1896,6 +1938,8 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
|||||||
} else {
|
} else {
|
||||||
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
|
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
|
||||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.successfulOperation"), tu.ReplyKeyboardRemove())
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.successfulOperation"), tu.ReplyKeyboardRemove())
|
||||||
|
t.sendClientIndividualLinks(chatId, client_Email)
|
||||||
|
t.sendClientQRLinks(chatId, client_Email)
|
||||||
}
|
}
|
||||||
case "reset_all_traffics_cancel":
|
case "reset_all_traffics_cancel":
|
||||||
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
|
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
|
||||||
@@ -2246,10 +2290,36 @@ func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.R
|
|||||||
if len(replyMarkup) > 0 && n == (len(allMessages)-1) {
|
if len(replyMarkup) > 0 && n == (len(allMessages)-1) {
|
||||||
params.ReplyMarkup = replyMarkup[0]
|
params.ReplyMarkup = replyMarkup[0]
|
||||||
}
|
}
|
||||||
_, err := bot.SendMessage(context.Background(), ¶ms)
|
|
||||||
if err != nil {
|
// Retry logic with exponential backoff for connection errors
|
||||||
logger.Warning("Error sending telegram message :", err)
|
maxRetries := 3
|
||||||
|
for attempt := range maxRetries {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
_, err := bot.SendMessage(ctx, ¶ms)
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
break // Success
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if error is a connection error
|
||||||
|
errStr := err.Error()
|
||||||
|
isConnectionError := strings.Contains(errStr, "connection") ||
|
||||||
|
strings.Contains(errStr, "timeout") ||
|
||||||
|
strings.Contains(errStr, "closed")
|
||||||
|
|
||||||
|
if isConnectionError && attempt < maxRetries-1 {
|
||||||
|
// Exponential backoff: 1s, 2s, 4s
|
||||||
|
backoff := time.Duration(1<<uint(attempt)) * time.Second
|
||||||
|
logger.Warningf("Connection error sending telegram message (attempt %d/%d), retrying in %v: %v",
|
||||||
|
attempt+1, maxRetries, backoff, err)
|
||||||
|
time.Sleep(backoff)
|
||||||
|
} else {
|
||||||
|
logger.Warning("Error sending telegram message:", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reduced delay to improve performance (only needed for rate limiting)
|
// Reduced delay to improve performance (only needed for rate limiting)
|
||||||
if n < len(allMessages)-1 { // Only delay between messages, not after the last one
|
if n < len(allMessages)-1 { // Only delay between messages, not after the last one
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
@@ -2584,8 +2654,12 @@ func (t *Tgbot) SendBackupToAdmins() {
|
|||||||
if !t.IsRunning() {
|
if !t.IsRunning() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, adminId := range adminIds {
|
for i, adminId := range adminIds {
|
||||||
t.sendBackup(int64(adminId))
|
t.sendBackup(int64(adminId))
|
||||||
|
// Add delay between sends to avoid Telegram rate limits
|
||||||
|
if i < len(adminIds)-1 {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2650,7 +2724,7 @@ func (t *Tgbot) prepareServerUsageInfo() string {
|
|||||||
info += t.I18nBot("tgbot.messages.ip", "IP=="+t.I18nBot("tgbot.unknown"))
|
info += t.I18nBot("tgbot.messages.ip", "IP=="+t.I18nBot("tgbot.unknown"))
|
||||||
info += "\r\n"
|
info += "\r\n"
|
||||||
} else {
|
} else {
|
||||||
for i := 0; i < len(netInterfaces); i++ {
|
for i := range netInterfaces {
|
||||||
if (netInterfaces[i].Flags & net.FlagUp) != 0 {
|
if (netInterfaces[i].Flags & net.FlagUp) != 0 {
|
||||||
addrs, _ := netInterfaces[i].Addrs()
|
addrs, _ := netInterfaces[i].Addrs()
|
||||||
|
|
||||||
@@ -2719,29 +2793,29 @@ func (t *Tgbot) UserLoginNotify(username string, password string, ip string, tim
|
|||||||
|
|
||||||
// getInboundUsages retrieves and formats inbound usage information.
|
// getInboundUsages retrieves and formats inbound usage information.
|
||||||
func (t *Tgbot) getInboundUsages() string {
|
func (t *Tgbot) getInboundUsages() string {
|
||||||
info := ""
|
var info strings.Builder
|
||||||
// get traffic
|
// get traffic
|
||||||
inbounds, err := t.inboundService.GetAllInbounds()
|
inbounds, err := t.inboundService.GetAllInbounds()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warning("GetAllInbounds run failed:", err)
|
logger.Warning("GetAllInbounds run failed:", err)
|
||||||
info += t.I18nBot("tgbot.answers.getInboundsFailed")
|
info.WriteString(t.I18nBot("tgbot.answers.getInboundsFailed"))
|
||||||
} else {
|
} else {
|
||||||
// NOTE:If there no any sessions here,need to notify here
|
// NOTE:If there no any sessions here,need to notify here
|
||||||
// TODO:Sub-node push, automatic conversion format
|
// TODO:Sub-node push, automatic conversion format
|
||||||
for _, inbound := range inbounds {
|
for _, inbound := range inbounds {
|
||||||
info += t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark)
|
info.WriteString(t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark))
|
||||||
info += t.I18nBot("tgbot.messages.port", "Port=="+strconv.Itoa(inbound.Port))
|
info.WriteString(t.I18nBot("tgbot.messages.port", "Port=="+strconv.Itoa(inbound.Port)))
|
||||||
info += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down))
|
info.WriteString(t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down)))
|
||||||
|
|
||||||
if inbound.ExpiryTime == 0 {
|
if inbound.ExpiryTime == 0 {
|
||||||
info += t.I18nBot("tgbot.messages.expire", "Time=="+t.I18nBot("tgbot.unlimited"))
|
info.WriteString(t.I18nBot("tgbot.messages.expire", "Time=="+t.I18nBot("tgbot.unlimited")))
|
||||||
} else {
|
} else {
|
||||||
info += t.I18nBot("tgbot.messages.expire", "Time=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
|
info.WriteString(t.I18nBot("tgbot.messages.expire", "Time=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05")))
|
||||||
}
|
}
|
||||||
info += "\r\n"
|
info.WriteString("\r\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return info
|
return info.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// getInbounds creates an inline keyboard with all inbounds.
|
// getInbounds creates an inline keyboard with all inbounds.
|
||||||
@@ -2991,12 +3065,9 @@ func (t *Tgbot) clientInfoMsg(
|
|||||||
status := t.I18nBot("tgbot.offline")
|
status := t.I18nBot("tgbot.offline")
|
||||||
isOnline := false
|
isOnline := false
|
||||||
if p.IsRunning() {
|
if p.IsRunning() {
|
||||||
for _, online := range p.GetOnlineClients() {
|
if slices.Contains(p.GetOnlineClients(), traffic.Email) {
|
||||||
if online == traffic.Email {
|
status = t.I18nBot("tgbot.online")
|
||||||
status = t.I18nBot("tgbot.online")
|
isOnline = true
|
||||||
isOnline = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3083,9 +3154,41 @@ func (t *Tgbot) searchClientIps(chatId int64, email string, messageID ...int) {
|
|||||||
ips = t.I18nBot("tgbot.noIpRecord")
|
ips = t.I18nBot("tgbot.noIpRecord")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
formattedIps := ips
|
||||||
|
if err == nil && len(ips) > 0 {
|
||||||
|
type ipWithTimestamp struct {
|
||||||
|
IP string `json:"ip"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var ipsWithTime []ipWithTimestamp
|
||||||
|
if json.Unmarshal([]byte(ips), &ipsWithTime) == nil && len(ipsWithTime) > 0 {
|
||||||
|
lines := make([]string, 0, len(ipsWithTime))
|
||||||
|
for _, item := range ipsWithTime {
|
||||||
|
if item.IP == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if item.Timestamp > 0 {
|
||||||
|
ts := time.Unix(item.Timestamp, 0).Format("2006-01-02 15:04:05")
|
||||||
|
lines = append(lines, fmt.Sprintf("%s (%s)", item.IP, ts))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lines = append(lines, item.IP)
|
||||||
|
}
|
||||||
|
if len(lines) > 0 {
|
||||||
|
formattedIps = strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var oldIps []string
|
||||||
|
if json.Unmarshal([]byte(ips), &oldIps) == nil && len(oldIps) > 0 {
|
||||||
|
formattedIps = strings.Join(oldIps, "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
output := ""
|
output := ""
|
||||||
output += t.I18nBot("tgbot.messages.email", "Email=="+email)
|
output += t.I18nBot("tgbot.messages.email", "Email=="+email)
|
||||||
output += t.I18nBot("tgbot.messages.ips", "IPs=="+ips)
|
output += t.I18nBot("tgbot.messages.ips", "IPs=="+formattedIps)
|
||||||
output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
|
output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
|
||||||
|
|
||||||
inlineKeyboard := tu.InlineKeyboard(
|
inlineKeyboard := tu.InlineKeyboard(
|
||||||
@@ -3203,6 +3306,27 @@ func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getCommonClientButtons returns the shared inline keyboard rows for client configuration
|
||||||
|
func (t *Tgbot) getCommonClientButtons() [][]telego.InlineKeyboardButton {
|
||||||
|
return [][]telego.InlineKeyboardButton{
|
||||||
|
tu.InlineKeyboardRow(
|
||||||
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.limitTraffic")).WithCallbackData("add_client_ch_default_traffic"),
|
||||||
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData("add_client_ch_default_exp"),
|
||||||
|
),
|
||||||
|
tu.InlineKeyboardRow(
|
||||||
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_comment")).WithCallbackData("add_client_ch_default_comment"),
|
||||||
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.ipLimit")).WithCallbackData("add_client_ch_default_ip_limit"),
|
||||||
|
),
|
||||||
|
tu.InlineKeyboardRow(
|
||||||
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitDisable")).WithCallbackData("add_client_submit_disable"),
|
||||||
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitEnable")).WithCallbackData("add_client_submit_enable"),
|
||||||
|
),
|
||||||
|
tu.InlineKeyboardRow(
|
||||||
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData("add_client_cancel"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// addClient handles the process of adding a new client to an inbound.
|
// addClient handles the process of adding a new client to an inbound.
|
||||||
func (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) {
|
func (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) {
|
||||||
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
|
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
|
||||||
@@ -3213,91 +3337,40 @@ func (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) {
|
|||||||
|
|
||||||
protocol := inbound.Protocol
|
protocol := inbound.Protocol
|
||||||
|
|
||||||
|
var protocolRows [][]telego.InlineKeyboardButton
|
||||||
switch protocol {
|
switch protocol {
|
||||||
case model.VMESS, model.VLESS:
|
case model.VMESS, model.VLESS:
|
||||||
inlineKeyboard := tu.InlineKeyboard(
|
protocolRows = [][]telego.InlineKeyboardButton{
|
||||||
tu.InlineKeyboardRow(
|
tu.InlineKeyboardRow(
|
||||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"),
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"),
|
||||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_id")).WithCallbackData("add_client_ch_default_id"),
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_id")).WithCallbackData("add_client_ch_default_id"),
|
||||||
),
|
),
|
||||||
tu.InlineKeyboardRow(
|
|
||||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.limitTraffic")).WithCallbackData("add_client_ch_default_traffic"),
|
|
||||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData("add_client_ch_default_exp"),
|
|
||||||
),
|
|
||||||
tu.InlineKeyboardRow(
|
|
||||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_comment")).WithCallbackData("add_client_ch_default_comment"),
|
|
||||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.ipLimit")).WithCallbackData("add_client_ch_default_ip_limit"),
|
|
||||||
),
|
|
||||||
tu.InlineKeyboardRow(
|
|
||||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitDisable")).WithCallbackData("add_client_submit_disable"),
|
|
||||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitEnable")).WithCallbackData("add_client_submit_enable"),
|
|
||||||
),
|
|
||||||
tu.InlineKeyboardRow(
|
|
||||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData("add_client_cancel"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if len(messageID) > 0 {
|
|
||||||
t.editMessageTgBot(chatId, messageID[0], msg, inlineKeyboard)
|
|
||||||
} else {
|
|
||||||
t.SendMsgToTgbot(chatId, msg, inlineKeyboard)
|
|
||||||
}
|
}
|
||||||
case model.Trojan:
|
case model.Trojan:
|
||||||
inlineKeyboard := tu.InlineKeyboard(
|
protocolRows = [][]telego.InlineKeyboardButton{
|
||||||
tu.InlineKeyboardRow(
|
tu.InlineKeyboardRow(
|
||||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"),
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"),
|
||||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_password")).WithCallbackData("add_client_ch_default_pass_tr"),
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_password")).WithCallbackData("add_client_ch_default_pass_tr"),
|
||||||
),
|
),
|
||||||
tu.InlineKeyboardRow(
|
|
||||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.limitTraffic")).WithCallbackData("add_client_ch_default_traffic"),
|
|
||||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData("add_client_ch_default_exp"),
|
|
||||||
),
|
|
||||||
tu.InlineKeyboardRow(
|
|
||||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_comment")).WithCallbackData("add_client_ch_default_comment"),
|
|
||||||
tu.InlineKeyboardButton("ip limit").WithCallbackData("add_client_ch_default_ip_limit"),
|
|
||||||
),
|
|
||||||
tu.InlineKeyboardRow(
|
|
||||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitDisable")).WithCallbackData("add_client_submit_disable"),
|
|
||||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitEnable")).WithCallbackData("add_client_submit_enable"),
|
|
||||||
),
|
|
||||||
tu.InlineKeyboardRow(
|
|
||||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData("add_client_cancel"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if len(messageID) > 0 {
|
|
||||||
t.editMessageTgBot(chatId, messageID[0], msg, inlineKeyboard)
|
|
||||||
} else {
|
|
||||||
t.SendMsgToTgbot(chatId, msg, inlineKeyboard)
|
|
||||||
}
|
}
|
||||||
case model.Shadowsocks:
|
case model.Shadowsocks:
|
||||||
inlineKeyboard := tu.InlineKeyboard(
|
protocolRows = [][]telego.InlineKeyboardButton{
|
||||||
tu.InlineKeyboardRow(
|
tu.InlineKeyboardRow(
|
||||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"),
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"),
|
||||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_password")).WithCallbackData("add_client_ch_default_pass_sh"),
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_password")).WithCallbackData("add_client_ch_default_pass_sh"),
|
||||||
),
|
),
|
||||||
tu.InlineKeyboardRow(
|
|
||||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.limitTraffic")).WithCallbackData("add_client_ch_default_traffic"),
|
|
||||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData("add_client_ch_default_exp"),
|
|
||||||
),
|
|
||||||
tu.InlineKeyboardRow(
|
|
||||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_comment")).WithCallbackData("add_client_ch_default_comment"),
|
|
||||||
tu.InlineKeyboardButton("ip limit").WithCallbackData("add_client_ch_default_ip_limit"),
|
|
||||||
),
|
|
||||||
tu.InlineKeyboardRow(
|
|
||||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitDisable")).WithCallbackData("add_client_submit_disable"),
|
|
||||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitEnable")).WithCallbackData("add_client_submit_enable"),
|
|
||||||
),
|
|
||||||
tu.InlineKeyboardRow(
|
|
||||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData("add_client_cancel"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(messageID) > 0 {
|
|
||||||
t.editMessageTgBot(chatId, messageID[0], msg, inlineKeyboard)
|
|
||||||
} else {
|
|
||||||
t.SendMsgToTgbot(chatId, msg, inlineKeyboard)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
commonRows := t.getCommonClientButtons()
|
||||||
|
inlineKeyboard := tu.InlineKeyboard(append(protocolRows, commonRows...)...)
|
||||||
|
|
||||||
|
if len(messageID) > 0 {
|
||||||
|
t.editMessageTgBot(chatId, messageID[0], msg, inlineKeyboard)
|
||||||
|
} else {
|
||||||
|
t.SendMsgToTgbot(chatId, msg, inlineKeyboard)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// searchInbound searches for inbounds by remark and sends the results.
|
// searchInbound searches for inbounds by remark and sends the results.
|
||||||
@@ -3329,11 +3402,11 @@ func (t *Tgbot) searchInbound(chatId int64, remark string) {
|
|||||||
t.SendMsgToTgbot(chatId, info)
|
t.SendMsgToTgbot(chatId, info)
|
||||||
|
|
||||||
if len(inbound.ClientStats) > 0 {
|
if len(inbound.ClientStats) > 0 {
|
||||||
output := ""
|
var output strings.Builder
|
||||||
for _, traffic := range inbound.ClientStats {
|
for _, traffic := range inbound.ClientStats {
|
||||||
output += t.clientInfoMsg(&traffic, true, true, true, true, true, true)
|
output.WriteString(t.clientInfoMsg(&traffic, true, true, true, true, true, true))
|
||||||
}
|
}
|
||||||
t.SendMsgToTgbot(chatId, output)
|
t.SendMsgToTgbot(chatId, output.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3563,13 +3636,17 @@ func (t *Tgbot) sendBackup(chatId int64) {
|
|||||||
logger.Error("Error in trigger a checkpoint operation: ", err)
|
logger.Error("Error in trigger a checkpoint operation: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send database backup
|
||||||
file, err := os.Open(config.GetDBPath())
|
file, err := os.Open(config.GetDBPath())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
defer file.Close()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
document := tu.Document(
|
document := tu.Document(
|
||||||
tu.ID(chatId),
|
tu.ID(chatId),
|
||||||
tu.File(file),
|
tu.File(file),
|
||||||
)
|
)
|
||||||
_, err = bot.SendDocument(context.Background(), document)
|
_, err = bot.SendDocument(ctx, document)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Error in uploading backup: ", err)
|
logger.Error("Error in uploading backup: ", err)
|
||||||
}
|
}
|
||||||
@@ -3577,13 +3654,20 @@ func (t *Tgbot) sendBackup(chatId int64) {
|
|||||||
logger.Error("Error in opening db file for backup: ", err)
|
logger.Error("Error in opening db file for backup: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Small delay between file sends
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
|
// Send config.json backup
|
||||||
file, err = os.Open(xray.GetConfigPath())
|
file, err = os.Open(xray.GetConfigPath())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
defer file.Close()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
document := tu.Document(
|
document := tu.Document(
|
||||||
tu.ID(chatId),
|
tu.ID(chatId),
|
||||||
tu.File(file),
|
tu.File(file),
|
||||||
)
|
)
|
||||||
_, err = bot.SendDocument(context.Background(), document)
|
_, err = bot.SendDocument(ctx, document)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Error in uploading config.json: ", err)
|
logger.Error("Error in uploading config.json: ", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func (s *UserService) GetFirstUser() (*model.User, error) {
|
|||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserService) CheckUser(username string, password string, twoFactorCode string) *model.User {
|
func (s *UserService) CheckUser(username string, password string, twoFactorCode string) (*model.User, error) {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
|
|
||||||
user := &model.User{}
|
user := &model.User{}
|
||||||
@@ -43,17 +43,16 @@ func (s *UserService) CheckUser(username string, password string, twoFactorCode
|
|||||||
First(user).
|
First(user).
|
||||||
Error
|
Error
|
||||||
if err == gorm.ErrRecordNotFound {
|
if err == gorm.ErrRecordNotFound {
|
||||||
return nil
|
return nil, errors.New("invalid credentials")
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
logger.Warning("check user err:", err)
|
logger.Warning("check user err:", err)
|
||||||
return nil
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If LDAP enabled and local password check fails, attempt LDAP auth
|
|
||||||
if !crypto.CheckPasswordHash(user.Password, password) {
|
if !crypto.CheckPasswordHash(user.Password, password) {
|
||||||
ldapEnabled, _ := s.settingService.GetLdapEnable()
|
ldapEnabled, _ := s.settingService.GetLdapEnable()
|
||||||
if !ldapEnabled {
|
if !ldapEnabled {
|
||||||
return nil
|
return nil, errors.New("invalid credentials")
|
||||||
}
|
}
|
||||||
|
|
||||||
host, _ := s.settingService.GetLdapHost()
|
host, _ := s.settingService.GetLdapHost()
|
||||||
@@ -77,15 +76,14 @@ func (s *UserService) CheckUser(username string, password string, twoFactorCode
|
|||||||
}
|
}
|
||||||
ok, err := ldaputil.AuthenticateUser(cfg, username, password)
|
ok, err := ldaputil.AuthenticateUser(cfg, username, password)
|
||||||
if err != nil || !ok {
|
if err != nil || !ok {
|
||||||
return nil
|
return nil, errors.New("invalid credentials")
|
||||||
}
|
}
|
||||||
// On successful LDAP auth, continue 2FA checks below
|
|
||||||
}
|
}
|
||||||
|
|
||||||
twoFactorEnable, err := s.settingService.GetTwoFactorEnable()
|
twoFactorEnable, err := s.settingService.GetTwoFactorEnable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warning("check two factor err:", err)
|
logger.Warning("check two factor err:", err)
|
||||||
return nil
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if twoFactorEnable {
|
if twoFactorEnable {
|
||||||
@@ -93,15 +91,15 @@ func (s *UserService) CheckUser(username string, password string, twoFactorCode
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warning("check two factor token err:", err)
|
logger.Warning("check two factor token err:", err)
|
||||||
return nil
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if gotp.NewDefaultTOTP(twoFactorToken).Now() != twoFactorCode {
|
if gotp.NewDefaultTOTP(twoFactorToken).Now() != twoFactorCode {
|
||||||
return nil
|
return nil, errors.New("invalid 2fa code")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return user
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserService) UpdateUser(id int, username string, password string) error {
|
func (s *UserService) UpdateUser(id int, username string, password string) error {
|
||||||
|
|||||||
@@ -525,6 +525,12 @@
|
|||||||
"accountInfo" = "معلومات الحساب"
|
"accountInfo" = "معلومات الحساب"
|
||||||
"outboundStatus" = "حالة المخرج"
|
"outboundStatus" = "حالة المخرج"
|
||||||
"sendThrough" = "أرسل من خلال"
|
"sendThrough" = "أرسل من خلال"
|
||||||
|
"test" = "اختبار"
|
||||||
|
"testResult" = "نتيجة الاختبار"
|
||||||
|
"testing" = "جاري اختبار الاتصال..."
|
||||||
|
"testSuccess" = "الاختبار ناجح"
|
||||||
|
"testFailed" = "فشل الاختبار"
|
||||||
|
"testError" = "فشل اختبار المخرج"
|
||||||
|
|
||||||
[pages.xray.balancer]
|
[pages.xray.balancer]
|
||||||
"addBalancer" = "أضف موازن تحميل"
|
"addBalancer" = "أضف موازن تحميل"
|
||||||
@@ -657,6 +663,7 @@
|
|||||||
"userSaved" = "✅ حفظت بيانات مستخدم Telegram."
|
"userSaved" = "✅ حفظت بيانات مستخدم Telegram."
|
||||||
"loginSuccess" = "✅ تسجيل الدخول للبانل تم بنجاح.\r\n"
|
"loginSuccess" = "✅ تسجيل الدخول للبانل تم بنجاح.\r\n"
|
||||||
"loginFailed" = "❗️فشل محاولة تسجيل الدخول للبانل.\r\n"
|
"loginFailed" = "❗️فشل محاولة تسجيل الدخول للبانل.\r\n"
|
||||||
|
"2faFailed" = "فشل 2FA"
|
||||||
"report" = "🕰 التقارير المجدولة: {{ .RunTime }}\r\n"
|
"report" = "🕰 التقارير المجدولة: {{ .RunTime }}\r\n"
|
||||||
"datetime" = "⏰ التاريخ والوقت: {{ .DateTime }}\r\n"
|
"datetime" = "⏰ التاريخ والوقت: {{ .DateTime }}\r\n"
|
||||||
"hostname" = "💻 السيرفر: {{ .Hostname }}\r\n"
|
"hostname" = "💻 السيرفر: {{ .Hostname }}\r\n"
|
||||||
|
|||||||
@@ -663,6 +663,7 @@
|
|||||||
"userSaved" = "✅ Telegram User saved."
|
"userSaved" = "✅ Telegram User saved."
|
||||||
"loginSuccess" = "✅ Logged in to the panel successfully.\r\n"
|
"loginSuccess" = "✅ Logged in to the panel successfully.\r\n"
|
||||||
"loginFailed" = "❗️Login attempt to the panel failed.\r\n"
|
"loginFailed" = "❗️Login attempt to the panel failed.\r\n"
|
||||||
|
"2faFailed" = "2FA Failed"
|
||||||
"report" = "🕰 Scheduled Reports: {{ .RunTime }}\r\n"
|
"report" = "🕰 Scheduled Reports: {{ .RunTime }}\r\n"
|
||||||
"datetime" = "⏰ Date&Time: {{ .DateTime }}\r\n"
|
"datetime" = "⏰ Date&Time: {{ .DateTime }}\r\n"
|
||||||
"hostname" = "💻 Host: {{ .Hostname }}\r\n"
|
"hostname" = "💻 Host: {{ .Hostname }}\r\n"
|
||||||
|
|||||||
@@ -525,6 +525,12 @@
|
|||||||
"accountInfo" = "Información de la Cuenta"
|
"accountInfo" = "Información de la Cuenta"
|
||||||
"outboundStatus" = "Estado de Salida"
|
"outboundStatus" = "Estado de Salida"
|
||||||
"sendThrough" = "Enviar a través de"
|
"sendThrough" = "Enviar a través de"
|
||||||
|
"test" = "Probar"
|
||||||
|
"testResult" = "Resultado de la prueba"
|
||||||
|
"testing" = "Probando conexión..."
|
||||||
|
"testSuccess" = "Prueba exitosa"
|
||||||
|
"testFailed" = "Prueba fallida"
|
||||||
|
"testError" = "Error al probar la salida"
|
||||||
|
|
||||||
[pages.xray.balancer]
|
[pages.xray.balancer]
|
||||||
"addBalancer" = "Agregar equilibrador"
|
"addBalancer" = "Agregar equilibrador"
|
||||||
@@ -657,6 +663,7 @@
|
|||||||
"userSaved" = "✅ Usuario de Telegram guardado."
|
"userSaved" = "✅ Usuario de Telegram guardado."
|
||||||
"loginSuccess" = "✅ Has iniciado sesión en el panel con éxito.\r\n"
|
"loginSuccess" = "✅ Has iniciado sesión en el panel con éxito.\r\n"
|
||||||
"loginFailed" = "❗️ Falló el inicio de sesión en el panel.\r\n"
|
"loginFailed" = "❗️ Falló el inicio de sesión en el panel.\r\n"
|
||||||
|
"2faFailed" = "Error de 2FA"
|
||||||
"report" = "🕰 Informes programados: {{ .RunTime }}\r\n"
|
"report" = "🕰 Informes programados: {{ .RunTime }}\r\n"
|
||||||
"datetime" = "⏰ Fecha y Hora: {{ .DateTime }}\r\n"
|
"datetime" = "⏰ Fecha y Hora: {{ .DateTime }}\r\n"
|
||||||
"hostname" = "💻 Nombre del Host: {{ .Hostname }}\r\n"
|
"hostname" = "💻 Nombre del Host: {{ .Hostname }}\r\n"
|
||||||
|
|||||||
@@ -525,6 +525,12 @@
|
|||||||
"accountInfo" = "اطلاعات حساب"
|
"accountInfo" = "اطلاعات حساب"
|
||||||
"outboundStatus" = "وضعیت خروجی"
|
"outboundStatus" = "وضعیت خروجی"
|
||||||
"sendThrough" = "ارسال با"
|
"sendThrough" = "ارسال با"
|
||||||
|
"test" = "تست"
|
||||||
|
"testResult" = "نتیجه تست"
|
||||||
|
"testing" = "در حال تست اتصال..."
|
||||||
|
"testSuccess" = "تست موفقیتآمیز"
|
||||||
|
"testFailed" = "تست ناموفق"
|
||||||
|
"testError" = "خطا در تست خروجی"
|
||||||
|
|
||||||
[pages.xray.balancer]
|
[pages.xray.balancer]
|
||||||
"addBalancer" = "افزودن بالانسر"
|
"addBalancer" = "افزودن بالانسر"
|
||||||
@@ -657,6 +663,7 @@
|
|||||||
"userSaved" = "✅ کاربر تلگرام ذخیره شد."
|
"userSaved" = "✅ کاربر تلگرام ذخیره شد."
|
||||||
"loginSuccess" = "✅ با موفقیت به پنل وارد شدید.\r\n"
|
"loginSuccess" = "✅ با موفقیت به پنل وارد شدید.\r\n"
|
||||||
"loginFailed" = "❗️ ورود به پنل ناموفقبود \r\n"
|
"loginFailed" = "❗️ ورود به پنل ناموفقبود \r\n"
|
||||||
|
"2faFailed" = "خطای 2FA"
|
||||||
"report" = "🕰 گزارشاتزمانبندیشده: {{ .RunTime }}\r\n"
|
"report" = "🕰 گزارشاتزمانبندیشده: {{ .RunTime }}\r\n"
|
||||||
"datetime" = "⏰ تاریخوزمان: {{ .DateTime }}\r\n"
|
"datetime" = "⏰ تاریخوزمان: {{ .DateTime }}\r\n"
|
||||||
"hostname" = "💻 ناممیزبان: {{ .Hostname }}\r\n"
|
"hostname" = "💻 ناممیزبان: {{ .Hostname }}\r\n"
|
||||||
|
|||||||
@@ -525,6 +525,12 @@
|
|||||||
"accountInfo" = "Informasi Akun"
|
"accountInfo" = "Informasi Akun"
|
||||||
"outboundStatus" = "Status Keluar"
|
"outboundStatus" = "Status Keluar"
|
||||||
"sendThrough" = "Kirim Melalui"
|
"sendThrough" = "Kirim Melalui"
|
||||||
|
"test" = "Tes"
|
||||||
|
"testResult" = "Hasil Tes"
|
||||||
|
"testing" = "Menguji koneksi..."
|
||||||
|
"testSuccess" = "Tes berhasil"
|
||||||
|
"testFailed" = "Tes gagal"
|
||||||
|
"testError" = "Gagal menguji outbound"
|
||||||
|
|
||||||
[pages.xray.balancer]
|
[pages.xray.balancer]
|
||||||
"addBalancer" = "Tambahkan Penyeimbang"
|
"addBalancer" = "Tambahkan Penyeimbang"
|
||||||
@@ -657,6 +663,7 @@
|
|||||||
"userSaved" = "✅ Pengguna Telegram tersimpan."
|
"userSaved" = "✅ Pengguna Telegram tersimpan."
|
||||||
"loginSuccess" = "✅ Berhasil masuk ke panel.\r\n"
|
"loginSuccess" = "✅ Berhasil masuk ke panel.\r\n"
|
||||||
"loginFailed" = "❗️ Gagal masuk ke panel.\r\n"
|
"loginFailed" = "❗️ Gagal masuk ke panel.\r\n"
|
||||||
|
"2faFailed" = "2FA Gagal"
|
||||||
"report" = "🕰 Laporan Terjadwal: {{ .RunTime }}\r\n"
|
"report" = "🕰 Laporan Terjadwal: {{ .RunTime }}\r\n"
|
||||||
"datetime" = "⏰ Tanggal & Waktu: {{ .DateTime }}\r\n"
|
"datetime" = "⏰ Tanggal & Waktu: {{ .DateTime }}\r\n"
|
||||||
"hostname" = "💻 Host: {{ .Hostname }}\r\n"
|
"hostname" = "💻 Host: {{ .Hostname }}\r\n"
|
||||||
|
|||||||
@@ -525,6 +525,12 @@
|
|||||||
"accountInfo" = "アカウント情報"
|
"accountInfo" = "アカウント情報"
|
||||||
"outboundStatus" = "アウトバウンドステータス"
|
"outboundStatus" = "アウトバウンドステータス"
|
||||||
"sendThrough" = "送信経路"
|
"sendThrough" = "送信経路"
|
||||||
|
"test" = "テスト"
|
||||||
|
"testResult" = "テスト結果"
|
||||||
|
"testing" = "接続をテスト中..."
|
||||||
|
"testSuccess" = "テスト成功"
|
||||||
|
"testFailed" = "テスト失敗"
|
||||||
|
"testError" = "アウトバウンドのテストに失敗しました"
|
||||||
|
|
||||||
[pages.xray.balancer]
|
[pages.xray.balancer]
|
||||||
"addBalancer" = "負荷分散追加"
|
"addBalancer" = "負荷分散追加"
|
||||||
@@ -657,6 +663,7 @@
|
|||||||
"userSaved" = "✅ Telegramユーザーが保存されました。"
|
"userSaved" = "✅ Telegramユーザーが保存されました。"
|
||||||
"loginSuccess" = "✅ パネルに正常にログインしました。\r\n"
|
"loginSuccess" = "✅ パネルに正常にログインしました。\r\n"
|
||||||
"loginFailed" = "❗️ パネルのログインに失敗しました。\r\n"
|
"loginFailed" = "❗️ パネルのログインに失敗しました。\r\n"
|
||||||
|
"2faFailed" = "2FAエラー"
|
||||||
"report" = "🕰 定期報告:{{ .RunTime }}\r\n"
|
"report" = "🕰 定期報告:{{ .RunTime }}\r\n"
|
||||||
"datetime" = "⏰ 日時:{{ .DateTime }}\r\n"
|
"datetime" = "⏰ 日時:{{ .DateTime }}\r\n"
|
||||||
"hostname" = "💻 ホスト名:{{ .Hostname }}\r\n"
|
"hostname" = "💻 ホスト名:{{ .Hostname }}\r\n"
|
||||||
|
|||||||
@@ -525,6 +525,12 @@
|
|||||||
"accountInfo" = "Informações da Conta"
|
"accountInfo" = "Informações da Conta"
|
||||||
"outboundStatus" = "Status de Saída"
|
"outboundStatus" = "Status de Saída"
|
||||||
"sendThrough" = "Enviar Através de"
|
"sendThrough" = "Enviar Através de"
|
||||||
|
"test" = "Testar"
|
||||||
|
"testResult" = "Resultado do teste"
|
||||||
|
"testing" = "Testando conexão..."
|
||||||
|
"testSuccess" = "Teste bem-sucedido"
|
||||||
|
"testFailed" = "Teste falhou"
|
||||||
|
"testError" = "Falha ao testar saída"
|
||||||
|
|
||||||
[pages.xray.balancer]
|
[pages.xray.balancer]
|
||||||
"addBalancer" = "Adicionar Balanceador"
|
"addBalancer" = "Adicionar Balanceador"
|
||||||
@@ -657,6 +663,7 @@
|
|||||||
"userSaved" = "✅ Usuário do Telegram salvo."
|
"userSaved" = "✅ Usuário do Telegram salvo."
|
||||||
"loginSuccess" = "✅ Conectado ao painel com sucesso.\r\n"
|
"loginSuccess" = "✅ Conectado ao painel com sucesso.\r\n"
|
||||||
"loginFailed" = "❗️Tentativa de login no painel falhou.\r\n"
|
"loginFailed" = "❗️Tentativa de login no painel falhou.\r\n"
|
||||||
|
"2faFailed" = "Falha no 2FA"
|
||||||
"report" = "🕰 Relatórios agendados: {{ .RunTime }}\r\n"
|
"report" = "🕰 Relatórios agendados: {{ .RunTime }}\r\n"
|
||||||
"datetime" = "⏰ Data&Hora: {{ .DateTime }}\r\n"
|
"datetime" = "⏰ Data&Hora: {{ .DateTime }}\r\n"
|
||||||
"hostname" = "💻 Host: {{ .Hostname }}\r\n"
|
"hostname" = "💻 Host: {{ .Hostname }}\r\n"
|
||||||
|
|||||||
@@ -149,7 +149,7 @@
|
|||||||
"geofileUpdateDialogDesc" = "Это обновит файл #filename#."
|
"geofileUpdateDialogDesc" = "Это обновит файл #filename#."
|
||||||
"geofilesUpdateDialogDesc" = "Это обновит все геофайлы."
|
"geofilesUpdateDialogDesc" = "Это обновит все геофайлы."
|
||||||
"geofilesUpdateAll" = "Обновить все"
|
"geofilesUpdateAll" = "Обновить все"
|
||||||
"geofileUpdatePopover" = "Геофайл успешно обновлён"
|
"geofileUpdatePopover" = "Геофайлы успешно обновлены"
|
||||||
"dontRefresh" = "Установка в процессе. Не обновляйте страницу"
|
"dontRefresh" = "Установка в процессе. Не обновляйте страницу"
|
||||||
"logs" = "Журнал"
|
"logs" = "Журнал"
|
||||||
"config" = "Конфигурация"
|
"config" = "Конфигурация"
|
||||||
@@ -525,6 +525,12 @@
|
|||||||
"accountInfo" = "Информация об учетной записи"
|
"accountInfo" = "Информация об учетной записи"
|
||||||
"outboundStatus" = "Статус исходящего подключения"
|
"outboundStatus" = "Статус исходящего подключения"
|
||||||
"sendThrough" = "Отправить через"
|
"sendThrough" = "Отправить через"
|
||||||
|
"test" = "Тест"
|
||||||
|
"testResult" = "Результат теста"
|
||||||
|
"testing" = "Тестирование соединения..."
|
||||||
|
"testSuccess" = "Тест успешен"
|
||||||
|
"testFailed" = "Тест не пройден"
|
||||||
|
"testError" = "Не удалось протестировать исходящее подключение"
|
||||||
|
|
||||||
[pages.xray.balancer]
|
[pages.xray.balancer]
|
||||||
"addBalancer" = "Создать балансировщик"
|
"addBalancer" = "Создать балансировщик"
|
||||||
@@ -657,6 +663,7 @@
|
|||||||
"userSaved" = "✅ Пользователь Telegram сохранен."
|
"userSaved" = "✅ Пользователь Telegram сохранен."
|
||||||
"loginSuccess" = "✅ Успешный вход в панель.\r\n"
|
"loginSuccess" = "✅ Успешный вход в панель.\r\n"
|
||||||
"loginFailed" = "❗️ Ошибка входа в панель.\r\n"
|
"loginFailed" = "❗️ Ошибка входа в панель.\r\n"
|
||||||
|
"2faFailed" = "Ошибка 2FA"
|
||||||
"report" = "🕰 Запланированные отчеты: {{ .RunTime }}\r\n"
|
"report" = "🕰 Запланированные отчеты: {{ .RunTime }}\r\n"
|
||||||
"datetime" = "⏰ Дата и время: {{ .DateTime }}\r\n"
|
"datetime" = "⏰ Дата и время: {{ .DateTime }}\r\n"
|
||||||
"hostname" = "💻 Имя хоста: {{ .Hostname }}\r\n"
|
"hostname" = "💻 Имя хоста: {{ .Hostname }}\r\n"
|
||||||
|
|||||||
@@ -525,6 +525,12 @@
|
|||||||
"accountInfo" = "Hesap Bilgileri"
|
"accountInfo" = "Hesap Bilgileri"
|
||||||
"outboundStatus" = "Giden Durumu"
|
"outboundStatus" = "Giden Durumu"
|
||||||
"sendThrough" = "Üzerinden Gönder"
|
"sendThrough" = "Üzerinden Gönder"
|
||||||
|
"test" = "Test"
|
||||||
|
"testResult" = "Test Sonucu"
|
||||||
|
"testing" = "Bağlantı test ediliyor..."
|
||||||
|
"testSuccess" = "Test başarılı"
|
||||||
|
"testFailed" = "Test başarısız"
|
||||||
|
"testError" = "Giden test edilemedi"
|
||||||
|
|
||||||
[pages.xray.balancer]
|
[pages.xray.balancer]
|
||||||
"addBalancer" = "Dengeleyici Ekle"
|
"addBalancer" = "Dengeleyici Ekle"
|
||||||
@@ -657,6 +663,7 @@
|
|||||||
"userSaved" = "✅ Telegram Kullanıcısı kaydedildi."
|
"userSaved" = "✅ Telegram Kullanıcısı kaydedildi."
|
||||||
"loginSuccess" = "✅ Panele başarıyla giriş yapıldı.\r\n"
|
"loginSuccess" = "✅ Panele başarıyla giriş yapıldı.\r\n"
|
||||||
"loginFailed" = "❗️Panele giriş denemesi başarısız oldu.\r\n"
|
"loginFailed" = "❗️Panele giriş denemesi başarısız oldu.\r\n"
|
||||||
|
"2faFailed" = "2FA Hatası"
|
||||||
"report" = "🕰 Planlanmış Raporlar: {{ .RunTime }}\r\n"
|
"report" = "🕰 Planlanmış Raporlar: {{ .RunTime }}\r\n"
|
||||||
"datetime" = "⏰ Tarih&Zaman: {{ .DateTime }}\r\n"
|
"datetime" = "⏰ Tarih&Zaman: {{ .DateTime }}\r\n"
|
||||||
"hostname" = "💻 Sunucu: {{ .Hostname }}\r\n"
|
"hostname" = "💻 Sunucu: {{ .Hostname }}\r\n"
|
||||||
|
|||||||
@@ -525,6 +525,12 @@
|
|||||||
"accountInfo" = "Інформація про обліковий запис"
|
"accountInfo" = "Інформація про обліковий запис"
|
||||||
"outboundStatus" = "Статус виходу"
|
"outboundStatus" = "Статус виходу"
|
||||||
"sendThrough" = "Надіслати через"
|
"sendThrough" = "Надіслати через"
|
||||||
|
"test" = "Тест"
|
||||||
|
"testResult" = "Результат тесту"
|
||||||
|
"testing" = "Тестування з'єднання..."
|
||||||
|
"testSuccess" = "Тест успішний"
|
||||||
|
"testFailed" = "Тест не пройдено"
|
||||||
|
"testError" = "Не вдалося протестувати вихідне з'єднання"
|
||||||
|
|
||||||
[pages.xray.balancer]
|
[pages.xray.balancer]
|
||||||
"addBalancer" = "Додати балансир"
|
"addBalancer" = "Додати балансир"
|
||||||
@@ -657,6 +663,7 @@
|
|||||||
"userSaved" = "✅ Користувача Telegram збережено."
|
"userSaved" = "✅ Користувача Telegram збережено."
|
||||||
"loginSuccess" = "✅ Успішно ввійшли в панель\r\n"
|
"loginSuccess" = "✅ Успішно ввійшли в панель\r\n"
|
||||||
"loginFailed" = "❗️ Помилка входу в панель.\r\n"
|
"loginFailed" = "❗️ Помилка входу в панель.\r\n"
|
||||||
|
"2faFailed" = "Помилка 2FA"
|
||||||
"report" = "🕰 Заплановані звіти: {{ .RunTime }}\r\n"
|
"report" = "🕰 Заплановані звіти: {{ .RunTime }}\r\n"
|
||||||
"datetime" = "⏰ Дата й час: {{ .DateTime }}\r\n"
|
"datetime" = "⏰ Дата й час: {{ .DateTime }}\r\n"
|
||||||
"hostname" = "💻 Хост: {{ .Hostname }}\r\n"
|
"hostname" = "💻 Хост: {{ .Hostname }}\r\n"
|
||||||
|
|||||||
@@ -525,6 +525,12 @@
|
|||||||
"accountInfo" = "Thông tin tài khoản"
|
"accountInfo" = "Thông tin tài khoản"
|
||||||
"outboundStatus" = "Trạng thái đầu ra"
|
"outboundStatus" = "Trạng thái đầu ra"
|
||||||
"sendThrough" = "Gửi qua"
|
"sendThrough" = "Gửi qua"
|
||||||
|
"test" = "Kiểm tra"
|
||||||
|
"testResult" = "Kết quả kiểm tra"
|
||||||
|
"testing" = "Đang kiểm tra kết nối..."
|
||||||
|
"testSuccess" = "Kiểm tra thành công"
|
||||||
|
"testFailed" = "Kiểm tra thất bại"
|
||||||
|
"testError" = "Không thể kiểm tra đầu ra"
|
||||||
|
|
||||||
[pages.xray.balancer]
|
[pages.xray.balancer]
|
||||||
"addBalancer" = "Thêm cân bằng"
|
"addBalancer" = "Thêm cân bằng"
|
||||||
@@ -657,6 +663,7 @@
|
|||||||
"userSaved" = "✅ Người dùng Telegram đã được lưu."
|
"userSaved" = "✅ Người dùng Telegram đã được lưu."
|
||||||
"loginSuccess" = "✅ Đăng nhập thành công vào bảng điều khiển.\r\n"
|
"loginSuccess" = "✅ Đăng nhập thành công vào bảng điều khiển.\r\n"
|
||||||
"loginFailed" = "❗️ Đăng nhập vào bảng điều khiển thất bại.\r\n"
|
"loginFailed" = "❗️ Đăng nhập vào bảng điều khiển thất bại.\r\n"
|
||||||
|
"2faFailed" = "Lỗi 2FA"
|
||||||
"report" = "🕰 Báo cáo định kỳ: {{ .RunTime }}\r\n"
|
"report" = "🕰 Báo cáo định kỳ: {{ .RunTime }}\r\n"
|
||||||
"datetime" = "⏰ Ngày-Giờ: {{ .DateTime }}\r\n"
|
"datetime" = "⏰ Ngày-Giờ: {{ .DateTime }}\r\n"
|
||||||
"hostname" = "💻 Tên máy chủ: {{ .Hostname }}\r\n"
|
"hostname" = "💻 Tên máy chủ: {{ .Hostname }}\r\n"
|
||||||
|
|||||||
@@ -525,6 +525,12 @@
|
|||||||
"accountInfo" = "帐户信息"
|
"accountInfo" = "帐户信息"
|
||||||
"outboundStatus" = "出站状态"
|
"outboundStatus" = "出站状态"
|
||||||
"sendThrough" = "发送通过"
|
"sendThrough" = "发送通过"
|
||||||
|
"test" = "测试"
|
||||||
|
"testResult" = "测试结果"
|
||||||
|
"testing" = "正在测试连接..."
|
||||||
|
"testSuccess" = "测试成功"
|
||||||
|
"testFailed" = "测试失败"
|
||||||
|
"testError" = "测试出站失败"
|
||||||
|
|
||||||
[pages.xray.balancer]
|
[pages.xray.balancer]
|
||||||
"addBalancer" = "添加负载均衡"
|
"addBalancer" = "添加负载均衡"
|
||||||
@@ -657,6 +663,7 @@
|
|||||||
"userSaved" = "✅ 电报用户已保存。"
|
"userSaved" = "✅ 电报用户已保存。"
|
||||||
"loginSuccess" = "✅ 成功登录到面板。\r\n"
|
"loginSuccess" = "✅ 成功登录到面板。\r\n"
|
||||||
"loginFailed" = "❗️ 面板登录失败。\r\n"
|
"loginFailed" = "❗️ 面板登录失败。\r\n"
|
||||||
|
"2faFailed" = "2FA 失败"
|
||||||
"report" = "🕰 定时报告:{{ .RunTime }}\r\n"
|
"report" = "🕰 定时报告:{{ .RunTime }}\r\n"
|
||||||
"datetime" = "⏰ 日期时间:{{ .DateTime }}\r\n"
|
"datetime" = "⏰ 日期时间:{{ .DateTime }}\r\n"
|
||||||
"hostname" = "💻 主机名:{{ .Hostname }}\r\n"
|
"hostname" = "💻 主机名:{{ .Hostname }}\r\n"
|
||||||
|
|||||||
@@ -525,6 +525,12 @@
|
|||||||
"accountInfo" = "帳戶資訊"
|
"accountInfo" = "帳戶資訊"
|
||||||
"outboundStatus" = "出站狀態"
|
"outboundStatus" = "出站狀態"
|
||||||
"sendThrough" = "傳送通過"
|
"sendThrough" = "傳送通過"
|
||||||
|
"test" = "測試"
|
||||||
|
"testResult" = "測試結果"
|
||||||
|
"testing" = "正在測試連接..."
|
||||||
|
"testSuccess" = "測試成功"
|
||||||
|
"testFailed" = "測試失敗"
|
||||||
|
"testError" = "測試出站失敗"
|
||||||
|
|
||||||
[pages.xray.balancer]
|
[pages.xray.balancer]
|
||||||
"addBalancer" = "新增負載均衡"
|
"addBalancer" = "新增負載均衡"
|
||||||
@@ -657,6 +663,7 @@
|
|||||||
"userSaved" = "✅ 電報使用者已儲存。"
|
"userSaved" = "✅ 電報使用者已儲存。"
|
||||||
"loginSuccess" = "✅ 成功登入到面板。\r\n"
|
"loginSuccess" = "✅ 成功登入到面板。\r\n"
|
||||||
"loginFailed" = "❗️ 面板登入失敗。\r\n"
|
"loginFailed" = "❗️ 面板登入失敗。\r\n"
|
||||||
|
"2faFailed" = "2FA 失敗"
|
||||||
"report" = "🕰 定時報告:{{ .RunTime }}\r\n"
|
"report" = "🕰 定時報告:{{ .RunTime }}\r\n"
|
||||||
"datetime" = "⏰ 日期時間:{{ .DateTime }}\r\n"
|
"datetime" = "⏰ 日期時間:{{ .DateTime }}\r\n"
|
||||||
"hostname" = "💻 主機名:{{ .Hostname }}\r\n"
|
"hostname" = "💻 主機名:{{ .Hostname }}\r\n"
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
engine.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedPaths([]string{basePath + "panel/api/"})))
|
engine.Use(gzip.Gzip(gzip.DefaultCompression))
|
||||||
assetsBasePath := basePath + "assets/"
|
assetsBasePath := basePath + "assets/"
|
||||||
|
|
||||||
store := cookie.NewStore(secret)
|
store := cookie.NewStore(secret)
|
||||||
@@ -490,3 +490,7 @@ func (s *Server) GetCron() *cron.Cron {
|
|||||||
func (s *Server) GetWSHub() any {
|
func (s *Server) GetWSHub() any {
|
||||||
return s.wsHub
|
return s.wsHub
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) RestartXray() error {
|
||||||
|
return s.xrayService.RestartXray(true)
|
||||||
|
}
|
||||||
|
|||||||
5
x-ui.rc
5
x-ui.rc
@@ -11,3 +11,8 @@ depend() {
|
|||||||
start_pre(){
|
start_pre(){
|
||||||
cd /usr/local/x-ui
|
cd /usr/local/x-ui
|
||||||
}
|
}
|
||||||
|
reload() {
|
||||||
|
ebegin "Reloading ${RC_SVCNAME}"
|
||||||
|
kill -USR1 $pidfile
|
||||||
|
eend $?
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ Environment="XRAY_VMESS_AEAD_FORCED=false"
|
|||||||
Type=simple
|
Type=simple
|
||||||
WorkingDirectory=/usr/lib/x-ui/
|
WorkingDirectory=/usr/lib/x-ui/
|
||||||
ExecStart=/usr/lib/x-ui/x-ui
|
ExecStart=/usr/lib/x-ui/x-ui
|
||||||
|
ExecReload=kill -USR1 $MAINPID
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=5s
|
RestartSec=5s
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ Environment="XRAY_VMESS_AEAD_FORCED=false"
|
|||||||
Type=simple
|
Type=simple
|
||||||
WorkingDirectory=/usr/local/x-ui/
|
WorkingDirectory=/usr/local/x-ui/
|
||||||
ExecStart=/usr/local/x-ui/x-ui
|
ExecStart=/usr/local/x-ui/x-ui
|
||||||
|
ExecReload=kill -USR1 $MAINPID
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=5s
|
RestartSec=5s
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ Environment="XRAY_VMESS_AEAD_FORCED=false"
|
|||||||
Type=simple
|
Type=simple
|
||||||
WorkingDirectory=/usr/local/x-ui/
|
WorkingDirectory=/usr/local/x-ui/
|
||||||
ExecStart=/usr/local/x-ui/x-ui
|
ExecStart=/usr/local/x-ui/x-ui
|
||||||
|
ExecReload=kill -USR1 $MAINPID
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=5s
|
RestartSec=5s
|
||||||
|
|
||||||
|
|||||||
84
x-ui.sh
84
x-ui.sh
@@ -317,12 +317,12 @@ check_config() {
|
|||||||
start >/dev/null 2>&1
|
start >/dev/null 2>&1
|
||||||
else
|
else
|
||||||
LOGE "IP certificate setup failed."
|
LOGE "IP certificate setup failed."
|
||||||
echo -e "${yellow}You can try again via option 18 (SSL Certificate Management).${plain}"
|
echo -e "${yellow}You can try again via option 19 (SSL Certificate Management).${plain}"
|
||||||
start >/dev/null 2>&1
|
start >/dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo -e "${yellow}Access URL: http://${server_ip}:${existing_port}${existing_webBasePath}${plain}"
|
echo -e "${yellow}Access URL: http://${server_ip}:${existing_port}${existing_webBasePath}${plain}"
|
||||||
echo -e "${yellow}For security, please configure SSL certificate using option 18 (SSL Certificate Management)${plain}"
|
echo -e "${yellow}For security, please configure SSL certificate using option 19 (SSL Certificate Management)${plain}"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -408,6 +408,16 @@ restart() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
restart_xray() {
|
||||||
|
systemctl reload x-ui
|
||||||
|
LOGI "xray-core Restart signal sent successfully, Please check the log information to confirm whether xray restarted successfully"
|
||||||
|
sleep 2
|
||||||
|
show_xray_status
|
||||||
|
if [[ $# == 0 ]]; then
|
||||||
|
before_show_menu
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
status() {
|
status() {
|
||||||
if [[ $release == "alpine" ]]; then
|
if [[ $release == "alpine" ]]; then
|
||||||
rc-service x-ui status
|
rc-service x-ui status
|
||||||
@@ -421,7 +431,7 @@ status() {
|
|||||||
|
|
||||||
enable() {
|
enable() {
|
||||||
if [[ $release == "alpine" ]]; then
|
if [[ $release == "alpine" ]]; then
|
||||||
rc-update add x-ui
|
rc-update add x-ui default
|
||||||
else
|
else
|
||||||
systemctl enable x-ui
|
systemctl enable x-ui
|
||||||
fi
|
fi
|
||||||
@@ -2002,7 +2012,7 @@ EOF
|
|||||||
cat << EOF > /etc/fail2ban/filter.d/3x-ipl.conf
|
cat << EOF > /etc/fail2ban/filter.d/3x-ipl.conf
|
||||||
[Definition]
|
[Definition]
|
||||||
datepattern = ^%%Y/%%m/%%d %%H:%%M:%%S
|
datepattern = ^%%Y/%%m/%%d %%H:%%M:%%S
|
||||||
failregex = \[LIMIT_IP\]\s*Email\s*=\s*<F-USER>.+</F-USER>\s*\|\|\s*SRC\s*=\s*<ADDR>
|
failregex = \[LIMIT_IP\]\s*Email\s*=\s*<F-USER>.+</F-USER>\s*\|\|\s*Disconnecting OLD IP\s*=\s*<ADDR>\s*\|\|\s*Timestamp\s*=\s*\d+
|
||||||
ignoreregex =
|
ignoreregex =
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
@@ -2062,11 +2072,15 @@ SSH_port_forwarding() {
|
|||||||
)
|
)
|
||||||
local server_ip=""
|
local server_ip=""
|
||||||
for ip_address in "${URL_lists[@]}"; do
|
for ip_address in "${URL_lists[@]}"; do
|
||||||
server_ip=$(curl -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]')
|
local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2>/dev/null)
|
||||||
if [[ -n "${server_ip}" ]]; then
|
local http_code=$(echo "$response" | tail -n1)
|
||||||
|
local ip_result=$(echo "$response" | head -n-1 | tr -d '[:space:]')
|
||||||
|
if [[ "${http_code}" == "200" && -n "${ip_result}" ]]; then
|
||||||
|
server_ip="${ip_result}"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}')
|
local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}')
|
||||||
local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
|
local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
|
||||||
local existing_listenIP=$(${xui_folder}/x-ui setting -getListen true | grep -Eo 'listenIP: .+' | awk '{print $2}')
|
local existing_listenIP=$(${xui_folder}/x-ui setting -getListen true | grep -Eo 'listenIP: .+' | awk '{print $2}')
|
||||||
@@ -2150,6 +2164,7 @@ show_usage() {
|
|||||||
│ ${blue}x-ui start${plain} - Start │
|
│ ${blue}x-ui start${plain} - Start │
|
||||||
│ ${blue}x-ui stop${plain} - Stop │
|
│ ${blue}x-ui stop${plain} - Stop │
|
||||||
│ ${blue}x-ui restart${plain} - Restart │
|
│ ${blue}x-ui restart${plain} - Restart │
|
||||||
|
| ${blue}x-ui restart-xray${plain} - Restart Xray │
|
||||||
│ ${blue}x-ui status${plain} - Current Status │
|
│ ${blue}x-ui status${plain} - Current Status │
|
||||||
│ ${blue}x-ui settings${plain} - Current Settings │
|
│ ${blue}x-ui settings${plain} - Current Settings │
|
||||||
│ ${blue}x-ui enable${plain} - Enable Autostart on OS Startup │
|
│ ${blue}x-ui enable${plain} - Enable Autostart on OS Startup │
|
||||||
@@ -2185,25 +2200,26 @@ show_menu() {
|
|||||||
│ ${green}11.${plain} Start │
|
│ ${green}11.${plain} Start │
|
||||||
│ ${green}12.${plain} Stop │
|
│ ${green}12.${plain} Stop │
|
||||||
│ ${green}13.${plain} Restart │
|
│ ${green}13.${plain} Restart │
|
||||||
│ ${green}14.${plain} Check Status │
|
| ${green}14.${plain} Restart Xray │
|
||||||
│ ${green}15.${plain} Logs Management │
|
│ ${green}15.${plain} Check Status │
|
||||||
|
│ ${green}16.${plain} Logs Management │
|
||||||
│────────────────────────────────────────────────│
|
│────────────────────────────────────────────────│
|
||||||
│ ${green}16.${plain} Enable Autostart │
|
│ ${green}17.${plain} Enable Autostart │
|
||||||
│ ${green}17.${plain} Disable Autostart │
|
│ ${green}18.${plain} Disable Autostart │
|
||||||
│────────────────────────────────────────────────│
|
│────────────────────────────────────────────────│
|
||||||
│ ${green}18.${plain} SSL Certificate Management │
|
│ ${green}19.${plain} SSL Certificate Management │
|
||||||
│ ${green}19.${plain} Cloudflare SSL Certificate │
|
│ ${green}20.${plain} Cloudflare SSL Certificate │
|
||||||
│ ${green}20.${plain} IP Limit Management │
|
│ ${green}21.${plain} IP Limit Management │
|
||||||
│ ${green}21.${plain} Firewall Management │
|
│ ${green}22.${plain} Firewall Management │
|
||||||
│ ${green}22.${plain} SSH Port Forwarding Management │
|
│ ${green}23.${plain} SSH Port Forwarding Management │
|
||||||
│────────────────────────────────────────────────│
|
│────────────────────────────────────────────────│
|
||||||
│ ${green}23.${plain} Enable BBR │
|
│ ${green}24.${plain} Enable BBR │
|
||||||
│ ${green}24.${plain} Update Geo Files │
|
│ ${green}25.${plain} Update Geo Files │
|
||||||
│ ${green}25.${plain} Speedtest by Ookla │
|
│ ${green}26.${plain} Speedtest by Ookla │
|
||||||
╚────────────────────────────────────────────────╝
|
╚────────────────────────────────────────────────╝
|
||||||
"
|
"
|
||||||
show_status
|
show_status
|
||||||
echo && read -rp "Please enter your selection [0-25]: " num
|
echo && read -rp "Please enter your selection [0-26]: " num
|
||||||
|
|
||||||
case "${num}" in
|
case "${num}" in
|
||||||
0)
|
0)
|
||||||
@@ -2249,43 +2265,46 @@ show_menu() {
|
|||||||
check_install && restart
|
check_install && restart
|
||||||
;;
|
;;
|
||||||
14)
|
14)
|
||||||
check_install && status
|
check_install && restart_xray
|
||||||
;;
|
;;
|
||||||
15)
|
15)
|
||||||
check_install && show_log
|
check_install && status
|
||||||
;;
|
;;
|
||||||
16)
|
16)
|
||||||
check_install && enable
|
check_install && show_log
|
||||||
;;
|
;;
|
||||||
17)
|
17)
|
||||||
check_install && disable
|
check_install && enable
|
||||||
;;
|
;;
|
||||||
18)
|
18)
|
||||||
ssl_cert_issue_main
|
check_install && disable
|
||||||
;;
|
;;
|
||||||
19)
|
19)
|
||||||
ssl_cert_issue_CF
|
ssl_cert_issue_main
|
||||||
;;
|
;;
|
||||||
20)
|
20)
|
||||||
iplimit_main
|
ssl_cert_issue_CF
|
||||||
;;
|
;;
|
||||||
21)
|
21)
|
||||||
firewall_menu
|
iplimit_main
|
||||||
;;
|
;;
|
||||||
22)
|
22)
|
||||||
SSH_port_forwarding
|
firewall_menu
|
||||||
;;
|
;;
|
||||||
23)
|
23)
|
||||||
bbr_menu
|
SSH_port_forwarding
|
||||||
;;
|
;;
|
||||||
24)
|
24)
|
||||||
update_geo
|
bbr_menu
|
||||||
;;
|
;;
|
||||||
25)
|
25)
|
||||||
|
update_geo
|
||||||
|
;;
|
||||||
|
26)
|
||||||
run_speedtest
|
run_speedtest
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
LOGE "Please enter the correct number [0-25]"
|
LOGE "Please enter the correct number [0-26]"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
@@ -2301,6 +2320,9 @@ if [[ $# > 0 ]]; then
|
|||||||
"restart")
|
"restart")
|
||||||
check_install 0 && restart 0
|
check_install 0 && restart 0
|
||||||
;;
|
;;
|
||||||
|
"restart-xray")
|
||||||
|
check_install 0 && restart_xray 0
|
||||||
|
;;
|
||||||
"status")
|
"status")
|
||||||
check_install 0 && status 0
|
check_install 0 && status 0
|
||||||
;;
|
;;
|
||||||
|
|||||||
Reference in New Issue
Block a user