mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-10-13 11:39:13 +00:00
Compare commits
14 commits
61406c809a
...
f7f54086f6
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f7f54086f6 | ||
![]() |
00baeffe74 | ||
![]() |
b578a33518 | ||
![]() |
8153e0ac05 | ||
![]() |
3b262cf180 | ||
![]() |
4c7249c451 | ||
![]() |
edd8b12988 | ||
![]() |
5e953bae45 | ||
![]() |
747af376f2 | ||
![]() |
a3ccccfe52 | ||
![]() |
3299d15f28 | ||
![]() |
ae82373457 | ||
![]() |
d65233cc2c | ||
![]() |
11dc06863e |
22 changed files with 1028 additions and 416 deletions
|
@ -37,6 +37,7 @@ func initModels() error {
|
|||
&model.InboundClientIps{},
|
||||
&xray.ClientTraffic{},
|
||||
&model.HistoryOfSeeders{},
|
||||
&model.Server{},
|
||||
}
|
||||
for _, model := range models {
|
||||
if err := db.AutoMigrate(model); err != nil {
|
||||
|
|
|
@ -119,3 +119,12 @@ type Client struct {
|
|||
CreatedAt int64 `json:"created_at,omitempty"` // Creation timestamp
|
||||
UpdatedAt int64 `json:"updated_at,omitempty"` // Last update timestamp
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Name string `json:"name" gorm:"unique;not null"`
|
||||
Address string `json:"address" gorm:"not null"`
|
||||
Port int `json:"port" gorm:"not null"`
|
||||
APIKey string `json:"apiKey" gorm:"not null"`
|
||||
Enable bool `json:"enable" gorm:"default:true"`
|
||||
}
|
||||
|
|
20
go.mod
20
go.mod
|
@ -6,7 +6,7 @@ require (
|
|||
github.com/gin-contrib/gzip v1.2.3
|
||||
github.com/gin-contrib/sessions v1.0.4
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.11
|
||||
github.com/go-ldap/ldap/v3 v3.4.12
|
||||
github.com/goccy/go-json v0.10.5
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
|
@ -15,7 +15,7 @@ require (
|
|||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/pelletier/go-toml/v2 v2.2.4
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/shirou/gopsutil/v4 v4.25.8
|
||||
github.com/shirou/gopsutil/v4 v4.25.9
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/valyala/fasthttp v1.66.0
|
||||
github.com/xlzd/gotp v0.1.0
|
||||
|
@ -24,7 +24,7 @@ require (
|
|||
golang.org/x/crypto v0.42.0
|
||||
golang.org/x/sys v0.36.0
|
||||
golang.org/x/text v0.29.0
|
||||
google.golang.org/grpc v1.75.1
|
||||
google.golang.org/grpc v1.76.0
|
||||
gorm.io/driver/sqlite v1.6.0
|
||||
gorm.io/gorm v1.31.0
|
||||
)
|
||||
|
@ -45,7 +45,7 @@ require (
|
|||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.28.0 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/gorilla/context v1.1.2 // indirect
|
||||
|
@ -68,13 +68,14 @@ require (
|
|||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pires/go-proxyproto v0.8.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||
github.com/quic-go/quic-go v0.55.0 // indirect
|
||||
github.com/refraction-networking/utls v1.8.0 // indirect
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/sagernet/sing v0.7.10 // indirect
|
||||
github.com/sagernet/sing v0.7.12 // indirect
|
||||
github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||
|
@ -86,9 +87,8 @@ require (
|
|||
github.com/valyala/fastjson v1.6.4 // indirect
|
||||
github.com/vishvananda/netlink v1.3.1 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c // indirect
|
||||
github.com/xtls/reality v0.0.0-20251005124704-8f4f0a188196 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/arch v0.21.0 // indirect
|
||||
golang.org/x/mod v0.28.0 // indirect
|
||||
|
@ -98,8 +98,8 @@ require (
|
|||
golang.org/x/tools v0.37.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251006185510-65f7160b3a87 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
|
||||
lukechampine.com/blake3 v1.4.1 // indirect
|
||||
)
|
||||
|
|
40
go.sum
40
go.sum
|
@ -2,8 +2,8 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+
|
|||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
|
@ -39,8 +39,8 @@ github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
|||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU=
|
||||
github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM=
|
||||
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
|
||||
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
|
@ -54,8 +54,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
|||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
|
@ -148,8 +148,8 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt
|
|||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
|
||||
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
|
||||
github.com/refraction-networking/utls v1.8.0 h1:L38krhiTAyj9EeiQQa2sg+hYb4qwLCqdMcpZrRfbONE=
|
||||
github.com/refraction-networking/utls v1.8.0/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
|
||||
|
@ -158,14 +158,14 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
|||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/sagernet/sing v0.7.10 h1:2yPhZFx+EkyHPH8hXNezgyRSHyGY12CboId7CtwLROw=
|
||||
github.com/sagernet/sing v0.7.10/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing v0.7.12 h1:MpMbO56crPRZTbltoj1wGk4Xj9+GiwH1wTO4s3fz1EA=
|
||||
github.com/sagernet/sing v0.7.12/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8=
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4=
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
|
||||
github.com/shirou/gopsutil/v4 v4.25.8 h1:NnAsw9lN7587WHxjJA9ryDnqhJpFH6A+wagYWTOH970=
|
||||
github.com/shirou/gopsutil/v4 v4.25.8/go.mod h1:q9QdMmfAOVIw7a+eF86P7ISEU6ka+NLgkUxlopV4RwI=
|
||||
github.com/shirou/gopsutil/v4 v4.25.9 h1:JImNpf6gCVhKgZhtaAHJ0serfFGtlfIlSC08eaKdTrU=
|
||||
github.com/shirou/gopsutil/v4 v4.25.9/go.mod h1:gxIxoC+7nQRwUl/xNhutXlD8lq+jxTgpIkEf3rADHL8=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
|
@ -200,8 +200,8 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
|
|||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
|
||||
github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
|
||||
github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c h1:LHLhQY3mKXSpTcQAkjFR4/6ar3rXjQryNeM7khK3AHU=
|
||||
github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c/go.mod h1:XxvnCCgBee4WWE0bc4E+a7wbk8gkJ/rS0vNVNtC5qp0=
|
||||
github.com/xtls/reality v0.0.0-20251005124704-8f4f0a188196 h1:jb1y+Rm6UBW/CEV0FehsKlQ/2dnLsQjyUjn3UfWwbic=
|
||||
github.com/xtls/reality v0.0.0-20251005124704-8f4f0a188196/go.mod h1:XxvnCCgBee4WWE0bc4E+a7wbk8gkJ/rS0vNVNtC5qp0=
|
||||
github.com/xtls/xray-core v1.250911.0 h1:KMN8zVurAjHFixiUoFV/jwmzYohf27dQRntjV+8LQno=
|
||||
github.com/xtls/xray-core v1.250911.0/go.mod h1:LkqA/BFVtPS2e5fRzg/bkYas9nQu4Uztlx+/fjlLM9k=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
|
@ -256,12 +256,12 @@ golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+Z
|
|||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 h1:V1jCN2HBa8sySkR5vLcCSqJSTMv093Rw9EJefhQGP7M=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
|
||||
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
||||
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251006185510-65f7160b3a87 h1:WgGZrMngVRRve7T3P5gbXdmedSmUpkf8uIUu1fg+biY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251006185510-65f7160b3a87/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
|
|
@ -140,6 +140,13 @@ config_after_install() {
|
|||
fi
|
||||
|
||||
/usr/local/x-ui/x-ui migrate
|
||||
|
||||
local existing_apiKey=$(/usr/local/x-ui/x-ui setting -show true | grep -oP 'ApiKey: \K.*')
|
||||
if [[ -z "$existing_apiKey" ]]; then
|
||||
local config_apiKey=$(gen_random_string 32)
|
||||
/usr/local/x-ui/x-ui setting -apiKey "${config_apiKey}"
|
||||
echo -e "${green}Generated random API Key: ${config_apiKey}${plain}"
|
||||
fi
|
||||
}
|
||||
|
||||
install_x-ui() {
|
||||
|
|
16
main.go
16
main.go
|
@ -240,7 +240,8 @@ func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime stri
|
|||
}
|
||||
|
||||
// updateSetting updates various panel settings including port, credentials, base path, listen IP, and two-factor authentication.
|
||||
func updateSetting(port int, username string, password string, webBasePath string, listenIP string, resetTwoFactor bool) {
|
||||
func updateSetting(port int, username string, password string, webBasePath string, listenIP string, resetTwoFactor bool, apiKey string) {
|
||||
|
||||
err := database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
fmt.Println("Database initialization failed:", err)
|
||||
|
@ -250,6 +251,15 @@ func updateSetting(port int, username string, password string, webBasePath strin
|
|||
settingService := service.SettingService{}
|
||||
userService := service.UserService{}
|
||||
|
||||
if apiKey != "" {
|
||||
err := settingService.SetAPIKey(apiKey)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to set API Key:", err)
|
||||
} else {
|
||||
fmt.Printf("API Key set successfully: %v\n", apiKey)
|
||||
}
|
||||
}
|
||||
|
||||
if port > 0 {
|
||||
err := settingService.SetPort(port)
|
||||
if err != nil {
|
||||
|
@ -402,9 +412,11 @@ func main() {
|
|||
var show bool
|
||||
var getCert bool
|
||||
var resetTwoFactor bool
|
||||
var apiKey string
|
||||
settingCmd.BoolVar(&reset, "reset", false, "Reset all settings")
|
||||
settingCmd.BoolVar(&show, "show", false, "Display current settings")
|
||||
settingCmd.IntVar(&port, "port", 0, "Set panel port number")
|
||||
settingCmd.StringVar(&apiKey, "apiKey", "", "Set API Key")
|
||||
settingCmd.StringVar(&username, "username", "", "Set login username")
|
||||
settingCmd.StringVar(&password, "password", "", "Set login password")
|
||||
settingCmd.StringVar(&webBasePath, "webBasePath", "", "Set base path for Panel")
|
||||
|
@ -454,7 +466,7 @@ func main() {
|
|||
if reset {
|
||||
resetSetting()
|
||||
} else {
|
||||
updateSetting(port, username, password, webBasePath, listenIP, resetTwoFactor)
|
||||
updateSetting(port, username, password, webBasePath, listenIP, resetTwoFactor, apiKey)
|
||||
}
|
||||
if show {
|
||||
showSetting(show)
|
||||
|
|
|
@ -162,26 +162,43 @@ func (s *SubService) getFallbackMaster(dest string, streamSettings string) (stri
|
|||
}
|
||||
|
||||
func (s *SubService) getLink(inbound *model.Inbound, email string) string {
|
||||
serverService := service.MultiServerService{}
|
||||
servers, err := serverService.GetServers()
|
||||
if err != nil {
|
||||
logger.Warning("Failed to get servers for subscription:", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
var links []string
|
||||
for _, server := range servers {
|
||||
if !server.Enable {
|
||||
continue
|
||||
}
|
||||
var link string
|
||||
switch inbound.Protocol {
|
||||
case "vmess":
|
||||
return s.genVmessLink(inbound, email)
|
||||
link = s.genVmessLink(inbound, email, server)
|
||||
case "vless":
|
||||
return s.genVlessLink(inbound, email)
|
||||
link = s.genVlessLink(inbound, email, server)
|
||||
case "trojan":
|
||||
return s.genTrojanLink(inbound, email)
|
||||
link = s.genTrojanLink(inbound, email, server)
|
||||
case "shadowsocks":
|
||||
return s.genShadowsocksLink(inbound, email)
|
||||
link = s.genShadowsocksLink(inbound, email, server)
|
||||
}
|
||||
return ""
|
||||
if link != "" {
|
||||
links = append(links, link)
|
||||
}
|
||||
}
|
||||
return strings.Join(links, "\n")
|
||||
}
|
||||
|
||||
func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||
func (s *SubService) genVmessLink(inbound *model.Inbound, email string, server *model.Server) string {
|
||||
if inbound.Protocol != model.VMESS {
|
||||
return ""
|
||||
}
|
||||
obj := map[string]any{
|
||||
"v": "2",
|
||||
"add": s.address,
|
||||
"add": server.Address,
|
||||
"port": inbound.Port,
|
||||
"type": "none",
|
||||
}
|
||||
|
@ -294,7 +311,7 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
|||
newObj[key] = value
|
||||
}
|
||||
}
|
||||
newObj["ps"] = s.genRemark(inbound, email, ep["remark"].(string))
|
||||
newObj["ps"] = s.genRemark(inbound, email, ep["remark"].(string), server.Name)
|
||||
newObj["add"] = ep["dest"].(string)
|
||||
newObj["port"] = int(ep["port"].(float64))
|
||||
|
||||
|
@ -310,14 +327,14 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
|||
return links
|
||||
}
|
||||
|
||||
obj["ps"] = s.genRemark(inbound, email, "")
|
||||
obj["ps"] = s.genRemark(inbound, email, "", server.Name)
|
||||
|
||||
jsonStr, _ := json.MarshalIndent(obj, "", " ")
|
||||
return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
|
||||
}
|
||||
|
||||
func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
address := s.address
|
||||
func (s *SubService) genVlessLink(inbound *model.Inbound, email string, server *model.Server) string {
|
||||
address := server.Address
|
||||
if inbound.Protocol != model.VLESS {
|
||||
return ""
|
||||
}
|
||||
|
@ -497,7 +514,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
|||
// Set the new query values on the URL
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
|
||||
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string), server.Name)
|
||||
|
||||
if index > 0 {
|
||||
links += "\n"
|
||||
|
@ -518,12 +535,12 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
|||
// Set the new query values on the URL
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
url.Fragment = s.genRemark(inbound, email, "")
|
||||
url.Fragment = s.genRemark(inbound, email, "", server.Name)
|
||||
return url.String()
|
||||
}
|
||||
|
||||
func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string {
|
||||
address := s.address
|
||||
func (s *SubService) genTrojanLink(inbound *model.Inbound, email string, server *model.Server) string {
|
||||
address := server.Address
|
||||
if inbound.Protocol != model.Trojan {
|
||||
return ""
|
||||
}
|
||||
|
@ -692,7 +709,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
|||
// Set the new query values on the URL
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
|
||||
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string), server.Name)
|
||||
|
||||
if index > 0 {
|
||||
links += "\n"
|
||||
|
@ -714,12 +731,12 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
|||
// Set the new query values on the URL
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
url.Fragment = s.genRemark(inbound, email, "")
|
||||
url.Fragment = s.genRemark(inbound, email, "", server.Name)
|
||||
return url.String()
|
||||
}
|
||||
|
||||
func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) string {
|
||||
address := s.address
|
||||
func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string, server *model.Server) string {
|
||||
address := server.Address
|
||||
if inbound.Protocol != model.Shadowsocks {
|
||||
return ""
|
||||
}
|
||||
|
@ -859,7 +876,7 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
|
|||
// Set the new query values on the URL
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
|
||||
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string), server.Name)
|
||||
|
||||
if index > 0 {
|
||||
links += "\n"
|
||||
|
@ -880,17 +897,18 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
|
|||
// Set the new query values on the URL
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
url.Fragment = s.genRemark(inbound, email, "")
|
||||
url.Fragment = s.genRemark(inbound, email, "", server.Name)
|
||||
return url.String()
|
||||
}
|
||||
|
||||
func (s *SubService) genRemark(inbound *model.Inbound, email string, extra string) string {
|
||||
func (s *SubService) genRemark(inbound *model.Inbound, email string, extra string, serverName string) string {
|
||||
separationChar := string(s.remarkModel[0])
|
||||
orderChars := s.remarkModel[1:]
|
||||
orders := map[byte]string{
|
||||
'i': "",
|
||||
'e': "",
|
||||
'o': "",
|
||||
's': "",
|
||||
}
|
||||
if len(email) > 0 {
|
||||
orders['e'] = email
|
||||
|
@ -901,6 +919,9 @@ func (s *SubService) genRemark(inbound *model.Inbound, email string, extra strin
|
|||
if len(extra) > 0 {
|
||||
orders['o'] = extra
|
||||
}
|
||||
if len(serverName) > 0 {
|
||||
orders['s'] = serverName
|
||||
}
|
||||
|
||||
var remark []string
|
||||
for i := 0; i < len(orderChars); i++ {
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
"strconv"
|
||||
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/session"
|
||||
|
|
89
web/controller/multi_server_controller.go
Normal file
89
web/controller/multi_server_controller.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"x-ui/database/model"
|
||||
"x-ui/web/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type MultiServerController struct {
|
||||
multiServerService service.MultiServerService
|
||||
}
|
||||
|
||||
func NewMultiServerController(g *gin.RouterGroup) *MultiServerController {
|
||||
c := &MultiServerController{}
|
||||
c.initRouter(g)
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *MultiServerController) initRouter(g *gin.RouterGroup) {
|
||||
g = g.Group("/server")
|
||||
|
||||
g.GET("/list", c.getServers)
|
||||
g.POST("/add", c.addServer)
|
||||
g.POST("/del/:id", c.delServer)
|
||||
g.POST("/update/:id", c.updateServer)
|
||||
}
|
||||
|
||||
func (c *MultiServerController) getServers(ctx *gin.Context) {
|
||||
servers, err := c.multiServerService.GetServers()
|
||||
if err != nil {
|
||||
jsonMsg(ctx, "Failed to get servers", err)
|
||||
return
|
||||
}
|
||||
jsonObj(ctx, servers, nil)
|
||||
}
|
||||
|
||||
func (c *MultiServerController) addServer(ctx *gin.Context) {
|
||||
server := &model.Server{}
|
||||
err := ctx.ShouldBind(server)
|
||||
if err != nil {
|
||||
jsonMsg(ctx, "Invalid data", err)
|
||||
return
|
||||
}
|
||||
err = c.multiServerService.AddServer(server)
|
||||
if err != nil {
|
||||
jsonMsg(ctx, "Failed to add server", err)
|
||||
return
|
||||
}
|
||||
jsonMsg(ctx, "Server added successfully", nil)
|
||||
}
|
||||
|
||||
func (c *MultiServerController) delServer(ctx *gin.Context) {
|
||||
id, err := strconv.Atoi(ctx.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(ctx, "Invalid ID", err)
|
||||
return
|
||||
}
|
||||
err = c.multiServerService.DeleteServer(id)
|
||||
if err != nil {
|
||||
jsonMsg(ctx, "Failed to delete server", err)
|
||||
return
|
||||
}
|
||||
jsonMsg(ctx, "Server deleted successfully", nil)
|
||||
}
|
||||
|
||||
func (c *MultiServerController) updateServer(ctx *gin.Context) {
|
||||
id, err := strconv.Atoi(ctx.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(ctx, "Invalid ID", err)
|
||||
return
|
||||
}
|
||||
server := &model.Server{
|
||||
Id: id,
|
||||
}
|
||||
err = ctx.ShouldBind(server)
|
||||
if err != nil {
|
||||
jsonMsg(ctx, "Invalid data", err)
|
||||
return
|
||||
}
|
||||
err = c.multiServerService.UpdateServer(server)
|
||||
if err != nil {
|
||||
jsonMsg(ctx, "Failed to update server", err)
|
||||
return
|
||||
}
|
||||
jsonMsg(ctx, "Server updated successfully", nil)
|
||||
}
|
|
@ -26,6 +26,7 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
|
|||
|
||||
g.GET("/", a.index)
|
||||
g.GET("/inbounds", a.inbounds)
|
||||
g.GET("/servers", a.servers)
|
||||
g.GET("/settings", a.settings)
|
||||
g.GET("/xray", a.xraySettings)
|
||||
|
||||
|
@ -52,3 +53,7 @@ func (a *XUIController) settings(c *gin.Context) {
|
|||
func (a *XUIController) xraySettings(c *gin.Context) {
|
||||
html(c, "xray.html", "pages.xray.title", nil)
|
||||
}
|
||||
|
||||
func (a *XUIController) servers(c *gin.Context) {
|
||||
html(c, "servers.html", "Servers", nil)
|
||||
}
|
||||
|
|
|
@ -54,6 +54,11 @@
|
|||
icon: 'user',
|
||||
title: '{{ i18n "menu.inbounds"}}'
|
||||
},
|
||||
{
|
||||
key: '{{ .base_path }}panel/servers',
|
||||
icon: 'cloud-server',
|
||||
title: 'Servers'
|
||||
},
|
||||
{
|
||||
key: '{{ .base_path }}panel/settings',
|
||||
icon: 'setting',
|
||||
|
|
165
web/html/servers.html
Normal file
165
web/html/servers.html
Normal file
|
@ -0,0 +1,165 @@
|
|||
{{template "header" .}}
|
||||
|
||||
<div id="app" class="row" v-cloak>
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Server Management</h3>
|
||||
<div class="card-tools">
|
||||
<button class="btn btn-primary" @click="showAddModal">Add Server</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Address</th>
|
||||
<th>Port</th>
|
||||
<th>Enabled</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(server, index) in servers">
|
||||
<td>{{index + 1}}</td>
|
||||
<td>{{server.name}}</td>
|
||||
<td>{{server.address}}</td>
|
||||
<td>{{server.port}}</td>
|
||||
<td>
|
||||
<span v-if="server.enable" class="badge bg-success">Yes</span>
|
||||
<span v-else class="badge bg-danger">No</span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-info btn-sm" @click="showEditModal(server)">Edit</button>
|
||||
<button class="btn btn-danger btn-sm" @click="deleteServer(server.id)">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit Modal -->
|
||||
<div class="modal fade" id="serverModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{modal.title}}</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label>Name</label>
|
||||
<input type="text" class="form-control" v-model="modal.server.name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Address (IP or Domain)</label>
|
||||
<input type="text" class="form-control" v-model="modal.server.address">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Port</label>
|
||||
<input type="number" class="form-control" v-model.number="modal.server.port">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>API Key</label>
|
||||
<input type="text" class="form-control" v-model="modal.server.apiKey">
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" v-model="modal.server.enable">
|
||||
<label class="form-check-label">Enabled</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" @click="saveServer">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const app = new Vue({
|
||||
el: '#app',
|
||||
data: {
|
||||
servers: [],
|
||||
modal: {
|
||||
title: '',
|
||||
server: {
|
||||
name: '',
|
||||
address: '',
|
||||
port: 0,
|
||||
apiKey: '',
|
||||
enable: true
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadServers() {
|
||||
axios.get('{{.base_path}}server/list')
|
||||
.then(response => {
|
||||
this.servers = response.data.obj;
|
||||
})
|
||||
.catch(error => {
|
||||
alert(error.response.data.msg);
|
||||
});
|
||||
},
|
||||
showAddModal() {
|
||||
this.modal.title = 'Add Server';
|
||||
this.modal.server = {
|
||||
name: '',
|
||||
address: '',
|
||||
port: 0,
|
||||
apiKey: '',
|
||||
enable: true
|
||||
};
|
||||
$('#serverModal').modal('show');
|
||||
},
|
||||
showEditModal(server) {
|
||||
this.modal.title = 'Edit Server';
|
||||
this.modal.server = Object.assign({}, server);
|
||||
$('#serverModal').modal('show');
|
||||
},
|
||||
saveServer() {
|
||||
let url = '{{.base_path}}server/add';
|
||||
if (this.modal.server.id) {
|
||||
url = `{{.base_path}}server/update/${this.modal.server.id}`;
|
||||
}
|
||||
axios.post(url, this.modal.server)
|
||||
.then(response => {
|
||||
alert(response.data.msg);
|
||||
$('#serverModal').modal('hide');
|
||||
this.loadServers();
|
||||
})
|
||||
.catch(error => {
|
||||
alert(error.response.data.msg);
|
||||
});
|
||||
},
|
||||
deleteServer(id) {
|
||||
if (!confirm('Are you sure you want to delete this server?')) {
|
||||
return;
|
||||
}
|
||||
axios.post(`{{.base_path}}server/del/${id}`)
|
||||
.then(response => {
|
||||
alert(response.data.msg);
|
||||
this.loadServers();
|
||||
})
|
||||
.catch(error => {
|
||||
alert(error.response.data.msg);
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadServers();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{{template "footer" .}}
|
|
@ -9,19 +9,20 @@
|
|||
<a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}'>
|
||||
<transition name="list" appear>
|
||||
<a-alert type="error" v-if="confAlerts.length>0 && loadingStates.fetched" :style="{ marginBottom: '10px' }"
|
||||
message='{{ i18n "secAlertTitle" }}'
|
||||
color="red"
|
||||
show-icon closable>
|
||||
message='{{ i18n "secAlertTitle" }}' color="red" show-icon closable>
|
||||
<template slot="description">
|
||||
<b>{{ i18n "secAlertConf" }}</b>
|
||||
<ul><li v-for="a in confAlerts">[[ a ]]</li></ul>
|
||||
<ul>
|
||||
<li v-for="a in confAlerts">[[ a ]]</li>
|
||||
</ul>
|
||||
</template>
|
||||
</a-alert>
|
||||
</transition>
|
||||
<transition name="list" appear>
|
||||
<template>
|
||||
<a-row v-if="!loadingStates.fetched">
|
||||
<a-card :style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
|
||||
<a-card
|
||||
:style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
|
||||
<a-spin tip='{{ i18n "loading" }}'></a-spin>
|
||||
</a-card>
|
||||
</a-row>
|
||||
|
@ -31,17 +32,19 @@
|
|||
<a-row :style="{ display: 'flex', flexWrap: 'wrap', alignItems: 'center' }">
|
||||
<a-col :xs="24" :sm="10" :style="{ padding: '4px' }">
|
||||
<a-space direction="horizontal">
|
||||
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.settings.save" }}</a-button>
|
||||
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button>
|
||||
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n
|
||||
"pages.settings.save" }}</a-button>
|
||||
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n
|
||||
"pages.settings.restartPanel" }}</a-button>
|
||||
</a-space>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="14">
|
||||
<template>
|
||||
<div>
|
||||
<a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200"></a-back-top>
|
||||
<a-back-top :target="() => document.getElementById('content-layout')"
|
||||
visibility-height="200"></a-back-top>
|
||||
<a-alert type="warning" :style="{ float: 'right', width: 'fit-content' }"
|
||||
message='{{ i18n "pages.settings.infoDesc" }}'
|
||||
show-icon>
|
||||
message='{{ i18n "pages.settings.infoDesc" }}' show-icon>
|
||||
</a-alert>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -132,7 +135,8 @@
|
|||
fragment: {
|
||||
packets: "tlshello",
|
||||
length: "100-200",
|
||||
interval: "10-20"
|
||||
interval: "10-20",
|
||||
maxSplit: "300-400"
|
||||
}
|
||||
},
|
||||
streamSettings: {
|
||||
|
@ -381,11 +385,11 @@
|
|||
},
|
||||
computed: {
|
||||
ldapInboundTagList: {
|
||||
get: function() {
|
||||
get: function () {
|
||||
const csv = this.allSetting.ldapInboundTags || "";
|
||||
return csv.length ? csv.split(',').map(s => s.trim()).filter(Boolean) : [];
|
||||
},
|
||||
set: function(list) {
|
||||
set: function (list) {
|
||||
this.allSetting.ldapInboundTags = Array.isArray(list) ? list.join(',') : '';
|
||||
}
|
||||
},
|
||||
|
@ -425,6 +429,16 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
fragmentMaxSplit: {
|
||||
get: function () { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).settings.fragment.maxSplit : ""; },
|
||||
set: function (v) {
|
||||
if (v != "") {
|
||||
newFragment = JSON.parse(this.allSetting.subJsonFragment);
|
||||
newFragment.settings.fragment.maxSplit = v;
|
||||
this.allSetting.subJsonFragment = JSON.stringify(newFragment);
|
||||
}
|
||||
}
|
||||
},
|
||||
noises: {
|
||||
get() {
|
||||
return this.allSetting?.subJsonNoises != "";
|
||||
|
|
|
@ -5,13 +5,10 @@
|
|||
<template #title>{{ i18n "pages.settings.subPath"}}</template>
|
||||
<template #description>{{ i18n "pages.settings.subPathDesc"}}</template>
|
||||
<template #control>
|
||||
<a-input
|
||||
type="text"
|
||||
v-model="allSetting.subJsonPath"
|
||||
<a-input type="text" v-model="allSetting.subJsonPath"
|
||||
@input="allSetting.subJsonPath = ((typeof $event === 'string' ? $event : ($event && $event.target ? $event.target.value : '')) || '').replace(/[:*]/g, '')"
|
||||
@blur="allSetting.subJsonPath = (p => { p = p || '/'; if (!p.startsWith('/')) p='/' + p; if (!p.endsWith('/')) p += '/'; return p.replace(/\/+/g,'/'); })(allSetting.subJsonPath)"
|
||||
placeholder="/json/"
|
||||
></a-input>
|
||||
placeholder="/json/"></a-input>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
<a-setting-list-item paddings="small">
|
||||
|
@ -53,6 +50,12 @@
|
|||
<a-input type="text" v-model="fragmentInterval" placeholder="10-20"></a-input>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
<a-setting-list-item paddings="small">
|
||||
<template #title>MaxSplit</template>
|
||||
<template #control>
|
||||
<a-input type="text" v-model="fragmentMaxSplit" placeholder="300-400"></a-input>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</a-list-item>
|
||||
|
@ -74,7 +77,8 @@
|
|||
<a-select :value="noise.type" :style="{ width: '100%' }"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
@change="(value) => updateNoiseType(index, value)">
|
||||
<a-select-option :value="p" :label="p" v-for="p in ['rand', 'base64', 'str', 'hex']" :key="p">
|
||||
<a-select-option :value="p" :label="p" v-for="p in ['rand', 'base64', 'str', 'hex']"
|
||||
:key="p">
|
||||
<span>[[ p ]]</span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
|
|
|
@ -3,14 +3,16 @@ package job
|
|||
import (
|
||||
"time"
|
||||
|
||||
"strings"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||
ldaputil "github.com/mhsanaei/3x-ui/v2/util/ldap"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||
"strings"
|
||||
|
||||
"strconv"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var DefaultTruthyValues = []string{"true", "1", "yes", "on"}
|
||||
|
@ -54,7 +56,6 @@ func mustGetStringOr(fn func() (string, error), fallback string) string {
|
|||
return v
|
||||
}
|
||||
|
||||
|
||||
func NewLdapSyncJob() *LdapSyncJob {
|
||||
return new(LdapSyncJob)
|
||||
}
|
||||
|
@ -170,8 +171,6 @@ func (j *LdapSyncJob) Run() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
func splitCsv(s string) []string {
|
||||
if s == "" {
|
||||
return DefaultTruthyValues
|
||||
|
@ -187,7 +186,6 @@ func splitCsv(s string) []string {
|
|||
return out
|
||||
}
|
||||
|
||||
|
||||
// buildClient creates a new client for auto-create
|
||||
func (j *LdapSyncJob) buildClient(ib *model.Inbound, email string, defGB, defExpiryDays, defLimitIP int) model.Client {
|
||||
c := model.Client{
|
||||
|
@ -310,20 +308,20 @@ func (j *LdapSyncJob) deleteClientsNotInLDAP(inboundTag string, ldapEmails map[s
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// clientsToJSON serializes an array of clients to JSON
|
||||
func (j *LdapSyncJob) clientsToJSON(clients []model.Client) string {
|
||||
b := strings.Builder{}
|
||||
b.WriteString("{\"clients\":[")
|
||||
for i, c := range clients {
|
||||
if i > 0 { b.WriteString(",") }
|
||||
if i > 0 {
|
||||
b.WriteString(",")
|
||||
}
|
||||
b.WriteString(j.clientToJSON(c))
|
||||
}
|
||||
b.WriteString("]}")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
|
||||
// ensureClientExists adds client with defaults to inbound tag if not present
|
||||
func (j *LdapSyncJob) ensureClientExists(inboundTag string, email string, defGB int, defExpiryDays int, defLimitIP int) {
|
||||
inbounds, err := j.inboundService.GetAllInbounds()
|
||||
|
@ -403,7 +401,11 @@ func (j *LdapSyncJob) clientToJSON(c model.Client) string {
|
|||
b.WriteString(c.Email)
|
||||
b.WriteString("\",")
|
||||
b.WriteString("\"enable\":")
|
||||
if c.Enable { b.WriteString("true") } else { b.WriteString("false") }
|
||||
if c.Enable {
|
||||
b.WriteString("true")
|
||||
} else {
|
||||
b.WriteString("false")
|
||||
}
|
||||
b.WriteString(",")
|
||||
b.WriteString("\"limitIp\":")
|
||||
b.WriteString(strconv.Itoa(c.LimitIP))
|
||||
|
@ -417,5 +419,3 @@ func (j *LdapSyncJob) clientToJSON(c model.Client) string {
|
|||
b.WriteString("}")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
|
||||
|
|
34
web/middleware/auth.go
Normal file
34
web/middleware/auth.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"x-ui/web/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func ApiAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
apiKey := c.GetHeader("Api-Key")
|
||||
if apiKey == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "API key is required"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
settingService := service.SettingService{}
|
||||
panelAPIKey, err := settingService.GetAPIKey()
|
||||
if err != nil || panelAPIKey == "" {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "API key not configured on the panel"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if apiKey != panelAPIKey {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid API key"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
|
@ -3,8 +3,11 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -673,6 +676,11 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
|
|||
}
|
||||
s.xrayApi.Close()
|
||||
|
||||
if err == nil {
|
||||
body, _ := json.Marshal(data)
|
||||
s.syncWithSlaves("POST", "/panel/inbound/api/addClient", bytes.NewReader(body))
|
||||
}
|
||||
|
||||
return needRestart, tx.Save(oldInbound).Error
|
||||
}
|
||||
|
||||
|
@ -761,6 +769,11 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) (bool,
|
|||
s.xrayApi.Close()
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
s.syncWithSlaves("POST", fmt.Sprintf("/panel/inbound/api/%d/delClient/%s", inboundId, clientId), nil)
|
||||
}
|
||||
|
||||
return needRestart, db.Save(oldInbound).Error
|
||||
}
|
||||
|
||||
|
@ -936,6 +949,12 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
|
|||
logger.Debug("Client old email not found")
|
||||
needRestart = true
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
body, _ := json.Marshal(data)
|
||||
s.syncWithSlaves("POST", fmt.Sprintf("/panel/inbound/api/updateClient/%s", clientId), bytes.NewReader(body))
|
||||
}
|
||||
|
||||
return needRestart, tx.Save(oldInbound).Error
|
||||
}
|
||||
|
||||
|
@ -2380,6 +2399,44 @@ func (s *InboundService) FilterAndSortClientEmails(emails []string) ([]string, [
|
|||
|
||||
return validEmails, extraEmails, nil
|
||||
}
|
||||
|
||||
func (s *InboundService) syncWithSlaves(method string, path string, body io.Reader) {
|
||||
serverService := MultiServerService{}
|
||||
servers, err := serverService.GetServers()
|
||||
if err != nil {
|
||||
logger.Warning("Failed to get servers for syncing:", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, server := range servers {
|
||||
if !server.Enable {
|
||||
continue
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("http://%s:%d%s", server.Address, server.Port, path)
|
||||
req, err := http.NewRequest(method, url, body)
|
||||
if err != nil {
|
||||
logger.Warningf("Failed to create request for server %s: %v", server.Name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Api-Key", server.APIKey)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.Warningf("Failed to send request to server %s: %v", server.Name, err)
|
||||
continue
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
logger.Warningf("Failed to sync with server %s. Status: %s, Body: %s", server.Name, resp.Status, string(bodyBytes))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *InboundService) DelInboundClientByEmail(inboundId int, email string) (bool, error) {
|
||||
oldInbound, err := s.GetInbound(inboundId)
|
||||
if err != nil {
|
||||
|
@ -2471,4 +2528,5 @@ func (s *InboundService) DelInboundClientByEmail(inboundId int, email string) (b
|
|||
}
|
||||
|
||||
return needRestart, db.Save(oldInbound).Error
|
||||
|
||||
}
|
||||
|
|
72
web/service/inbound_service_sync_test.go
Normal file
72
web/service/inbound_service_sync_test.go
Normal file
|
@ -0,0 +1,72 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestInboundServiceSync(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
// Mock server to simulate a slave
|
||||
var receivedApiKey string
|
||||
var receivedBody []byte
|
||||
mockSlave := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
receivedApiKey = r.Header.Get("Api-Key")
|
||||
receivedBody, _ = io.ReadAll(r.Body)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer mockSlave.Close()
|
||||
|
||||
// Add the mock slave to the database
|
||||
multiServerService := MultiServerService{}
|
||||
mockSlaveURL, _ := url.Parse(mockSlave.URL)
|
||||
mockSlavePort, _ := strconv.Atoi(mockSlaveURL.Port())
|
||||
slaveServer := &model.Server{
|
||||
Name: "mock-slave",
|
||||
Address: mockSlaveURL.Hostname(),
|
||||
Port: mockSlavePort,
|
||||
APIKey: "slave-api-key",
|
||||
Enable: true,
|
||||
}
|
||||
multiServerService.AddServer(slaveServer)
|
||||
|
||||
// Create a test inbound and client
|
||||
inboundService := InboundService{}
|
||||
db := database.GetDB()
|
||||
testInbound := &model.Inbound{
|
||||
UserId: 1,
|
||||
Remark: "test-inbound",
|
||||
Enable: true,
|
||||
Settings: `{"clients":[]}`,
|
||||
}
|
||||
db.Create(testInbound)
|
||||
|
||||
clientData := model.Client{
|
||||
Email: "test@example.com",
|
||||
ID: "test-id",
|
||||
}
|
||||
clientBytes, _ := json.Marshal([]model.Client{clientData})
|
||||
inboundData := &model.Inbound{
|
||||
Id: testInbound.Id,
|
||||
Settings: string(clientBytes),
|
||||
}
|
||||
|
||||
// Test AddInboundClient sync
|
||||
inboundService.AddInboundClient(inboundData)
|
||||
|
||||
assert.Equal(t, "slave-api-key", receivedApiKey)
|
||||
var receivedInbound model.Inbound
|
||||
json.Unmarshal(receivedBody, &receivedInbound)
|
||||
assert.Equal(t, 1, receivedInbound.Id)
|
||||
}
|
37
web/service/multi_server_service.go
Normal file
37
web/service/multi_server_service.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
)
|
||||
|
||||
type MultiServerService struct{}
|
||||
|
||||
func (s *MultiServerService) GetServers() ([]*model.Server, error) {
|
||||
db := database.GetDB()
|
||||
var servers []*model.Server
|
||||
err := db.Find(&servers).Error
|
||||
return servers, err
|
||||
}
|
||||
|
||||
func (s *MultiServerService) GetServer(id int) (*model.Server, error) {
|
||||
db := database.GetDB()
|
||||
var server model.Server
|
||||
err := db.First(&server, id).Error
|
||||
return &server, err
|
||||
}
|
||||
|
||||
func (s *MultiServerService) AddServer(server *model.Server) error {
|
||||
db := database.GetDB()
|
||||
return db.Create(server).Error
|
||||
}
|
||||
|
||||
func (s *MultiServerService) UpdateServer(server *model.Server) error {
|
||||
db := database.GetDB()
|
||||
return db.Save(server).Error
|
||||
}
|
||||
|
||||
func (s *MultiServerService) DeleteServer(id int) error {
|
||||
db := database.GetDB()
|
||||
return db.Delete(&model.Server{}, id).Error
|
||||
}
|
63
web/service/multi_server_service_test.go
Normal file
63
web/service/multi_server_service_test.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func setup() {
|
||||
dbPath := "test.db"
|
||||
os.Remove(dbPath)
|
||||
database.InitDB(dbPath)
|
||||
}
|
||||
|
||||
func teardown() {
|
||||
db, _ := database.GetDB().DB()
|
||||
db.Close()
|
||||
os.Remove("test.db")
|
||||
}
|
||||
|
||||
func TestMultiServerService(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
service := MultiServerService{}
|
||||
|
||||
// Test AddServer
|
||||
server := &model.Server{
|
||||
Name: "test-server",
|
||||
Address: "127.0.0.1",
|
||||
Port: 54321,
|
||||
APIKey: "test-key",
|
||||
Enable: true,
|
||||
}
|
||||
err := service.AddServer(server)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test GetServer
|
||||
retrievedServer, err := service.GetServer(server.Id)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, server.Name, retrievedServer.Name)
|
||||
|
||||
// Test GetServers
|
||||
servers, err := service.GetServers()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, servers, 1)
|
||||
|
||||
// Test UpdateServer
|
||||
retrievedServer.Name = "updated-server"
|
||||
err = service.UpdateServer(retrievedServer)
|
||||
assert.NoError(t, err)
|
||||
updatedServer, _ := service.GetServer(server.Id)
|
||||
assert.Equal(t, "updated-server", updatedServer.Name)
|
||||
|
||||
// Test DeleteServer
|
||||
err = service.DeleteServer(server.Id)
|
||||
assert.NoError(t, err)
|
||||
_, err = service.GetServer(server.Id)
|
||||
assert.Error(t, err)
|
||||
}
|
|
@ -204,6 +204,21 @@ func (s *SettingService) getSetting(key string) (*model.Setting, error) {
|
|||
return setting, nil
|
||||
}
|
||||
|
||||
func (s *SettingService) GetAPIKey() (string, error) {
|
||||
setting, err := s.getSetting("ApiKey")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if setting == nil {
|
||||
return "", nil
|
||||
}
|
||||
return setting.Value, nil
|
||||
}
|
||||
|
||||
func (s *SettingService) SetAPIKey(apiKey string) error {
|
||||
return s.saveSetting("ApiKey", apiKey)
|
||||
}
|
||||
|
||||
func (s *SettingService) saveSetting(key string, value string) error {
|
||||
setting, err := s.getSetting(key)
|
||||
db := database.GetDB()
|
||||
|
|
|
@ -99,7 +99,7 @@
|
|||
|
||||
[pages.login]
|
||||
"hello" = "Привет!"
|
||||
"title" = "Приветствие!"
|
||||
"title" = "Добро пожаловать!"
|
||||
"loginAgain" = "Сессия истекла. Войдите в систему снова"
|
||||
|
||||
[pages.login.toasts]
|
||||
|
|
Loading…
Reference in a new issue