diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index a7a8c790..00ac9169 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -27,7 +27,7 @@ jobs: - name: Docker meta id: meta - uses: docker/metadata-action@v5.5.0 + uses: docker/metadata-action@v5.5.1 with: images: ghcr.io/${{ github.repository }} diff --git a/Dockerfile b/Dockerfile index 7db1a175..5f4a36c3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,12 +28,14 @@ WORKDIR /app RUN apk add --no-cache --update \ ca-certificates \ tzdata \ - fail2ban + fail2ban \ + bash COPY --from=builder /app/build/ /app/ COPY --from=builder /app/DockerEntrypoint.sh /app/ COPY --from=builder /app/x-ui.sh /usr/bin/x-ui + # Configure fail2ban RUN rm -f /etc/fail2ban/jail.d/alpine-ssh.conf \ && cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local \ @@ -47,4 +49,5 @@ RUN chmod +x \ /usr/bin/x-ui VOLUME [ "/etc/x-ui" ] +CMD [ "./x-ui" ] ENTRYPOINT [ "/app/DockerEntrypoint.sh" ] diff --git a/README.md b/README.md index ec1e5a15..a1c902e2 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,39 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install. ## Install Custom Version -To install your desired version, add the version to the end of the installation command. e.g., ver `v2.1.2`: +To install your desired version, add the version to the end of the installation command. e.g., ver `v2.1.3`: ``` -bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.1.2 +bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.1.3 ``` + +## SSL Certificate + +
+ Click for SSL Certificate + +### Cloudflare + +The Management script has a built-in SSL certificate application for Cloudflare. To use this script to apply for a certificate, you need the following: + +- Cloudflare registered email +- Cloudflare Global API Key +- The domain name has been resolved to the current server through cloudflare + +**1:** Run the`x-ui`command on the terminal, then choose `Cloudflare SSL Certificate`. + + +### Certbot +``` +apt-get install certbot -y +certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com +certbot renew --dry-run +``` + +***Tip:*** *Certbot is also built into the Management script. You can run the `x-ui` command, then choose `SSL Certificate Management`.* + +
+ ## Manual Install & Upgrade
@@ -201,34 +229,6 @@ Supports a variety of different architectures and devices. Here are some of the
- -## SSL Certificate - -
- Click for SSL Certificate - -### Cloudflare - -The Management script has a built-in SSL certificate application for Cloudflare. To use this script to apply for a certificate, you need the following: - -- Cloudflare registered email -- Cloudflare Global API Key -- The domain name has been resolved to the current server through cloudflare - -**1:** Run the`x-ui`command on the terminal, then choose `Cloudflare SSL Certificate`. - - -### Certbot -``` -apt-get install certbot -y -certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com -certbot renew --dry-run -``` - -***Tip:*** *Certbot is also built into the Management script. You can run the `x-ui` command, then choose `SSL Certificate Management`.* - -
- ## [WARP Configuration](https://gitlab.com/fscarmen/warp)
@@ -281,13 +281,13 @@ If you want to use routing to WARP before v2.1.0 follow steps as below: 2. Select `IP Limit Management`. 3. Choose the appropriate options based on your needs. - - make sure you have access.log on your Xray Configuration + - make sure you have ./access.log on your Xray Configuration after v2.1.3 we have an option for it ```sh "log": { - "loglevel": "warning", "access": "./access.log", - "error": "./error.log" + "dnsLog": false, + "loglevel": "warning" }, ``` diff --git a/config/version b/config/version index 8f9174b4..abae0d9a 100644 --- a/config/version +++ b/config/version @@ -1 +1 @@ -2.1.2 \ No newline at end of file +2.1.3 \ No newline at end of file diff --git a/database/db.go b/database/db.go index 8bd0fb49..c75953f0 100644 --- a/database/db.go +++ b/database/db.go @@ -21,6 +21,7 @@ var db *gorm.DB var initializers = []func() error{ initUser, initInbound, + initOutbound, initSetting, initInboundClientIps, initClientTraffic, @@ -51,6 +52,10 @@ func initInbound() error { return db.AutoMigrate(&model.Inbound{}) } +func initOutbound() error { + return db.AutoMigrate(&model.OutboundTraffics{}) +} + func initSetting() error { return db.AutoMigrate(&model.Setting{}) } diff --git a/database/model/model.go b/database/model/model.go index e2d54436..32ab255f 100644 --- a/database/model/model.go +++ b/database/model/model.go @@ -44,6 +44,15 @@ type Inbound struct { Tag string `json:"tag" form:"tag" gorm:"unique"` Sniffing string `json:"sniffing" form:"sniffing"` } + +type OutboundTraffics struct { + Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` + Tag string `json:"tag" form:"tag" gorm:"unique"` + Up int64 `json:"up" form:"up" gorm:"default:0"` + Down int64 `json:"down" form:"down" gorm:"default:0"` + Total int64 `json:"total" form:"total" gorm:"default:0"` +} + type InboundClientIps struct { Id int `json:"id" gorm:"primaryKey;autoIncrement"` ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"` diff --git a/go.mod b/go.mod index d46ed1fe..722df287 100644 --- a/go.mod +++ b/go.mod @@ -7,23 +7,23 @@ require ( github.com/gin-contrib/gzip v0.0.6 github.com/gin-gonic/gin v1.9.1 github.com/goccy/go-json v0.10.2 - github.com/mymmrac/telego v0.28.0 - github.com/nicksnyder/go-i18n/v2 v2.3.0 + github.com/mymmrac/telego v0.29.1 + github.com/nicksnyder/go-i18n/v2 v2.4.0 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/pelletier/go-toml/v2 v2.1.1 github.com/robfig/cron/v3 v3.0.1 - github.com/shirou/gopsutil/v3 v3.23.12 - github.com/valyala/fasthttp v1.51.0 + github.com/shirou/gopsutil/v3 v3.24.1 + github.com/valyala/fasthttp v1.52.0 github.com/xtls/xray-core v1.8.7 go.uber.org/atomic v1.11.0 golang.org/x/text v0.14.0 - google.golang.org/grpc v1.61.0 - gorm.io/driver/sqlite v1.5.4 - gorm.io/gorm v1.25.6 + google.golang.org/grpc v1.61.1 + gorm.io/driver/sqlite v1.5.5 + gorm.io/gorm v1.25.7 ) require ( - github.com/andybalholm/brotli v1.0.6 // indirect + github.com/andybalholm/brotli v1.1.0 // indirect github.com/bytedance/sonic v1.10.2 // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/iasm v0.9.1 // indirect @@ -45,10 +45,11 @@ require ( github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/sessions v1.2.2 // indirect github.com/gorilla/websocket v1.5.1 // indirect + github.com/grbit/go-json v0.11.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.17.4 // indirect + github.com/klauspost/compress v1.17.6 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed // indirect @@ -75,6 +76,7 @@ require ( github.com/ugorji/go/codec v1.2.11 // indirect github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fastjson v1.6.4 // indirect github.com/vishvananda/netlink v1.2.1-beta.2.0.20230316163032-ced5aaba43e3 // indirect github.com/vishvananda/netns v0.0.4 // indirect github.com/xtls/reality v0.0.0-20231112171332-de1173cf2b19 // indirect @@ -82,16 +84,16 @@ require ( go.uber.org/mock v0.4.0 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect golang.org/x/arch v0.6.0 // indirect - golang.org/x/crypto v0.18.0 // indirect + golang.org/x/crypto v0.19.0 // indirect golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect golang.org/x/mod v0.14.0 // indirect - golang.org/x/net v0.20.0 // indirect - golang.org/x/sys v0.16.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.17.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.16.1 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 // indirect google.golang.org/protobuf v1.32.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gvisor.dev/gvisor v0.0.0-20231104011432-48a6d7d5bd0b // indirect diff --git a/go.sum b/go.sum index 8b6465d7..0c5f04a0 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,8 @@ github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8 github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/Calidity/gin-sessions v1.3.1 h1:nF3dCBWa7TZ4j26iYLwGRmzZy9YODhWoOS3fmi+snyE= github.com/Calidity/gin-sessions v1.3.1/go.mod h1:I0+QE6qkO50TeN/n6If6novvxHk4Isvr23U8EdvPdns= -github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= -github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= @@ -124,6 +124,8 @@ github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTj github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/grbit/go-json v0.11.0 h1:bAbyMdYrYl/OjYsSqLH99N2DyQ291mHy726Mx+sYrnc= +github.com/grbit/go-json v0.11.0/go.mod h1:IYpHsdybQ386+6g3VE6AXQ3uTGa5mquBme5/ZWmtzek= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= @@ -136,8 +138,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= -github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= +github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= @@ -175,12 +177,12 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/mymmrac/telego v0.28.0 h1:DNXaYISeZw1J9oB81vCNdskLow8gCRRUJxufqLuH3XE= -github.com/mymmrac/telego v0.28.0/go.mod h1:oRperySNzJq8dRTl24+uBF1Uy7tlQGIjid/JQtHDsZg= +github.com/mymmrac/telego v0.29.1 h1:nsNnK0mS18OL+unoDjDI6BVfafJBbT8Wtj7rCzEWoM8= +github.com/mymmrac/telego v0.29.1/go.mod h1:ZLD1+L2TQRr97NPOCoN1V2w8y9kmFov33OfZ3qT8cF4= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= -github.com/nicksnyder/go-i18n/v2 v2.3.0 h1:2NPsCsNFCVd7i+Su0xYsBrIhS3bE2XMv5gNTft2O+PQ= -github.com/nicksnyder/go-i18n/v2 v2.3.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4= +github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM= +github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4= github.com/onsi/ginkgo/v2 v2.13.2 h1:Bi2gGVkfn6gQcjNjZJVO8Gf0FHzMPf2phUei9tejVMs= github.com/onsi/ginkgo/v2 v2.13.2/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM= github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= @@ -230,8 +232,8 @@ github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJ github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U= github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= -github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shirou/gopsutil/v3 v3.24.1 h1:R3t6ondCEvmARp3wxODhXMTLC/klMa87h2PHUw5m7QI= +github.com/shirou/gopsutil/v3 v3.24.1/go.mod h1:UU7a2MSBQa+kW1uuDq8DeEBS8kmrnQwsv2b5O513rwU= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= @@ -288,8 +290,10 @@ github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= -github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0= +github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ= +github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= +github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= github.com/vishvananda/netlink v1.2.1-beta.2.0.20230316163032-ced5aaba43e3 h1:tkMT5pTye+1NlKIXETU78NXw0fyjnaNHmJyyLyzw8+U= @@ -319,8 +323,8 @@ golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc h1:ao2WRsKSzW6KuUY9IWPwWahcHCgR0s52IfwutMfEbdM= golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= @@ -338,8 +342,8 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -369,9 +373,9 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -407,14 +411,14 @@ google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 h1:gphdwh0npgs8elJ4T6J+DQJHPVF7RsuJHCfwztUb4J4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 h1:6G8oQ016D88m1xAKljMlBOOGWDZkes4kMhgGFlf8WcQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0= -google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= +google.golang.org/grpc v1.61.1 h1:kLAiWrZs7YeDM6MumDe7m3y4aM6wacLzM1Y/wiLP9XY= +google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= @@ -435,10 +439,10 @@ gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0= -gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4= -gorm.io/gorm v1.25.6 h1:V92+vVda1wEISSOMtodHVRcUIOPYa2tgQtyF+DfFx+A= -gorm.io/gorm v1.25.6/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E= +gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE= +gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= gvisor.dev/gvisor v0.0.0-20231104011432-48a6d7d5bd0b h1:yqkg3pTifuKukuWanp8spDsL4irJkHF5WI0J47hU87o= gvisor.dev/gvisor v0.0.0-20231104011432-48a6d7d5bd0b/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk= diff --git a/logger/logger.go b/logger/logger.go index a1386b05..ca047cbc 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -65,6 +65,16 @@ func Infof(format string, args ...interface{}) { addToBuffer("INFO", fmt.Sprintf(format, args...)) } +func Notice(args ...interface{}) { + logger.Notice(args...) + addToBuffer("NOTICE", fmt.Sprint(args...)) +} + +func Noticef(format string, args ...interface{}) { + logger.Noticef(format, args...) + addToBuffer("NOTICE", fmt.Sprintf(format, args...)) +} + func Warning(args ...interface{}) { logger.Warning(args...) addToBuffer("WARNING", fmt.Sprint(args...)) diff --git a/web/assets/css/custom.css b/web/assets/css/custom.css index ca7d23b1..c41bcc16 100644 --- a/web/assets/css/custom.css +++ b/web/assets/css/custom.css @@ -1050,12 +1050,17 @@ li.ant-select-dropdown-menu-item:empty:after { color: rgba(255, 255, 255, 0.25); } +.ant-input-group.ant-input-group-compact-addon:not(:first-child):not( + :last-child + ), +.ant-input-group.ant-input-group-compact-wrap:not(:first-child):not( + :last-child + ), +.ant-input-group.ant-input-group-compact + > .ant-input:not(:first-child):not(:last-child), +.ant-input-number-handler, .ant-input-number-handler-wrap { - border-radius: 0; -} - -.ant-input-number-handler { - border-radius: 0; + border-radius: 0; } .ant-input-number { @@ -1089,7 +1094,8 @@ li.ant-select-dropdown-menu-item:empty:after { > td, .ant-table-thead > tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected) - > td { + > td, +.ant-calendar-time-picker-select li:hover { background-color: rgb(232 244 242); } @@ -1121,3 +1127,11 @@ li.ant-select-dropdown-menu-item:empty:after { .ant-input-group-addon:not(:first-child):not(:last-child), .ant-input-group-wrap:not(:first-child):not(:last-child), .ant-input-group>.ant-input:not(:first-child):not(:last-child) { border-radius: 0rem 1rem 1rem 0rem; } + +.ant-tag { + margin-right: 6px; +} + +b, strong { + font-weight: 500; +} diff --git a/web/assets/js/langs.js b/web/assets/js/langs.js index 59f0696f..42fa49ff 100644 --- a/web/assets/js/langs.js +++ b/web/assets/js/langs.js @@ -29,6 +29,11 @@ const supportLangs = [ value: 'es-ES', icon: '🇪🇸', }, + { + name: 'Indonesian', + value: 'id-ID', + icon: '🇮🇩', + }, ]; function getLang() { diff --git a/web/assets/js/model/outbound.js b/web/assets/js/model/outbound.js index 42fb50a1..6a52563d 100644 --- a/web/assets/js/model/outbound.js +++ b/web/assets/js/model/outbound.js @@ -418,7 +418,7 @@ class Outbound extends CommonClass { } canEnableTls() { - if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan].includes(this.protocol)) return false; + if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(this.protocol)) return false; return ["tcp", "ws", "http", "quic", "grpc"].includes(this.stream.network); } @@ -861,13 +861,13 @@ Outbound.SocksSettings = class extends CommonClass { } static fromJson(json={}) { - servers = json.servers; + let servers = json.servers; if(ObjectUtil.isArrEmpty(servers)) servers=[{users: [{}]}]; return new Outbound.SocksSettings( servers[0].address, servers[0].port, ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].user, - ObjectUtil.isArrEmpty(servers[0].pass) ? '' : servers[0].users[0].pass, + ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].pass, ); } @@ -891,13 +891,13 @@ Outbound.HttpSettings = class extends CommonClass { } static fromJson(json={}) { - servers = json.servers; + let servers = json.servers; if(ObjectUtil.isArrEmpty(servers)) servers=[{users: [{}]}]; return new Outbound.HttpSettings( servers[0].address, servers[0].port, ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].user, - ObjectUtil.isArrEmpty(servers[0].pass) ? '' : servers[0].users[0].pass, + ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].pass, ); } @@ -914,8 +914,8 @@ Outbound.HttpSettings = class extends CommonClass { Outbound.WireguardSettings = class extends CommonClass { constructor( - mtu=1420, secretKey=Wireguard.generateKeypair().privateKey, - address=[''], workers=2, domainStrategy='ForceIPv6v4', reserved='', + mtu=1420, secretKey='', + address=[''], workers=2, domainStrategy='', reserved='', peers=[new Outbound.WireguardSettings.Peer()], kernelMode=false) { super(); this.mtu = mtu; @@ -965,7 +965,7 @@ Outbound.WireguardSettings = class extends CommonClass { }; Outbound.WireguardSettings.Peer = class extends CommonClass { - constructor(publicKey=Wireguard.generateKeypair().publicKey, psk='', allowedIPs=['0.0.0.0/0','::/0'], endpoint='', keepAlive=0) { + constructor(publicKey='', psk='', allowedIPs=['0.0.0.0/0','::/0'], endpoint='', keepAlive=0) { super(); this.publicKey = publicKey; this.psk = psk; diff --git a/web/assets/js/sw.js b/web/assets/js/sw.js new file mode 100644 index 00000000..46cf61c4 --- /dev/null +++ b/web/assets/js/sw.js @@ -0,0 +1,39 @@ +var self = this; + +var filesToCache = [ + '/' +]; + +self.addEventListener('install', function (e) { + e.waitUntil( + caches.open('3xPanel').then(function (cache) { + return cache.addAll(filesToCache); + }) + ); +}); +self.addEventListener('activate', function (event) { + event.waitUntil( + caches.keys().then(function (cacheNames) { + return Promise.all( + cacheNames.filter(function (cacheName) { + }).map(function (cacheName) { + return caches.delete(cacheName); + }) + ); + }) + ); +}); +self.addEventListener('fetch', function (event) { + event.respondWith( + caches.open('mysite-dynamic').then(function (cache) { + return cache.match(event.request).then(function (response) { + return response || fetch(event.request).then(function (response) { + cache.put(event.request, response.clone()); + return response; + }); + }); + }) + ); +}); + +console.clear(); \ No newline at end of file diff --git a/web/assets/manifest.json b/web/assets/manifest.json new file mode 100644 index 00000000..04c63c1d --- /dev/null +++ b/web/assets/manifest.json @@ -0,0 +1,41 @@ +{ + "name": "3x-UI Panel", + "short_name": "3xPanel", + "description": "3x-ui panel converted to PWA", + "start_url": "/", + "background_color": "#F4F4F4", + "display": "fullscreen", + "theme_color": "#293343", + "icons": [ + { + "src": "./assets/icons/16.png", + "sizes": "16x16", + "type": "image/png" + }, + { + "src": "./assets/icons/24.png", + "sizes": "24x24", + "type": "image/png" + }, + { + "src": "./assets/icons/32.png", + "sizes": "32x32", + "type": "image/png" + }, + { + "src": "./assets/icons/64.png", + "sizes": "64x64", + "type": "image/png" + }, + { + "src": "./assets/icons/192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "./assets/icons/512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/web/assets/persian-datepicker/persian-datepicker.min.css b/web/assets/persian-datepicker/persian-datepicker.min.css index bbbef736..9e99974a 100644 --- a/web/assets/persian-datepicker/persian-datepicker.min.css +++ b/web/assets/persian-datepicker/persian-datepicker.min.css @@ -1 +1,454 @@ -jdp-overlay{height:0;width:0}jdp-container{-moz-animation:.3s cubic-bezier(.23,1,.32,1) jdpOpenAnimation;-webkit-animation:.3s cubic-bezier(.23,1,.32,1) jdpOpenAnimation;animation:.3s cubic-bezier(.23,1,.32,1) jdpOpenAnimation;background:#fff;border-radius:4px;box-shadow:0 1px 6px rgba(0,0,0,.12),0 1px 4px rgba(0,0,0,.24);direction:rtl;display:none;max-width:307.875px;min-width:307.875px;overflow:hidden;padding:.5rem 0;position:absolute;-ms-touch-action:manipulation;touch-action:manipulation;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}jdp-container,jdp-container *,jdp-container :after,jdp-container :before{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}jdp-container .jdp-icon-minus,jdp-container .jdp-icon-plus{border:1px solid #e6e6e6;border-radius:4px;cursor:pointer;display:inline-block;flex:none;overflow:hidden;text-align:center;text-decoration:none;vertical-align:middle}jdp-container .jdp-icon-minus svg,jdp-container .jdp-icon-plus svg{height:1.5rem;padding:.25rem;vertical-align:middle;width:1.5rem}jdp-container .jdp-icon-minus.not-in-range,jdp-container .jdp-icon-plus.not-in-range{cursor:not-allowed}jdp-container .jdp-icon-minus.not-in-range svg,jdp-container .jdp-icon-plus.not-in-range svg{opacity:.3}jdp-container .jdp-months,jdp-container .jdp-years{fill:rgba(0,0,0,.9);color:rgba(0,0,0,.9);display:-webkit-inline-box;display:-webkit-flex;display:-ms-inline-flexbox;display:inline-flex;font-size:120%;margin:0 2.5%}jdp-container .jdp-months{width:50%}jdp-container .jdp-years{width:40%}jdp-container .jdp-month,jdp-container .jdp-month input,jdp-container .jdp-month select,jdp-container .jdp-time,jdp-container .jdp-time input,jdp-container .jdp-time select,jdp-container .jdp-year,jdp-container .jdp-year input,jdp-container .jdp-year select{background:#fff;border:none;border-radius:0;color:inherit;display:inline-block;font-family:inherit;font-size:inherit;font-weight:300;height:auto;line-height:inherit;margin:0;outline:0;padding:0;text-align:center;vertical-align:initial;width:100%}jdp-container .jdp-month input:active,jdp-container .jdp-month input:focus,jdp-container .jdp-month select:active,jdp-container .jdp-month select:focus,jdp-container .jdp-month:active,jdp-container .jdp-month:focus,jdp-container .jdp-time input:active,jdp-container .jdp-time input:focus,jdp-container .jdp-time select:active,jdp-container .jdp-time select:focus,jdp-container .jdp-time:active,jdp-container .jdp-time:focus,jdp-container .jdp-year input:active,jdp-container .jdp-year input:focus,jdp-container .jdp-year select:active,jdp-container .jdp-year select:focus,jdp-container .jdp-year:active,jdp-container .jdp-year:focus{outline:0}jdp-container .jdp-month input option,jdp-container .jdp-month option,jdp-container .jdp-month select option,jdp-container .jdp-time input option,jdp-container .jdp-time option,jdp-container .jdp-time select option,jdp-container .jdp-year input option,jdp-container .jdp-year option,jdp-container .jdp-year select option{font-size:95%;min-height:1.3rem;outline:0;padding:0}jdp-container .jdp-month:hover,jdp-container .jdp-time:hover,jdp-container .jdp-year:hover{filter:brightness(.9)}jdp-container .jdp-month input,jdp-container .jdp-time input,jdp-container .jdp-year input{-webkit-appearance:none;-moz-appearance:textfield;cursor:text}jdp-container .jdp-month input::-webkit-inner-spin-button,jdp-container .jdp-month input::-webkit-outer-spin-button,jdp-container .jdp-time input::-webkit-inner-spin-button,jdp-container .jdp-time input::-webkit-outer-spin-button,jdp-container .jdp-year input::-webkit-inner-spin-button,jdp-container .jdp-year input::-webkit-outer-spin-button{-webkit-appearance:none}jdp-container .jdp-month select,jdp-container .jdp-time select,jdp-container .jdp-year select{-webkit-appearance:menulist;-moz-appearance:menulist;cursor:pointer;position:relative}jdp-container .jdp-days{-ms-flex-pack:justify;display:inline-block;display:-ms-flexbox;display:flex;flex-wrap:wrap;-ms-flex-wrap:wrap;justify-content:space-around;outline:0;padding:.5rem 0 0;text-align:left;width:100%}jdp-container .jdp-day,jdp-container .jdp-day-name{background:0 0;border:1px solid transparent;color:rgba(0,0,0,.9);display:inline-block;flex-basis:14.2857143%;font-weight:400;height:31px;justify-content:center;line-height:31px;margin:2px 0;position:relative;text-align:center;width:14.2857143%}jdp-container .jdp-day-name.today,jdp-container .jdp-day.today{border-color:rgba(0,0,0,.3)}.dark jdp-container .jdp-day-name.selected,.dark jdp-container .jdp-day.selected,jdp-container .jdp-day-name.selected,jdp-container .jdp-day.selected{background-color:#008771!important;color:#fff!important;opacity:1!important}.dark jdp-container .jdp-day-name.holly-day,.dark jdp-container .jdp-day-name.last-week,.dark jdp-container .jdp-day.holly-day,.dark jdp-container .jdp-day.last-week,jdp-container .jdp-day-name.holly-day,jdp-container .jdp-day-name.last-week,jdp-container .jdp-day.holly-day,jdp-container .jdp-day.last-week{color:#f44336}.dark jdp-container .jdp-day.not-in-month,jdp-container .jdp-day.not-in-month{opacity:.4}jdp-container .jdp-day.disabled-day{cursor:not-allowed;opacity:.15}jdp-container .jdp-day:not(.disabled-day){border-radius:4px;cursor:pointer;transition:.1s linear}jdp-container .jdp-day:not(.disabled-day):hover{background:rgba(0,0,0,.1);transform:scale(1.15);z-index:1}jdp-container .jdp-day-name{background-color:rgba(0,0,0,.1);border-radius:0;cursor:default;font-size:90%;font-weight:900}jdp-container .jdp-footer{-ms-flex-pack:justify;display:inline-block;display:-ms-flexbox;display:flex;flex-wrap:nowrap;-ms-flex-wrap:nowrap;justify-content:space-between;outline:0;padding:.5rem .5rem 0;width:100%}jdp-container .jdp-btn-close,jdp-container .jdp-btn-empty,jdp-container .jdp-btn-today{background:#008771;border-radius:5px;color:#fff;cursor:pointer;display:inline-block;font-size:90%;font-weight:400;padding:.3em .6em;text-align:center}jdp-container .jdp-btn-close.disabled-btn,jdp-container .jdp-btn-empty.disabled-btn,jdp-container .jdp-btn-today.disabled-btn{cursor:not-allowed;opacity:.2}@media only screen and (max-width:481px){jdp-overlay{backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px);background-color:rgba(0,0,0,.3);display:none;height:100%;left:0;position:fixed;top:0;width:100%}jdp-container{-moz-animation:.3s cubic-bezier(.23,1,.32,1) jdpOpenAnimationMobile;-webkit-animation:.3s cubic-bezier(.23,1,.32,1) jdpOpenAnimationMobile;animation:.3s cubic-bezier(.23,1,.32,1) jdpOpenAnimationMobile;border-radius:4px 4px 0 0;bottom:0!important;left:50%!important;max-width:100%;min-width:280px;top:unset!important;transform:translateX(-50%)!important;width:100%}jdp-container .jdp-footer{margin:.5rem 0}jdp-container .jdp-btn-close,jdp-container .jdp-btn-empty,jdp-container .jdp-btn-today{font-size:100%;padding:.5em .8em}jdp-container .jdp-btn-today~.jdp-btn-empty{margin-left:auto;margin-right:1em}}jdp-container .jdp-time-container{display:flex;padding:0}jdp-container .jdp-time-container .jdp-time{flex:auto;margin:0 .5rem;position:relative}jdp-container .jdp-time-container .jdp-time select{background:rgba(0,0,0,.03);border-radius:5px;padding:.5rem 1rem .5rem 7px}jdp-container .jdp-time-container .jdp-time:after{content:":";font-size:1.5rem;height:100%;position:absolute;right:-.7rem;top:50%;transform:translateY(-50%)}jdp-container .jdp-time-container .jdp-time:first-child:after{display:none}jdp-container .jdp-time-container.jdp-only-time .jdp-time select{font-size:1.5rem;padding:.8rem 1rem .8rem 7px}jdp-container .jdp-time-container.jdp-only-time .jdp-time:after{font-size:2.3rem;position:absolute;right:-.8rem}@-webkit-keyframes jdpOpenAnimation{0%{opacity:0;transform:scale(.8)}to{opacity:1;transform:scale(1)}}@keyframes jdpOpenAnimation{0%{opacity:0;transform:scale(.8)}to{opacity:1;transform:scale(1)}}@-webkit-keyframes jdpOpenAnimationMobile{0%{bottom:-10%;opacity:0}to{bottom:0;opacity:1}}@keyframes jdpOpenAnimationMobile{0%{margin-bottom:-20%;opacity:0}to{margin-bottom:0;opacity:1}}.dark jdp-overlay{background-color:#181f2c}.dark jdp-container{background:#181f2c;border-color:#2c3950;box-shadow:0 1px 6px rgba(255,255,255,.12),0 1px 4px rgba(255,255,255,.24);color:#fff}.dark jdp-container .jdp-icon-minus,.dark jdp-container .jdp-icon-plus{border:1px solid #ccc}.dark jdp-container .jdp-months,.dark jdp-container .jdp-years{fill:rgba(255,255,255,0.9);color:rgba(255,255,255,.9)}.dark jdp-container .jdp-month,.dark jdp-container .jdp-month input,.dark jdp-container .jdp-month select,.dark jdp-container .jdp-time,.dark jdp-container .jdp-time input,.dark jdp-container .jdp-time select,.dark jdp-container .jdp-year,.dark jdp-container .jdp-year input,.dark jdp-container .jdp-year select{background:#222d42;color:#fff}.dark jdp-container .jdp-day,.dark jdp-container .jdp-day-name{border:1px solid transparent;color:rgba(255,255,255,.9)}.dark jdp-container .jdp-day-name.today,.dark jdp-container .jdp-day.today{border-color:rgba(255,255,255,.3)}.dark jdp-container .jdp-day.disabled-day{opacity:.15}.dark jdp-container .jdp-day:not(.disabled-day):hover{background:rgba(255,255,255,.1);transform:scale(1.15);z-index:1}.dark jdp-container .jdp-day-name{background-color:#222d42}.dark jdp-container .jdp-footer{background:#181f2c}.dark jdp-container .jdp-btn-close,.dark jdp-container .jdp-btn-empty,.dark jdp-container .jdp-btn-today{background:#008771;color:#fff}.dark jdp-container .jdp-btn-close.disabled-btn,.dark jdp-container .jdp-btn-empty.disabled-btn,.dark jdp-container .jdp-btn-today.disabled-btn{opacity:.2} \ No newline at end of file +jdp-overlay { + height: 0; + width: 0; +} +jdp-container { + -moz-animation: 0.3s cubic-bezier(0.23, 1, 0.32, 1) jdpOpenAnimation; + -webkit-animation: 0.3s cubic-bezier(0.23, 1, 0.32, 1) jdpOpenAnimation; + animation: 0.3s cubic-bezier(0.23, 1, 0.32, 1) jdpOpenAnimation; + background: #fff; + border-radius: 1rem; + box-shadow: 0 2px 8px rgba(0,0,0,.15); + direction: rtl; + display: none; + width: 280px; + overflow: hidden; + padding: 0.5rem 0; + position: absolute; + -ms-touch-action: manipulation; + touch-action: manipulation; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; + transform-origin: bottom; +} +jdp-container, +jdp-container *, +jdp-container :after, +jdp-container :before { + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +jdp-container .jdp-icon-minus, +jdp-container .jdp-icon-plus { + border: 1px solid rgb(232 244 242); + border-radius: 6px; + cursor: pointer; + display: flex; + flex: none; + overflow: hidden; + text-align: center; + text-decoration: none; + vertical-align: middle; + transition: all 0.2s; + width: 24px; + height: 24px; + align-items: center; + justify-content: center; +} + +jdp-container .jdp-icon-minus:hover, +jdp-container .jdp-icon-plus:hover { + background-color: rgb(232 244 242); +} + +jdp-container .jdp-icon-minus svg, +jdp-container .jdp-icon-plus svg { + height: 1.5rem; + padding: 0.25rem; + vertical-align: middle; + width: 1.5rem; +} +jdp-container .jdp-icon-minus.not-in-range, +jdp-container .jdp-icon-plus.not-in-range { + cursor: not-allowed; +} +jdp-container .jdp-icon-minus.not-in-range svg, +jdp-container .jdp-icon-plus.not-in-range svg { + opacity: 0.3; +} +jdp-container .jdp-months, +jdp-container .jdp-years { + fill: rgba(0, 0, 0, 0.9); + color: rgba(0, 0, 0, 0.9); + display: -webkit-inline-box; + display: -webkit-flex; + display: -ms-inline-flexbox; + display: inline-flex; + font-size: 120%; + margin: 0 2.5%; +} +jdp-container .jdp-months { + width: 40%; + margin-right: 1rem; +} +jdp-container .jdp-years { + width: 40%; + margin: 0.5rem 1.2rem 0.8rem 0; +} +jdp-container .jdp-month, +jdp-container .jdp-month input, +jdp-container .jdp-month select, +jdp-container .jdp-time, +jdp-container .jdp-time input, +jdp-container .jdp-time select, +jdp-container .jdp-year, +jdp-container .jdp-year input, +jdp-container .jdp-year select { + background: #fff; + border: none; + border-radius: 0; + color: inherit; + display: inline-block; + font-family: inherit; + font-size: inherit; + font-weight: 300; + height: auto; + line-height: inherit; + margin: 0; + outline: 0; + padding: 0; + text-align: center; + vertical-align: initial; + width: 100%; + font-feature-settings: "ss01"; +} +jdp-container .jdp-month input:active, +jdp-container .jdp-month input:focus, +jdp-container .jdp-month select:active, +jdp-container .jdp-month select:focus, +jdp-container .jdp-month:active, +jdp-container .jdp-month:focus, +jdp-container .jdp-time input:active, +jdp-container .jdp-time input:focus, +jdp-container .jdp-time select:active, +jdp-container .jdp-time select:focus, +jdp-container .jdp-time:active, +jdp-container .jdp-time:focus, +jdp-container .jdp-year input:active, +jdp-container .jdp-year input:focus, +jdp-container .jdp-year select:active, +jdp-container .jdp-year select:focus, +jdp-container .jdp-year:active, +jdp-container .jdp-year:focus { + outline: 0; +} +jdp-container .jdp-month input option, +jdp-container .jdp-month option, +jdp-container .jdp-month select option, +jdp-container .jdp-time input option, +jdp-container .jdp-time option, +jdp-container .jdp-time select option, +jdp-container .jdp-year input option, +jdp-container .jdp-year option, +jdp-container .jdp-year select option { + font-size: 95%; + min-height: 1.3rem; + outline: 0; + padding: 0; +} +jdp-container .jdp-month input, +jdp-container .jdp-time input, +jdp-container .jdp-year input { + -webkit-appearance: none; + -moz-appearance: textfield; + cursor: text; +} +jdp-container .jdp-month input::-webkit-inner-spin-button, +jdp-container .jdp-month input::-webkit-outer-spin-button, +jdp-container .jdp-time input::-webkit-inner-spin-button, +jdp-container .jdp-time input::-webkit-outer-spin-button, +jdp-container .jdp-year input::-webkit-inner-spin-button, +jdp-container .jdp-year input::-webkit-outer-spin-button { + -webkit-appearance: none; +} +jdp-container .jdp-month select, +jdp-container .jdp-time select, +jdp-container .jdp-year select { + -webkit-appearance: none; + -moz-appearance: none; + cursor: pointer; + appearance: none; + position: relative; +} +jdp-container .jdp-days { + -ms-flex-pack: justify; + display: inline-block; + display: -ms-flexbox; + display: flex; + flex-wrap: wrap; + -ms-flex-wrap: wrap; + justify-content: space-around; + outline: 0; + padding: 8px 12px; + text-align: left; + width: 100%; + border-top: 1px solid #e8e8e8; +} +jdp-container .jdp-day, +jdp-container .jdp-day-name { + background: 0 0; + border: 1px solid transparent; + color: rgba(0,0,0,.65); + display: block; + font-weight: 400; + height: 24px; + justify-content: center; + line-height: 22px; + margin: 2px 6px; + position: relative; + text-align: center; + width: 24px; + font-feature-settings: "ss01"; +} +jdp-container .jdp-day-name.today, +jdp-container .jdp-day.today { + border-color: #008771; + color: #008771; + font-weight: 700; +} +.dark jdp-container .jdp-day-name.selected, +.dark jdp-container .jdp-day.selected, +jdp-container .jdp-day-name.selected, +jdp-container .jdp-day.selected { + background-color: #008771 !important; + color: #fff !important; + opacity: 1 !important; +} +.dark jdp-container .jdp-day-name.holly-day, +.dark jdp-container .jdp-day-name.last-week, +.dark jdp-container .jdp-day.holly-day, +.dark jdp-container .jdp-day.last-week, +jdp-container .jdp-day-name.holly-day, +jdp-container .jdp-day-name.last-week, +jdp-container .jdp-day.holly-day, +jdp-container .jdp-day.last-week { + color: #f44336; +} +.dark jdp-container .jdp-day.not-in-month, +jdp-container .jdp-day.not-in-month { + opacity: 0.4; +} +jdp-container .jdp-day.disabled-day { + cursor: not-allowed; + opacity: 0.15; +} +jdp-container .jdp-day:not(.disabled-day) { + border-radius: 6px; + cursor: pointer; + transition: 0.1s linear; +} +jdp-container .jdp-day:not(.disabled-day):hover { + background: rgb(232 244 242); +} +jdp-container .jdp-day-name { + background-color: rgb(0 0 0 / 0%); + border-radius: 6px; + cursor: default; +} +jdp-container .jdp-footer { + -ms-flex-pack: justify; + display: inline-block; + display: -ms-flexbox; + display: flex; + flex-wrap: nowrap; + -ms-flex-wrap: nowrap; + justify-content: space-between; + outline: 0; + padding: 6px 12px 0; + width: 100%; + border-top: 1px solid #e8e8e8; +} +jdp-container .jdp-btn-close, +jdp-container .jdp-btn-empty, +jdp-container .jdp-btn-today { + background: #00877000; + border-radius: 5px; + color: #008771; + cursor: pointer; + display: inline-block; + font-size: 90%; + font-weight: 400; + padding: 0.3em 0.6em; + text-align: center; +} +jdp-container .jdp-btn-close.disabled-btn, +jdp-container .jdp-btn-empty.disabled-btn, +jdp-container .jdp-btn-today.disabled-btn { + cursor: not-allowed; + opacity: 0.2; +} +jdp-container .jdp-time-container { + display: flex; + padding: 6px 12px 12px 12px; +} +jdp-container .jdp-time-container .jdp-time { + flex: auto; + margin: 0 0.5rem; + position: relative; +} +jdp-container .jdp-time-container .jdp-time select { + border: 1px solid rgb(232 244 242); + border-radius: 6px; + appearance: none; + transition: all 0.2s; +} + +jdp-container .jdp-time-container .jdp-time select:hover { + background-color: rgb(232 244 242); +} + +jdp-container .jdp-time-container .jdp-time:after { + content: ":"; + font-size: 1.5rem; + height: 100%; + position: absolute; + right: -0.7rem; + transform: translateY(-50%); +} +jdp-container .jdp-time-container .jdp-time:first-child:after { + display: none; +} +jdp-container .jdp-time-container.jdp-only-time .jdp-time select { + font-size: 1.5rem; + padding: 0.8rem 1rem 0.8rem 7px; +} +jdp-container .jdp-time-container.jdp-only-time .jdp-time:after { + font-size: 2.3rem; + position: absolute; + right: -0.8rem; +} +@-webkit-keyframes jdpOpenAnimation { + 0% { + transform: scaleY(.8); + transform-origin: 0% 0%; + opacity: 0 + } + + to { + transform: scaleY(1); + transform-origin: 0% 0%; + opacity: 1 + } +} +@keyframes jdpOpenAnimation { + 0% { + transform: scaleY(.8); + transform-origin: 0% 0%; + opacity: 0 + } + + to { + transform: scaleY(1); + transform-origin: 0% 0%; + opacity: 1 + } +} +@-webkit-keyframes jdpOpenAnimationMobile { + 0% { + bottom: -10%; + opacity: 0; + } + to { + bottom: 0; + opacity: 1; + } +} +@keyframes jdpOpenAnimationMobile { + 0% { + margin-bottom: -20%; + opacity: 0; + } + to { + margin-bottom: 0; + opacity: 1; + } +} + +.dark jdp-container .jdp-days { + border-color: #313f5a; +} + +.dark jdp-overlay { + background-color: #181f2c; +} +.dark jdp-container { + background: #101828; + border-color: #2c3950; + box-shadow: 0 2px 8px rgba(0,0,0,.15); + color: #fff; +} +.dark jdp-container .jdp-icon-minus, +.dark jdp-container .jdp-icon-plus { + border-color: #313f5a; +} + +.dark jdp-container .jdp-icon-minus:hover, +.dark jdp-container .jdp-icon-plus:hover { + background-color: #313f5a; +} + +.dark jdp-container .jdp-months, +.dark jdp-container .jdp-years { + fill: rgba(255, 255, 255, 0.9); + color: rgba(255, 255, 255, 0.9); +} +.dark jdp-container .jdp-month, +.dark jdp-container .jdp-month input, +.dark jdp-container .jdp-month select, +.dark jdp-container .jdp-time, +.dark jdp-container .jdp-time input, +.dark jdp-container .jdp-time select, +.dark jdp-container .jdp-year, +.dark jdp-container .jdp-year input, +.dark jdp-container .jdp-year select { + background: #101828; + color: rgb(255 255 255 / 65%); +} +.dark jdp-container .jdp-day, +.dark jdp-container .jdp-day-name { + border: 1px solid transparent; + color: rgba(255, 255, 255, 0.65); +} +.dark jdp-container .jdp-day-name.today, +.dark jdp-container .jdp-day.today { + border-color: #008771; +} +.dark jdp-container .jdp-day.disabled-day { + opacity: 0.15; +} +.dark jdp-container .jdp-day:not(.disabled-day):hover { + background-color: #313f5a; + color: #fff; +} +.dark jdp-container .jdp-footer { + border-color: #313f5a; +} +.dark jdp-container .jdp-btn-close, +.dark jdp-container .jdp-btn-empty, +.dark jdp-container .jdp-btn-today { + color: rgb(255 255 255 / 65%); +} + +.dark jdp-container .jdp-btn-close:hover, +.dark jdp-container .jdp-btn-empty:hover, +.dark jdp-container .jdp-btn-today:hover { + color: rgb(255, 255, 255); +} + +.dark jdp-container .jdp-btn-close.disabled-btn, +.dark jdp-container .jdp-btn-empty.disabled-btn, +.dark jdp-container .jdp-btn-today.disabled-btn { + opacity: 0.2; +} + +.dark jdp-container .jdp-time-container .jdp-time select:hover { + background-color: #313f5a; + color: #fff; +} + +.dark jdp-container .jdp-time-container .jdp-time select { + border: 1px solid rgb(49 63 90); +} diff --git a/web/controller/xray_setting.go b/web/controller/xray_setting.go index 09e9115f..28f55b54 100644 --- a/web/controller/xray_setting.go +++ b/web/controller/xray_setting.go @@ -10,6 +10,7 @@ type XraySettingController struct { XraySettingService service.XraySettingService SettingService service.SettingService InboundService service.InboundService + OutboundService service.OutboundService XrayService service.XrayService } @@ -27,6 +28,8 @@ func (a *XraySettingController) initRouter(g *gin.RouterGroup) { g.GET("/getXrayResult", a.getXrayResult) g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig) g.POST("/warp/:action", a.warp) + g.GET("/getOutboundsTraffic", a.getOutboundsTraffic) + g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic) } func (a *XraySettingController) getXraySetting(c *gin.Context) { @@ -84,3 +87,22 @@ func (a *XraySettingController) warp(c *gin.Context) { jsonObj(c, resp, err) } + +func (a *XraySettingController) getOutboundsTraffic(c *gin.Context) { + outboundsTraffic, err := a.OutboundService.GetOutboundsTraffic() + if err != nil { + jsonMsg(c, "Error getting traffics", err) + return + } + jsonObj(c, outboundsTraffic, nil) +} + +func (a *XraySettingController) resetOutboundsTraffic(c *gin.Context) { + tag := c.PostForm("tag") + err := a.OutboundService.ResetOutboundTraffic(tag) + if err != nil { + jsonMsg(c, "Error in reset outbound traffics", err) + return + } + jsonObj(c, "", nil) +} diff --git a/web/html/common/head.html b/web/html/common/head.html index 4fa2ea8a..e20cdc24 100644 --- a/web/html/common/head.html +++ b/web/html/common/head.html @@ -9,6 +9,16 @@ + + + @@ -36,15 +44,15 @@ -
CPU: [[ cpuCoreFormat(status.cpuCores) ]]
-
Speed: [[ cpuSpeedFormat(status.cpuSpeedMhz) ]]
+
CPU: [[ cpuCoreFormat(status.cpuCores) ]]
+
Speed: [[ cpuSpeedFormat(status.cpuSpeedMhz) ]]
- {{ i18n "pages.index.memory"}}: [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]] + {{ i18n "pages.index.memory"}}: [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]]
@@ -56,7 +64,7 @@ :stroke-color="status.swap.color" :percent="status.swap.percent">
- Swap: [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]] + Swap: [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]]
@@ -64,7 +72,7 @@ :stroke-color="status.disk.color" :percent="status.disk.percent">
- {{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]] + {{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]]
@@ -75,25 +83,25 @@ - + - 3X-UI v{{ .cur_ver }} - Xray v[[ status.xray.version ]] - @panel3xui + 3X-UI: + v{{ .cur_ver }} + @Panel3xui - + - {{ i18n "menu.link" }}: - {{ i18n "pages.index.logs" }} - {{ i18n "pages.index.config" }} - {{ i18n "pages.index.backup" }} + {{ i18n "pages.index.operationHours" }}: + Xray [[ formatSecond(status.appStats.uptime) ]] + OS [[ formatSecond(status.uptime) ]] - + - {{ i18n "pages.index.xrayStatus" }}: - [[ status.xray.state ]] + {{ i18n "pages.index.xrayStatus" }}: + [[ status.xray.state ]] + An error occurred while running Xray @@ -106,137 +114,143 @@ {{ i18n "pages.index.stopXray" }} {{ i18n "pages.index.restartXray" }} - {{ i18n "pages.index.xraySwitch" }} + v[[ status.xray.version ]] - + - {{ i18n "pages.index.operationHours" }}: - Xray - [[ formatSecond(status.appStats.uptime) ]] - OS - [[ formatSecond(status.uptime) ]] + {{ i18n "menu.link" }}: + {{ i18n "pages.index.logs" }} + {{ i18n "pages.index.config" }} + {{ i18n "pages.index.backup" }} - + - {{ i18n "pages.index.systemLoad" }}: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]] + {{ i18n "pages.index.systemLoad" }}: + + [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]] - + - + - {{ i18n "usage"}}: - RAM [[ sizeFormat(status.appStats.mem) ]] - - Threads [[ status.appStats.threads ]] - + {{ i18n "usage"}}: + + RAM [[ sizeFormat(status.appStats.mem) ]] + + + Threads [[ status.appStats.threads ]] + - + - - IPv4: + + IPv4 - - - - - IPv6: + + + + + IPv6 - + - + - - TCP: [[ status.tcpCount ]] + + TCP: [[ status.tcpCount ]] - + - - UDP: [[ status.udpCount ]] + + UDP: [[ status.udpCount ]] - + - + - - [[ sizeFormat(status.netIO.up) ]]/s + + + Up: [[ sizeFormat(status.netIO.up) ]]/s - + - - [[ sizeFormat(status.netIO.down) ]]/s + + + Down: [[ sizeFormat(status.netIO.down) ]]/s - + - + - - [[ sizeFormat(status.netTraffic.sent) ]] + + - + Out: [[ sizeFormat(status.netTraffic.sent) ]] + - - [[ sizeFormat(status.netTraffic.recv) ]] + + - + In: [[ sizeFormat(status.netTraffic.recv) ]] + @@ -256,7 +270,7 @@ > @@ -440,8 +454,8 @@ loading: false, show(logs) { this.visible = true; - this.logs = logs; - this.formattedLogs = logs.length > 0 ? this.formatLogs(logs) : "No Record..."; + this.logs = logs || []; + this.formattedLogs = this.logs.length > 0 ? this.formatLogs(this.logs) : "No Record..."; }, formatLogs(logs) { let formattedLogs = ''; diff --git a/web/html/xui/settings.html b/web/html/xui/settings.html index 0acbbfec..533553c5 100644 --- a/web/html/xui/settings.html +++ b/web/html/xui/settings.html @@ -76,15 +76,15 @@ - - - + + + {{ i18n "pages.settings.save" }} {{ i18n "pages.settings.restartPanel" }} - + + + @@ -408,6 +467,41 @@ + + {{ i18n "pages.xray.balancer.addBalancer"}} + + + + + + @@ -430,6 +524,7 @@ {{template "ruleModal"}} {{template "outModal"}} {{template "reverseModal"}} +{{template "balancerModal"}} {{template "warpModal"}} +{{end}} \ No newline at end of file diff --git a/web/html/xui/xray_rule_modal.html b/web/html/xui/xray_rule_modal.html index 9ed9e06a..07cc3217 100644 --- a/web/html/xui/xray_rule_modal.html +++ b/web/html/xui/xray_rule_modal.html @@ -107,6 +107,19 @@ [[ tag ]] + + + + [[ tag ]] + + @@ -133,11 +146,12 @@ protocol: [], attrs: [], outboundTag: "", + balancerTag: "", }, inboundTags: [], outboundTags: [], users: [], - balancerTag: [], + balancerTags: [], ok() { newRule = ruleModal.getResult(); ObjectUtil.execute(ruleModal.confirm, newRule); @@ -160,6 +174,7 @@ this.rule.protocol = rule.protocol; this.rule.attrs = rule.attrs ? Object.entries(rule.attrs) : []; this.rule.outboundTag = rule.outboundTag; + this.rule.balancerTag = rule.balancerTag ? rule.balancerTag : "" } else { this.rule = { domainMatcher: "", @@ -174,6 +189,7 @@ protocol: [], attrs: [], outboundTag: "", + balancerTag: "", } } this.isEdit = isEdit; @@ -186,6 +202,10 @@ } if(app.templateSettings.reverse.portals) this.outboundTags.push(...app.templateSettings.reverse.portals.map(b => b.tag)); } + + if (app.templateSettings.routing && app.templateSettings.routing.balancers) { + this.balancerTags = app.templateSettings.routing.balancers.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag) + } }, close() { ruleModal.visible = false; @@ -211,6 +231,7 @@ rule.protocol = value.protocol; rule.attrs = Object.fromEntries(value.attrs); rule.outboundTag = value.outboundTag; + rule.balancerTag = value.balancerTag; for (const [key, value] of Object.entries(rule)) { if ( diff --git a/web/job/check_client_ip_job.go b/web/job/check_client_ip_job.go index b393e68d..51a09db8 100644 --- a/web/job/check_client_ip_job.go +++ b/web/job/check_client_ip_job.go @@ -1,7 +1,9 @@ package job import ( + "bufio" "encoding/json" + "io" "log" "os" "os/exec" @@ -23,7 +25,6 @@ type CheckClientIpJob struct { var job *CheckClientIpJob var ipFiles = []string{ xray.GetIPLimitLogPath(), - xray.GetIPLimitPrevLogPath(), xray.GetIPLimitBannedLogPath(), xray.GetIPLimitBannedPrevLogPath(), xray.GetAccessPersistentLogPath(), @@ -49,6 +50,37 @@ func (j *CheckClientIpJob) Run() { j.checkFail2BanInstalled() j.processLogFile() } + + if !j.hasLimitIp() && xray.GetAccessLogPath() == "./access.log" { + go j.clearLogTime() + } +} + +func (j *CheckClientIpJob) clearLogTime() { + for { + time.Sleep(time.Hour) + j.clearAccessLog() + } +} + +func (j *CheckClientIpJob) clearAccessLog() { + accessLogPath := xray.GetAccessLogPath() + logAccessP, err := os.OpenFile(xray.GetAccessPersistentLogPath(), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644) + j.checkError(err) + defer logAccessP.Close() + + // reopen the access log file for reading + file, err := os.Open(accessLogPath) + j.checkError(err) + defer file.Close() + + // copy access log content to persistent file + _, err = io.Copy(logAccessP, file) + j.checkError(err) + + // clean access log + err = os.Truncate(accessLogPath, 0) + j.checkError(err) } func (j *CheckClientIpJob) hasLimitIp() bool { @@ -86,30 +118,40 @@ func (j *CheckClientIpJob) checkFail2BanInstalled() { err := exec.Command(cmd, args...).Run() if err != nil { - logger.Warning("fail2ban is not installed. IP limiting may not work properly.") + logger.Error("fail2ban is not installed. IP limiting may not work properly.") } } func (j *CheckClientIpJob) processLogFile() { accessLogPath := xray.GetAccessLogPath() - if accessLogPath == "" { - logger.Warning("access.log doesn't exist in your config.json") + + if accessLogPath == "none" { + logger.Error("Access log is set to 'none' check your Xray Configs") return } - data, err := os.ReadFile(accessLogPath) - InboundClientIps := make(map[string][]string) - j.checkError(err) + if accessLogPath == "" { + logger.Error("Access log doesn't exist in your Xray Configs") + return + } - lines := strings.Split(string(data), "\n") - for _, line := range lines { - ipRegx, _ := regexp.Compile(`[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`) + file, err := os.Open(accessLogPath) + j.checkError(err) + defer file.Close() + + InboundClientIps := make(map[string][]string) + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + + ipRegx, _ := regexp.Compile(`(\d+\.\d+\.\d+\.\d+).* accepted`) emailRegx, _ := regexp.Compile(`email:.+`) - matchesIp := ipRegx.FindString(line) - if len(matchesIp) > 0 { - ip := string(matchesIp) - if ip == "127.0.0.1" || ip == "1.1.1.1" { + matches := ipRegx.FindStringSubmatch(line) + if len(matches) > 1 { + ip := matches[1] + if ip == "127.0.0.1" { continue } @@ -124,13 +166,14 @@ func (j *CheckClientIpJob) processLogFile() { continue } InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip) - } else { InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip) } } } + j.checkError(scanner.Err()) + shouldCleanLog := false for clientEmail, ips := range InboundClientIps { @@ -141,27 +184,13 @@ func (j *CheckClientIpJob) processLogFile() { } else { shouldCleanLog = j.updateInboundClientIps(inboundClientIps, clientEmail, ips) } - } // added delay before cleaning logs to reduce chance of logging IP that already has been banned time.Sleep(time.Second * 2) if shouldCleanLog { - // copy access log to persistent file - logAccessP, err := os.OpenFile(xray.GetAccessPersistentLogPath(), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644) - j.checkError(err) - input, err := os.ReadFile(accessLogPath) - j.checkError(err) - if _, err := logAccessP.Write(input); err != nil { - j.checkError(err) - } - defer logAccessP.Close() - - // clean access log - if err := os.Truncate(xray.GetAccessLogPath(), 0); err != nil { - j.checkError(err) - } + j.clearAccessLog() } } diff --git a/web/job/clear_logs_job.go b/web/job/clear_logs_job.go index 5ceb5a75..c6312006 100644 --- a/web/job/clear_logs_job.go +++ b/web/job/clear_logs_job.go @@ -15,8 +15,8 @@ func NewClearLogsJob() *ClearLogsJob { // Here Run is an interface method of the Job interface func (j *ClearLogsJob) Run() { logFiles := []string{xray.GetIPLimitLogPath(), xray.GetIPLimitBannedLogPath(), xray.GetAccessPersistentLogPath()} - logFilesPrev := []string{xray.GetIPLimitPrevLogPath(), xray.GetIPLimitBannedPrevLogPath(), xray.GetAccessPersistentPrevLogPath()} - + logFilesPrev := []string{xray.GetIPLimitBannedPrevLogPath(), xray.GetAccessPersistentPrevLogPath()} + // clear old previous logs for i := 0; i < len(logFilesPrev); i++ { if err := os.Truncate(logFilesPrev[i], 0); err != nil { @@ -26,25 +26,26 @@ func (j *ClearLogsJob) Run() { // clear log files and copy to previous logs for i := 0; i < len(logFiles); i++ { - - // copy to previous logs - logFilePrev, err := os.OpenFile(logFilesPrev[i], os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644) - if err != nil { - logger.Warning("clear logs job err:", err) + if i > 0 { + // copy to previous logs + logFilePrev, err := os.OpenFile(logFilesPrev[i-1], os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644) + if err != nil { + logger.Warning("clear logs job err:", err) + } + + logFile, err := os.ReadFile(logFiles[i]) + if err != nil { + logger.Warning("clear logs job err:", err) + } + + _, err = logFilePrev.Write(logFile) + if err != nil { + logger.Warning("clear logs job err:", err) + } + defer logFilePrev.Close() } - logFile, err := os.ReadFile(logFiles[i]) - if err != nil { - logger.Warning("clear logs job err:", err) - } - - _, err = logFilePrev.Write(logFile) - if err != nil { - logger.Warning("clear logs job err:", err) - } - defer logFilePrev.Close() - - err = os.Truncate(logFiles[i], 0) + err := os.Truncate(logFiles[i], 0) if err != nil { logger.Warning("clear logs job err:", err) } diff --git a/web/job/xray_traffic_job.go b/web/job/xray_traffic_job.go index 158930a4..c0de4428 100644 --- a/web/job/xray_traffic_job.go +++ b/web/job/xray_traffic_job.go @@ -6,8 +6,9 @@ import ( ) type XrayTrafficJob struct { - xrayService service.XrayService - inboundService service.InboundService + xrayService service.XrayService + inboundService service.InboundService + outboundService service.OutboundService } func NewXrayTrafficJob() *XrayTrafficJob { @@ -24,11 +25,15 @@ func (j *XrayTrafficJob) Run() { logger.Warning("get xray traffic failed:", err) return } - err, needRestart := j.inboundService.AddTraffic(traffics, clientTraffics) + err, needRestart0 := j.inboundService.AddTraffic(traffics, clientTraffics) if err != nil { - logger.Warning("add traffic failed:", err) + logger.Warning("add inbound traffic failed:", err) } - if needRestart { + err, needRestart1 := j.outboundService.AddTraffic(traffics, clientTraffics) + if err != nil { + logger.Warning("add outbound traffic failed:", err) + } + if needRestart0 || needRestart1 { j.xrayService.SetToNeedRestart() } diff --git a/web/service/config.json b/web/service/config.json index 82f7dddf..6cf6c3a6 100644 --- a/web/service/config.json +++ b/web/service/config.json @@ -1,7 +1,8 @@ { "log": { - "loglevel": "warning", - "error": "./error.log" + "access": "none", + "dnsLog": false, + "loglevel": "warning" }, "api": { "tag": "api", @@ -43,7 +44,9 @@ }, "system": { "statsInboundDownlink": true, - "statsInboundUplink": true + "statsInboundUplink": true, + "statsOutboundDownlink": true, + "statsOutboundUplink": true } }, "routing": { diff --git a/web/service/inbound.go b/web/service/inbound.go index f3445101..291c0dee 100644 --- a/web/service/inbound.go +++ b/web/service/inbound.go @@ -682,7 +682,7 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin return needRestart, tx.Save(oldInbound).Error } -func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) { +func (s *InboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) { var err error db := database.GetDB() tx := db.Begin() @@ -694,7 +694,7 @@ func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraff tx.Commit() } }() - err = s.addInboundTraffic(tx, inboundTraffics) + err = s.addInboundTraffic(tx, traffics) if err != nil { return err, false } diff --git a/web/service/outbound.go b/web/service/outbound.go new file mode 100644 index 00000000..244d7a14 --- /dev/null +++ b/web/service/outbound.go @@ -0,0 +1,102 @@ +package service + +import ( + "x-ui/database" + "x-ui/database/model" + "x-ui/logger" + "x-ui/xray" + + "gorm.io/gorm" +) + +type OutboundService struct { + xrayApi xray.XrayAPI +} + +func (s *OutboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) { + var err error + db := database.GetDB() + tx := db.Begin() + + defer func() { + if err != nil { + tx.Rollback() + } else { + tx.Commit() + } + }() + + err = s.addOutboundTraffic(tx, traffics) + if err != nil { + return err, false + } + + return nil, false +} + +func (s *OutboundService) addOutboundTraffic(tx *gorm.DB, traffics []*xray.Traffic) error { + if len(traffics) == 0 { + return nil + } + + var err error + + for _, traffic := range traffics { + if traffic.IsOutbound { + + var outbound model.OutboundTraffics + + err = tx.Model(&model.OutboundTraffics{}).Where("tag = ?", traffic.Tag). + FirstOrCreate(&outbound).Error + if err != nil { + return err + } + + outbound.Tag = traffic.Tag + outbound.Up = outbound.Up + traffic.Up + outbound.Down = outbound.Down + traffic.Down + outbound.Total = outbound.Up + outbound.Down + + err = tx.Save(&outbound).Error + if err != nil { + return err + } + } + } + return nil +} + +func (s *OutboundService) GetOutboundsTraffic() ([]*model.OutboundTraffics, error) { + db := database.GetDB() + var traffics []*model.OutboundTraffics + + err := db.Model(model.OutboundTraffics{}).Find(&traffics).Error + if err != nil { + logger.Warning(err) + return nil, err + } + + return traffics, nil +} + +func (s *OutboundService) ResetOutboundTraffic(tag string) error { + db := database.GetDB() + + whereText := "tag " + if tag == "-alltags-" { + whereText += " <> ?" + } else { + whereText += " = ?" + } + + result := db.Model(model.OutboundTraffics{}). + Where(whereText, tag). + Updates(map[string]interface{}{"up": 0, "down": 0, "total": 0}) + + err := result.Error + if err != nil { + return err + } + + return nil +} diff --git a/web/service/tgbot.go b/web/service/tgbot.go index fdb5d312..87445a9a 100644 --- a/web/service/tgbot.go +++ b/web/service/tgbot.go @@ -6,9 +6,9 @@ import ( "net" "net/url" "os" + "slices" "strconv" "strings" - "slices" "time" "x-ui/config" "x-ui/database" @@ -115,14 +115,19 @@ func (t *Tgbot) Start(i18nFS embed.FS) error { } func (t *Tgbot) NewBot(token string, proxyUrl string) (*telego.Bot, error) { - if proxyUrl == "" || !strings.HasPrefix(proxyUrl, "socks5://") { - logger.Warning("invalid socks5 url, start with default") + if proxyUrl == "" { + // No proxy URL provided, use default instance + return telego.NewBot(token) + } + + if !strings.HasPrefix(proxyUrl, "socks5://") { + logger.Warning("Invalid socks5 URL, starting with default") return telego.NewBot(token) } _, err := url.Parse(proxyUrl) if err != nil { - logger.Warning("cant parse proxy url, use default instance for tgbot:", err) + logger.Warning("Can't parse proxy URL, using default instance for tgbot:", err) return telego.NewBot(token) } @@ -197,9 +202,13 @@ func (t *Tgbot) OnReceive() { }, th.AnyCallbackQueryWithMessage()) botHandler.HandleMessage(func(_ *telego.Bot, message telego.Message) { - if message.UserShared != nil { + if message.UsersShared != nil { if checkAdmin(message.From.ID) { - err := t.inboundService.SetClientTelegramUserID(message.UserShared.RequestID, strconv.FormatInt(message.UserShared.UserID, 10)) + userIDsStr := "" + for _, userID := range message.UsersShared.UserIDs { + userIDsStr += strconv.FormatInt(userID, 10) + " " + } + err := t.inboundService.SetClientTelegramUserID(message.UsersShared.RequestID, userIDsStr) output := "" if err != nil { output += t.I18nBot("tgbot.messages.selectUserFailed") @@ -260,7 +269,7 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo msg += t.I18nBot("tgbot.commands.unknown") } - if msg != ""{ + if msg != "" { if onlyMessage { t.SendMsgToTgbot(chatId, msg) return @@ -272,7 +281,7 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool) { - chatId := callbackQuery.Message.Chat.ID + chatId := callbackQuery.Message.GetChat().ID if isAdmin { // get query from hash storage @@ -291,22 +300,22 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool t.searchClient(chatId, email) case "client_refresh": t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.clientRefreshSuccess", "Email=="+email)) - t.searchClient(chatId, email, callbackQuery.Message.MessageID) + t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) case "client_cancel": t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+email)) - t.searchClient(chatId, email, callbackQuery.Message.MessageID) + t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) case "ips_refresh": t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.IpRefreshSuccess", "Email=="+email)) - t.searchClientIps(chatId, email, callbackQuery.Message.MessageID) + t.searchClientIps(chatId, email, callbackQuery.Message.GetMessageID()) case "ips_cancel": t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+email)) - t.searchClientIps(chatId, email, callbackQuery.Message.MessageID) + t.searchClientIps(chatId, email, callbackQuery.Message.GetMessageID()) case "tgid_refresh": t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.TGIdRefreshSuccess", "Email=="+email)) - t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.MessageID) + t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.GetMessageID()) case "tgid_cancel": t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+email)) - t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.MessageID) + t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.GetMessageID()) case "reset_traffic": inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( @@ -316,13 +325,13 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmResetTraffic")).WithCallbackData(t.encodeQuery("reset_traffic_c "+email)), ), ) - t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard) + t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) case "reset_traffic_c": err := t.inboundService.ResetClientTrafficByEmail(email) if err == nil { t.xrayService.SetToNeedRestart() t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.resetTrafficSuccess", "Email=="+email)) - t.searchClient(chatId, email, callbackQuery.Message.MessageID) + t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) } else { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) } @@ -346,7 +355,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool tu.InlineKeyboardButton("40 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 40")), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("50 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 60")), + tu.InlineKeyboardButton("50 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 50")), tu.InlineKeyboardButton("60 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 60")), tu.InlineKeyboardButton("80 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 80")), ), @@ -356,7 +365,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool tu.InlineKeyboardButton("200 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 200")), ), ) - t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard) + t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) case "limit_traffic_c": if len(dataArray) == 3 { limitTraffic, err := strconv.Atoi(dataArray[2]) @@ -365,13 +374,13 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool if err == nil { t.xrayService.SetToNeedRestart() t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.setTrafficLimitSuccess", "Email=="+email)) - t.searchClient(chatId, email, callbackQuery.Message.MessageID) + t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) return } } } t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) - t.searchClient(chatId, email, callbackQuery.Message.MessageID) + t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) case "limit_traffic_in": if len(dataArray) >= 3 { oldInputNumber, err := strconv.Atoi(dataArray[2]) @@ -427,12 +436,12 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool tu.InlineKeyboardButton("⬅️").WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" "+strconv.Itoa(inputNumber)+" -1")), ), ) - t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard) + t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) return } } t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) - t.searchClient(chatId, email, callbackQuery.Message.MessageID) + t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) case "reset_exp": inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( @@ -459,7 +468,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 12 "+t.I18nBot("tgbot.months")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 365")), ), ) - t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard) + t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) case "reset_exp_c": if len(dataArray) == 3 { days, err := strconv.Atoi(dataArray[2]) @@ -494,13 +503,13 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool if err == nil { t.xrayService.SetToNeedRestart() t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.expireResetSuccess", "Email=="+email)) - t.searchClient(chatId, email, callbackQuery.Message.MessageID) + t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) return } } } t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) - t.searchClient(chatId, email, callbackQuery.Message.MessageID) + t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) case "reset_exp_in": if len(dataArray) >= 3 { oldInputNumber, err := strconv.Atoi(dataArray[2]) @@ -556,12 +565,12 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool tu.InlineKeyboardButton("⬅️").WithCallbackData(t.encodeQuery("reset_exp_in "+email+" "+strconv.Itoa(inputNumber)+" -1")), ), ) - t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard) + t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) return } } t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) - t.searchClient(chatId, email, callbackQuery.Message.MessageID) + t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) case "ip_limit": inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( @@ -590,7 +599,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool tu.InlineKeyboardButton("10").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 10")), ), ) - t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard) + t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) case "ip_limit_c": if len(dataArray) == 3 { count, err := strconv.Atoi(dataArray[2]) @@ -599,13 +608,13 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool if err == nil { t.xrayService.SetToNeedRestart() t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.resetIpSuccess", "Email=="+email, "Count=="+strconv.Itoa(count))) - t.searchClient(chatId, email, callbackQuery.Message.MessageID) + t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) return } } } t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) - t.searchClient(chatId, email, callbackQuery.Message.MessageID) + t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) case "ip_limit_in": if len(dataArray) >= 3 { oldInputNumber, err := strconv.Atoi(dataArray[2]) @@ -661,12 +670,12 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool tu.InlineKeyboardButton("⬅️").WithCallbackData(t.encodeQuery("ip_limit_in "+email+" "+strconv.Itoa(inputNumber)+" -1")), ), ) - t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard) + t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) return } } t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) - t.searchClient(chatId, email, callbackQuery.Message.MessageID) + t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) case "clear_ips": inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( @@ -676,12 +685,12 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmClearIps")).WithCallbackData(t.encodeQuery("clear_ips_c "+email)), ), ) - t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard) + t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) case "clear_ips_c": err := t.inboundService.ClearClientIps(email) if err == nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.clearIpSuccess", "Email=="+email)) - t.searchClientIps(chatId, email, callbackQuery.Message.MessageID) + t.searchClientIps(chatId, email, callbackQuery.Message.GetMessageID()) } else { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) } @@ -700,7 +709,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmRemoveTGUser")).WithCallbackData(t.encodeQuery("tgid_remove_c "+email)), ), ) - t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard) + t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) case "tgid_remove_c": traffic, err := t.inboundService.GetClientTrafficByEmail(email) if err != nil || traffic == nil { @@ -710,7 +719,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool err = t.inboundService.SetClientTelegramUserID(traffic.Id, "") if err == nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.removedTGUserSuccess", "Email=="+email)) - t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.MessageID) + t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.GetMessageID()) } else { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) } @@ -723,7 +732,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmToggle")).WithCallbackData(t.encodeQuery("toggle_enable_c "+email)), ), ) - t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard) + t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) case "toggle_enable_c": enabled, err := t.inboundService.ToggleClientEnableByEmail(email) if err == nil { @@ -733,7 +742,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool } else { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.disableSuccess", "Email=="+email)) } - t.searchClient(chatId, email, callbackQuery.Message.MessageID) + t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) } else { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) } @@ -769,7 +778,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool t.onlineClients(chatId) case "onlines_refresh": t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) - t.onlineClients(chatId, callbackQuery.Message.MessageID) + t.onlineClients(chatId, callbackQuery.Message.GetMessageID()) case "commands": t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.commands")) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.helpAdminCommands")) @@ -1022,7 +1031,7 @@ func (t *Tgbot) getInboundUsages() string { } func (t *Tgbot) clientInfoMsg(traffic *xray.ClientTraffic, printEnabled bool, printOnline bool, printActive bool, - printDate bool, printTraffic bool, printRefreshed bool) string { + printDate bool, printTraffic bool, printRefreshed bool) string { now := time.Now().Unix() expiryTime := "" @@ -1210,13 +1219,13 @@ func (t *Tgbot) clientTelegramUserInfo(chatId int64, email string, messageID ... t.editMessageTgBot(chatId, messageID[0], output, inlineKeyboard) } else { t.SendMsgToTgbot(chatId, output, inlineKeyboard) - requestUser := telego.KeyboardButtonRequestUser{ + requestUser := telego.KeyboardButtonRequestUsers{ RequestID: int32(traffic.Id), UserIsBot: new(bool), } keyboard := tu.Keyboard( tu.KeyboardRow( - tu.KeyboardButton(t.I18nBot("tgbot.buttons.selectTGUser")).WithRequestUser(&requestUser), + tu.KeyboardButton(t.I18nBot("tgbot.buttons.selectTGUser")).WithRequestUsers(&requestUser), ), tu.KeyboardRow( tu.KeyboardButton(t.I18nBot("tgbot.buttons.closeKeyboard")), @@ -1380,7 +1389,6 @@ func (t *Tgbot) getExhausted(chatId int64) { output += t.I18nBot("tgbot.messages.exhaustedCount", "Type=="+t.I18nBot("tgbot.clients")) output += t.I18nBot("tgbot.messages.disabled", "Disabled=="+strconv.Itoa(len(disabledClients))) output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(exhaustedCC)) - if exhaustedCC > 0 { output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+t.I18nBot("tgbot.clients")) @@ -1490,7 +1498,6 @@ func (t *Tgbot) onlineClients(chatId int64, messageID ...int) { output := t.I18nBot("tgbot.messages.onlinesCount", "Count=="+fmt.Sprint(onlinesCount)) keyboard := tu.InlineKeyboard(tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.refresh")).WithCallbackData(t.encodeQuery("onlines_refresh")))) - if onlinesCount > 0 { var buttons []telego.InlineKeyboardButton @@ -1565,30 +1572,44 @@ func (t *Tgbot) sendBanLogs(chatId int64, dt bool) { file, err := os.Open(xray.GetIPLimitBannedPrevLogPath()) if err == nil { - document := tu.Document( - tu.ID(chatId), - tu.File(file), - ) - _, err = bot.SendDocument(document) - if err != nil { - logger.Error("Error in uploading backup: ", err) + // Check if the file is non-empty before attempting to upload + fileInfo, _ := file.Stat() + if fileInfo.Size() > 0 { + document := tu.Document( + tu.ID(chatId), + tu.File(file), + ) + _, err = bot.SendDocument(document) + if err != nil { + logger.Error("Error in uploading IPLimitBannedPrevLog: ", err) + } + } else { + logger.Warning("IPLimitBannedPrevLog file is empty, not uploading.") } + file.Close() } else { - logger.Error("Error in opening db file for backup: ", err) + logger.Error("Error in opening IPLimitBannedPrevLog file for backup: ", err) } file, err = os.Open(xray.GetIPLimitBannedLogPath()) if err == nil { - document := tu.Document( - tu.ID(chatId), - tu.File(file), - ) - _, err = bot.SendDocument(document) - if err != nil { - logger.Error("Error in uploading config.json: ", err) + // Check if the file is non-empty before attempting to upload + fileInfo, _ := file.Stat() + if fileInfo.Size() > 0 { + document := tu.Document( + tu.ID(chatId), + tu.File(file), + ) + _, err = bot.SendDocument(document) + if err != nil { + logger.Error("Error in uploading IPLimitBannedLog: ", err) + } + } else { + logger.Warning("IPLimitBannedLog file is empty, not uploading.") } + file.Close() } else { - logger.Error("Error in opening config.json file for backup: ", err) + logger.Error("Error in opening IPLimitBannedLog file for backup: ", err) } } diff --git a/web/service/xray.go b/web/service/xray.go index 82d1cc3f..7cd1612c 100644 --- a/web/service/xray.go +++ b/web/service/xray.go @@ -95,7 +95,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) { if !clientTraffic.Enable { clients = RemoveIndex(clients, index-indexDecrease) indexDecrease++ - logger.Info("Remove Inbound User", c["email"], "due the expire or traffic limit") + logger.Info("Remove Inbound User ", c["email"], " due the expire or traffic limit") } diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml index 7e0f26c5..157baf8c 100644 --- a/web/translation/translate.en_US.toml +++ b/web/translation/translate.en_US.toml @@ -76,7 +76,7 @@ "title" = "Overview" "memory" = "RAM" "hard" = "Disk" -"xrayStatus" = "Status" +"xrayStatus" = "Xray" "stopXray" = "Stop" "restartXray" = "Restart" "xraySwitch" = "Version" @@ -309,8 +309,8 @@ "restart" = "Restart Xray" "basicTemplate" = "Basics" "advancedTemplate" = "Advanced" -"generalConfigs" = "General Strategy" -"generalConfigsDesc" = "These options will determine general strategy adjustments." +"generalConfigs" = "General" +"generalConfigsDesc" = "These options will determine general adjustments." "blockConfigs" = "Protection Shield" "blockConfigsDesc" = "These options will block traffic based on specific requested protocols and websites." "blockCountryConfigs" = "Block Country" @@ -388,10 +388,15 @@ "Inbounds" = "Inbounds" "InboundsDesc" = "Accepting the specific clients." "Outbounds" = "Outbounds" +"Balancers" = "Balancers" "OutboundsDesc" = "Set the outgoing traffic pathway." "Routings" = "Routing Rules" "RoutingsDesc" = "The priority of each rule is important!" "completeTemplate" = "All" +"logLevel" = "Log Level" +"logLevelDesc" = "The log level for error logs, indicating the information that needs to be recorded." +"accessLog" = "Access Log" +"accessLogDesc" = "The file path for the access log. The special value 'none' disabled access logs" [pages.xray.rules] "first" = "First" @@ -402,6 +407,7 @@ "dest" = "Destination" "inbound" = "Inbound" "outbound" = "Outbound" +"balancer" = "Balancer" "info" = "Info" "add" = "Add Rule" "edit" = "Edit Rule" @@ -422,6 +428,15 @@ "portal" = "Portal" "intercon" = "Interconnection" +[pages.xray.balancer] +"addBalancer" = "Add Balancer" +"editBalancer" = "Edit Balancer" +"balancerStrategy" = "Strategy" +"balancerSelectors" = "Selectors" +"tag" = "Tag" +"tagDesc" = "Unique Tag" +"balancerDesc" = "It is not possible to use balancerTag and outboundTag at the same time. If used at the same time, only outboundTag will work." + [pages.xray.wireguard] "secretKey" = "Secret Key" "publicKey" = "Public Key" @@ -452,7 +467,7 @@ "wentWrong" = "❌ Something went wrong!" "noIpRecord" = "❗ No IP Record!" "noInbounds" = "❗ No inbound found!" -"unlimited" = "♾ Unlimited" +"unlimited" = "♾ Unlimited(Reset)" "add" = "Add" "month" = "Month" "months" = "Months" diff --git a/web/translation/translate.es_ES.toml b/web/translation/translate.es_ES.toml index ac9de9a4..b0d54d87 100644 --- a/web/translation/translate.es_ES.toml +++ b/web/translation/translate.es_ES.toml @@ -20,7 +20,7 @@ "check" = "Verificar" "indefinite" = "Indefinido" "unlimited" = "Ilimitado" -"none" = "Ninguno" +"none" = "None" "qrCode" = "Código QR" "info" = "Más Información" "edit" = "Editar" @@ -76,7 +76,7 @@ "title" = "Estado del Sistema" "memory" = "Memoria" "hard" = "Disco Duro" -"xrayStatus" = "Estado de" +"xrayStatus" = "Xray" "stopXray" = "Detener" "restartXray" = "Reiniciar" "xraySwitch" = "Versión" @@ -388,10 +388,15 @@ "Inbounds" = "Entrante" "InboundsDesc" = "Cambia la plantilla de configuración para aceptar clientes específicos." "Outbounds" = "Salidas" +"Balancers" = "Equilibradores" "OutboundsDesc" = "Cambia la plantilla de configuración para definir formas de salida para este servidor." "Routings" = "Reglas de enrutamiento" "RoutingsDesc" = "¡La prioridad de cada regla es importante!" "completeTemplate" = "Todos" +"logLevel" = "Nivel de registro" +"logLevelDesc" = "El nivel de registro para registros de errores, que indica la información que debe registrarse." +"accessLog" = "Registro de acceso" +"accessLogDesc" = "La ruta del archivo para el registro de acceso. El valor especial 'ninguno' deshabilita los registros de acceso" [pages.xray.rules] "first" = "Primero" @@ -402,6 +407,7 @@ "dest" = "Destino" "inbound" = "Entrante" "outbound" = "saliente" +"balancer" = "Balancín" "info" = "Información" "add" = "Agregar regla" "edit" = "Editar regla" @@ -422,6 +428,15 @@ "portal" = "portal" "intercon" = "Interconexión" +[pages.xray.balancer] +"addBalancer" = "Agregar equilibrador" +"editBalancer" = "Editar balanceador" +"balancerStrategy" = "Estrategia" +"balancerSelectors" = "Selectores" +"tag" = "Etiqueta" +"tagDesc" = "etiqueta única" +"balancerDesc" = "No es posible utilizar balancerTag y outboundTag al mismo tiempo. Si se utilizan al mismo tiempo, sólo funcionará outboundTag." + [pages.xray.wireguard] "secretKey" = "Llave secreta" "publicKey" = "Llave pública" diff --git a/web/translation/translate.fa_IR.toml b/web/translation/translate.fa_IR.toml index 11b66788..5be08677 100644 --- a/web/translation/translate.fa_IR.toml +++ b/web/translation/translate.fa_IR.toml @@ -76,7 +76,7 @@ "title" = "نمای کلی" "memory" = "RAM" "hard" = "Disk" -"xrayStatus" = "وضعیت‌ایکس‌ری" +"xrayStatus" = "ایکس‌ری" "stopXray" = "توقف" "restartXray" = "شروع‌مجدد" "xraySwitch" = "‌نسخه" @@ -388,10 +388,15 @@ "Inbounds" = "ورودی‌ها" "InboundsDesc" = "پذیرش کلاینت خاص" "Outbounds" = "خروجی‌ها" +"Balancers" = "بالانسرها" "OutboundsDesc" = "مسیر ترافیک خروجی را تنظیم کنید" "Routings" = "قوانین مسیریابی" "RoutingsDesc" = "اولویت هر قانون مهم است" "completeTemplate" = "کامل" +"logLevel" = "سطح گزارش" +"logLevelDesc" = "سطح گزارش برای گزارش های خطا، نشان دهنده اطلاعاتی است که باید ثبت شوند." +"accessLog" = "مسیر گزارش" +"accessLogDesc" = "مسیر فایل برای گزارش دسترسی. مقدار ویژه «هیچ» گزارش‌های دسترسی را غیرفعال میکند." [pages.xray.rules] "first" = "اولین" @@ -402,6 +407,7 @@ "dest" = "مقصد" "inbound" = "ورودی" "outbound" = "خروجی" +"balancer" = "بالانسر" "info" = "اطلاعات" "add" = "افزودن قانون" "edit" = "ویرایش قانون" @@ -422,6 +428,15 @@ "portal" = "پورتال" "intercon" = "اتصال میانی" +[pages.xray.balancer] +"addBalancer" = "افزودن بالانسر" +"editBalancer" = "ویرایش بالانسر" +"balancerStrategy" = "استراتژی" +"balancerSelectors" = "انتخاب‌گرها" +"tag" = "برچسب" +"tagDesc" = "برچسب یگانه" +"balancerDesc" = "امکان استفاده همزمان balancerTag و outboundTag باهم وجود ندارد. درصورت استفاده همزمان فقط outboundTag عمل خواهد کرد." + [pages.xray.wireguard] "secretKey" = "کلید شخصی" "publicKey" = "کلید عمومی" @@ -452,7 +467,7 @@ "wentWrong" = "❌ مشکلی رخ داده است!" "noIpRecord" = "❗ رکورد IP یافت نشد!" "noInbounds" = "❗ هیچ ورودی یافت نشد!" -"unlimited" = "♾ نامحدود" +"unlimited" = "♾ - نامحدود(ریست)" "add" = "اضافه کردن" "month" = "ماه" "months" = "ماه‌ها" diff --git a/web/translation/translate.id_ID.toml b/web/translation/translate.id_ID.toml new file mode 100644 index 00000000..ad108b05 --- /dev/null +++ b/web/translation/translate.id_ID.toml @@ -0,0 +1,591 @@ +"username" = "Nama Pengguna" +"password" = "Kata Sandi" +"login" = "Masuk" +"confirm" = "Konfirmasi" +"cancel" = "Batal" +"close" = "Tutup" +"copy" = "Salin" +"copied" = "Tersalin" +"download" = "Unduh" +"remark" = "Catatan" +"enable" = "Aktifkan" +"protocol" = "Protokol" +"search" = "Cari" +"filter" = "Filter" +"loading" = "Memuat..." +"second" = "Detik" +"minute" = "Menit" +"hour" = "Jam" +"day" = "Hari" +"check" = "Centang" +"indefinite" = "Tak Terbatas" +"unlimited" = "Tanpa Batas" +"none" = "None" +"qrCode" = "Kode QR" +"info" = "Informasi Lebih Lanjut" +"edit" = "Edit" +"delete" = "Hapus" +"reset" = "Reset" +"copySuccess" = "Berhasil Disalin" +"sure" = "Yakin" +"encryption" = "Enkripsi" +"transmission" = "Transmisi" +"host" = "Host" +"path" = "Jalur" +"camouflage" = "Obfuscation" +"status" = "Status" +"enabled" = "Aktif" +"disabled" = "Nonaktif" +"depleted" = "Habis" +"depletingSoon" = "Akan Habis" +"offline" = "Offline" +"online" = "Online" +"domainName" = "Nama Domain" +"monitor" = "IP Pemantauan" +"certificate" = "Sertifikat" +"fail" = "Gagal" +"success" = "Berhasil" +"getVersion" = "Dapatkan Versi" +"install" = "Instal" +"clients" = "Klien" +"usage" = "Penggunaan" +"secretToken" = "Token Rahasia" +"remained" = "Tersisa" +"security" = "Keamanan" + +[menu] +"dashboard" = "Ikhtisar" +"inbounds" = "Masuk" +"settings" = "Pengaturan Panel" +"xray" = "Konfigurasi Xray" +"logout" = "Keluar" +"link" = "Kelola" + +[pages.login] +"title" = "Selamat Datang" +"loginAgain" = "Sesi Anda telah berakhir, harap masuk kembali" + +[pages.login.toasts] +"invalidFormData" = "Format data input tidak valid." +"emptyUsername" = "Nama Pengguna diperlukan" +"emptyPassword" = "Kata Sandi diperlukan" +"wrongUsernameOrPassword" = "Nama pengguna atau kata sandi tidak valid." +"successLogin" = "Login berhasil" + +[pages.index] +"title" = "Ikhtisar" +"memory" = "RAM" +"hard" = "Disk" +"xrayStatus" = "Xray" +"stopXray" = "Stop" +"restartXray" = "Restart" +"xraySwitch" = "Versi" +"xraySwitchClick" = "Pilih versi yang ingin Anda pindah." +"xraySwitchClickDesk" = "Pilih dengan hati-hati, karena versi yang lebih lama mungkin tidak kompatibel dengan konfigurasi saat ini." +"operationHours" = "Waktu Aktif" +"systemLoad" = "Beban Sistem" +"systemLoadDesc" = "Rata-rata beban sistem selama 1, 5, dan 15 menit terakhir" +"connectionTcpCountDesc" = "Total koneksi TCP di seluruh sistem" +"connectionUdpCountDesc" = "Total koneksi UDP di seluruh sistem" +"connectionCount" = "Statistik Koneksi" +"upSpeed" = "Kecepatan unggah keseluruhan di seluruh sistem" +"downSpeed" = "Kecepatan unduh keseluruhan di seluruh sistem" +"totalSent" = "Total data terkirim di seluruh sistem sejak startup OS" +"totalReceive" = "Total data diterima di seluruh sistem sejak startup OS" +"xraySwitchVersionDialog" = "Ganti Versi Xray" +"xraySwitchVersionDialogDesc" = "Apakah Anda yakin ingin mengubah versi Xray menjadi" +"dontRefresh" = "Instalasi sedang berlangsung, harap jangan menyegarkan halaman ini" +"logs" = "Log" +"config" = "Konfigurasi" +"backup" = "Cadangan & Pulihkan" +"backupTitle" = "Cadangan & Pulihkan Database" +"backupDescription" = "Disarankan untuk membuat cadangan sebelum memulihkan database." +"exportDatabase" = "Cadangkan" +"importDatabase" = "Pulihkan" + +[pages.inbounds] +"title" = "Masuk" +"totalDownUp" = "Total Terkirim/Diterima" +"totalUsage" = "Penggunaan Total" +"inboundCount" = "Total Masuk" +"operate" = "Menu" +"enable" = "Aktifkan" +"remark" = "Catatan" +"protocol" = "Protokol" +"port" = "Port" +"traffic" = "Traffic" +"details" = "Rincian" +"transportConfig" = "Transport" +"expireDate" = "Durasi" +"resetTraffic" = "Reset Traffic" +"addInbound" = "Tambahkan Masuk" +"generalActions" = "Tindakan Umum" +"create" = "Buat" +"update" = "Perbarui" +"modifyInbound" = "Ubah Masuk" +"deleteInbound" = "Hapus Masuk" +"deleteInboundContent" = "Apakah Anda yakin ingin menghapus masuk?" +"deleteClient" = "Hapus Klien" +"deleteClientContent" = "Apakah Anda yakin ingin menghapus klien?" +"resetTrafficContent" = "Apakah Anda yakin ingin mereset traffic?" +"copyLink" = "Salin URL" +"address" = "Alamat" +"network" = "Jaringan" +"destinationPort" = "Port Tujuan" +"targetAddress" = "Alamat Target" +"monitorDesc" = "Biarkan kosong untuk mendengarkan semua IP" +"meansNoLimit" = " = Unlimited. (unit: GB)" +"totalFlow" = "Total Aliran" +"leaveBlankToNeverExpire" = "Biarkan kosong untuk tidak pernah kedaluwarsa" +"noRecommendKeepDefault" = "Disarankan untuk tetap menggunakan pengaturan default" +"certificatePath" = "Path Berkas" +"certificateContent" = "Konten Berkas" +"publicKeyPath" = "Path Kunci Publik" +"publicKeyContent" = "Konten Kunci Publik" +"keyPath" = "Path Kunci Privat" +"keyContent" = "Konten Kunci Privat" +"clickOnQRcode" = "Klik pada Kode QR untuk Menyalin" +"client" = "Klien" +"export" = "Ekspor Semua URL" +"clone" = "Duplikat" +"cloneInbound" = "Duplikat" +"cloneInboundContent" = "Semua pengaturan masuk ini, kecuali Port, Listening IP, dan Klien, akan diterapkan pada duplikat." +"cloneInboundOk" = "Duplikat" +"resetAllTraffic" = "Reset Semua Traffic Masuk" +"resetAllTrafficTitle" = "Reset Semua Traffic Masuk" +"resetAllTrafficContent" = "Apakah Anda yakin ingin mereset traffic semua masuk?" +"resetInboundClientTraffics" = "Reset Traffic Klien Masuk" +"resetInboundClientTrafficTitle" = "Reset Traffic Klien Masuk" +"resetInboundClientTrafficContent" = "Apakah Anda yakin ingin mereset traffic klien masuk ini?" +"resetAllClientTraffics" = "Reset Traffic Semua Klien" +"resetAllClientTrafficTitle" = "Reset Traffic Semua Klien" +"resetAllClientTrafficContent" = "Apakah Anda yakin ingin mereset traffic semua klien?" +"delDepletedClients" = "Hapus Klien Habis" +"delDepletedClientsTitle" = "Hapus Klien Habis" +"delDepletedClientsContent" = "Apakah Anda yakin ingin menghapus semua klien yang habis?" +"email" = "Email" +"emailDesc" = "Harap berikan alamat email yang unik." +"IPLimit" = "Batas IP" +"IPLimitDesc" = "Menonaktifkan masuk jika jumlah melebihi nilai yang ditetapkan. (0 = nonaktif)" +"IPLimitlog" = "Log IP" +"IPLimitlogDesc" = "Log histori IP. (untuk mengaktifkan masuk setelah menonaktifkan, hapus log)" +"IPLimitlogclear" = "Hapus Log" +"setDefaultCert" = "Atur Sertifikat dari Panel" +"xtlsDesc" = "Xray harus versi 1.7.5" +"realityDesc" = "Xray harus versi 1.8.0+" +"telegramDesc" = "Harap berikan ID Telegram atau obrolan tanpa menggunakan '@'. (dapatkan di sini @userinfobot) atau (gunakan perintah '/id' di bot)" +"subscriptionDesc" = "Untuk menemukan URL langganan Anda, buka 'Rincian'. Selain itu, Anda dapat menggunakan nama yang sama untuk beberapa klien." +"info" = "Info" +"same" = "Sama" +"inboundData" = "Data Masuk" +"exportInbound" = "Ekspor Masuk" +"import" = "Impor" +"importInbound" = "Impor Masuk" + +[pages.client] +"add" = "Tambah Klien" +"edit" = "Edit Klien" +"submitAdd" = "Tambah Klien" +"submitEdit" = "Simpan Perubahan" +"clientCount" = "Jumlah Klien" +"bulk" = "Tambahkan Massal" +"method" = "Metode" +"first" = "Pertama" +"last" = "Terakhir" +"prefix" = "Awalan" +"postfix" = "Akhiran" +"delayedStart" = "Mulai saat Penggunaan Awal" +"expireDays" = "Durasi" +"days" = "Hari" +"renew" = "Perpanjang Otomatis" +"renewDesc" = "Perpanjangan otomatis setelah kedaluwarsa. (0 = nonaktif)(unit: hari)" + +[pages.inbounds.toasts] +"obtain" = "Dapatkan" + +[pages.inbounds.stream.general] +"request" = "Permintaan" +"response" = "Respons" +"name" = "Nama" +"value" = "Nilai" + +[pages.inbounds.stream.tcp] +"version" = "Versi" +"method" = "Metode" +"path" = "Path" +"status" = "Status" +"statusDescription" = "Deskripsi Status" +"requestHeader" = "Header Permintaan" +"responseHeader" = "Header Respons" + +[pages.inbounds.stream.quic] +"encryption" = "Enkripsi" + +[pages.settings] +"title" = "Pengaturan Panel" +"save" = "Simpan" +"infoDesc" = "Setiap perubahan yang dibuat di sini perlu disimpan. Harap restart panel untuk menerapkan perubahan." +"restartPanel" = "Restart Panel" +"restartPanelDesc" = "Apakah Anda yakin ingin merestart panel? Jika Anda tidak dapat mengakses panel setelah merestart, lihat info log panel di server." +"actions" = "Tindakan" +"resetDefaultConfig" = "Reset ke Default" +"panelSettings" = "Umum" +"securitySettings" = "Otentikasi" +"TGBotSettings" = "Bot Telegram" +"panelListeningIP" = "IP Pendengar" +"panelListeningIPDesc" = "Alamat IP untuk panel web. (biarkan kosong untuk mendengarkan semua IP)" +"panelListeningDomain" = "Domain Pendengar" +"panelListeningDomainDesc" = "Nama domain untuk panel web. (biarkan kosong untuk mendengarkan semua domain dan IP)" +"panelPort" = "Port Pendengar" +"panelPortDesc" = "Nomor port untuk panel web. (harus menjadi port yang tidak digunakan)" +"publicKeyPath" = "Path Kunci Publik" +"publicKeyPathDesc" = "Path berkas kunci publik untuk panel web. (dimulai dengan ‘/‘)" +"privateKeyPath" = "Path Kunci Privat" +"privateKeyPathDesc" = "Path berkas kunci privat untuk panel web. (dimulai dengan ‘/‘)" +"panelUrlPath" = "URI Path" +"panelUrlPathDesc" = "URI path untuk panel web. (dimulai dengan ‘/‘ dan diakhiri dengan ‘/‘)" +"pageSize" = "Ukuran Halaman" +"pageSizeDesc" = "Tentukan ukuran halaman untuk tabel masuk. (0 = nonaktif)" +"remarkModel" = "Model Catatan & Karakter Pemisah" +"datepicker" = "Jenis Kalender" +"datepickerPlaceholder" = "Pilih tanggal" +"datepickerDescription" = "Tugas terjadwal akan berjalan berdasarkan kalender ini." +"sampleRemark" = "Contoh Catatan" +"oldUsername" = "Username Saat Ini" +"currentPassword" = "Kata Sandi Saat Ini" +"newUsername" = "Username Baru" +"newPassword" = "Kata Sandi Baru" +"telegramBotEnable" = "Aktifkan Bot Telegram" +"telegramBotEnableDesc" = "Mengaktifkan bot Telegram." +"telegramToken" = "Token Telegram" +"telegramTokenDesc" = "Token bot Telegram yang diperoleh dari '@BotFather'." +"telegramProxy" = "Proxy SOCKS" +"telegramProxyDesc" = "Mengaktifkan proxy SOCKS5 untuk terhubung ke Telegram. (sesuaikan pengaturan sesuai panduan)" +"telegramChatId" = "ID Obrolan Admin" +"telegramChatIdDesc" = "ID Obrolan Admin Telegram. (dipisahkan koma)(dapatkan di sini @userinfobot) atau (gunakan perintah '/id' di bot)" +"telegramNotifyTime" = "Waktu Notifikasi" +"telegramNotifyTimeDesc" = "Waktu notifikasi bot Telegram yang diatur untuk laporan berkala. (gunakan format waktu crontab)" +"tgNotifyBackup" = "Cadangan Database" +"tgNotifyBackupDesc" = "Kirim berkas cadangan database dengan laporan." +"tgNotifyLogin" = "Notifikasi Login" +"tgNotifyLoginDesc" = "Dapatkan notifikasi tentang username, alamat IP, dan waktu setiap kali seseorang mencoba masuk ke panel web Anda." +"sessionMaxAge" = "Durasi Sesi" +"sessionMaxAgeDesc" = "Durasi di mana Anda dapat tetap masuk. (unit: menit)" +"expireTimeDiff" = "Notifikasi Tanggal Kedaluwarsa" +"expireTimeDiffDesc" = "Dapatkan notifikasi tentang tanggal kedaluwarsa saat mencapai ambang batas ini. (unit: hari)" +"trafficDiff" = "Notifikasi Batas Traffic" +"trafficDiffDesc" = "Dapatkan notifikasi tentang batas traffic saat mencapai ambang batas ini. (unit: GB)" +"tgNotifyCpu" = "Notifikasi Beban CPU" +"tgNotifyCpuDesc" = "Dapatkan notifikasi jika beban CPU melebihi ambang batas ini. (unit: %)" +"timeZone" = "Zone Waktu" +"timeZoneDesc" = "Tugas terjadwal akan berjalan berdasarkan zona waktu ini." +"subSettings" = "Langganan" +"subEnable" = "Aktifkan Layanan Langganan" +"subEnableDesc" = "Mengaktifkan layanan langganan." +"subListen" = "IP Pendengar" +"subListenDesc" = "Alamat IP untuk layanan langganan. (biarkan kosong untuk mendengarkan semua IP)" +"subPort" = "Port Pendengar" +"subPortDesc" = "Nomor port untuk layanan langganan. (harus menjadi port yang tidak digunakan)" +"subCertPath" = "Path Kunci Publik" +"subCertPathDesc" = "Path berkas kunci publik untuk layanan langganan. (dimulai dengan ‘/‘)" +"subKeyPath" = "Path Kunci Privat" +"subKeyPathDesc" = "Path berkas kunci privat untuk layanan langganan. (dimulai dengan ‘/‘)" +"subPath" = "URI Path" +"subPathDesc" = "URI path untuk layanan langganan. (dimulai dengan ‘/‘ dan diakhiri dengan ‘/‘)" +"subDomain" = "Domain Pendengar" +"subDomainDesc" = "Nama domain untuk layanan langganan. (biarkan kosong untuk mendengarkan semua domain dan IP)" +"subUpdates" = "Interval Pembaruan" +"subUpdatesDesc" = "Interval pembaruan URL langganan dalam aplikasi klien. (unit: jam)" +"subEncrypt" = "Encode" +"subEncryptDesc" = "Konten yang dikembalikan dari layanan langganan akan dienkripsi Base64." +"subShowInfo" = "Tampilkan Info Penggunaan" +"subShowInfoDesc" = "Sisa traffic dan tanggal akan ditampilkan di aplikasi klien." +"subURI" = "URI Proxy Terbalik" +"subURIDesc" = "URI path URL langganan untuk penggunaan di belakang proxy." + +[pages.xray] +"title" = "Konfigurasi Xray" +"save" = "Simpan" +"restart" = "Restart Xray" +"basicTemplate" = "Dasar" +"advancedTemplate" = "Lanjutan" +"generalConfigs" = "Strategi Umum" +"generalConfigsDesc" = "Opsi ini akan menentukan penyesuaian strategi umum." +"blockConfigs" = "Pelindung" +"blockConfigsDesc" = "Opsi ini akan memblokir lalu lintas berdasarkan protokol dan situs web yang diminta." +"blockCountryConfigs" = "Blokir Negara" +"blockCountryConfigsDesc" = "Opsi ini akan memblokir lalu lintas berdasarkan negara yang diminta." +"directCountryConfigs" = "Langsung ke Negara" +"directCountryConfigsDesc" = "Opsi ini akan langsung meneruskan lalu lintas berdasarkan negara yang diminta." +"ipv4Configs" = "Pengalihan IPv4" +"ipv4ConfigsDesc" = "Opsi ini akan mengalihkan lalu lintas berdasarkan tujuan tertentu melalui IPv4." +"warpConfigs" = "Pengalihan WARP" +"warpConfigsDesc" = "Opsi ini akan mengalihkan lalu lintas berdasarkan tujuan tertentu melalui WARP." +"Template" = "Template Konfigurasi Xray Lanjutan" +"TemplateDesc" = "File konfigurasi Xray akhir akan dibuat berdasarkan template ini." +"FreedomStrategy" = "Strategi Protokol Freedom" +"FreedomStrategyDesc" = "Atur strategi output untuk jaringan dalam Protokol Freedom." +"RoutingStrategy" = "Strategi Pengalihan Keseluruhan" +"RoutingStrategyDesc" = "Atur strategi pengalihan lalu lintas keseluruhan untuk menyelesaikan semua permintaan." +"Torrent" = "Blokir Protokol BitTorrent" +"TorrentDesc" = "Memblokir protokol BitTorrent." +"PrivateIp" = "Blokir Koneksi ke IP Pribadi" +"PrivateIpDesc" = "Memblokir pembentukan koneksi ke rentang IP pribadi." +"Ads" = "Blokir Iklan" +"AdsDesc" = "Memblokir situs web periklanan." +"Family" = "Proteksi Keluarga" +"FamilyDesc" = "Memblokir konten dewasa dan situs web berbahaya." +"Security" = "Pelindung Keamanan" +"SecurityDesc" = "Memblokir situs web malware, phishing, dan penambang kripto." +"Speedtest" = "Blokir Speedtest" +"SpeedtestDesc" = "Memblokir pembentukan koneksi ke situs web speedtest." +"IRIp" = "Blokir Koneksi ke IP Iran" +"IRIpDesc" = "Memblokir pembentukan koneksi ke rentang IP Iran." +"IRDomain" = "Blokir Koneksi ke Domain Iran" +"IRDomainDesc" = "Memblokir pembentukan koneksi ke domain Iran." +"ChinaIp" = "Blokir Koneksi ke IP China" +"ChinaIpDesc" = "Memblokir pembentukan koneksi ke rentang IP China." +"ChinaDomain" = "Blokir Koneksi ke Domain China" +"ChinaDomainDesc" = "Memblokir pembentukan koneksi ke domain China." +"RussiaIp" = "Blokir Koneksi ke IP Rusia" +"RussiaIpDesc" = "Memblokir pembentukan koneksi ke rentang IP Rusia." +"RussiaDomain" = "Blokir Koneksi ke Domain Rusia" +"RussiaDomainDesc" = "Memblokir pembentukan koneksi ke domain Rusia." +"VNIp" = "Blokir Koneksi ke IP Vietnam" +"VNIpDesc" = "Memblokir pembentukan koneksi ke rentang IP Vietnam." +"VNDomain" = "Blokir Koneksi ke Domain Vietnam" +"VNDomainDesc" = "Memblokir pembentukan koneksi ke domain Vietnam." +"DirectIRIp" = "Koneksi Langsung ke IP Iran" +"DirectIRIpDesc" = "Membentuk koneksi langsung ke rentang IP Iran." +"DirectIRDomain" = "Koneksi Langsung ke Domain Iran" +"DirectIRDomainDesc" = "Membentuk koneksi langsung ke domain Iran." +"DirectChinaIp" = "Koneksi Langsung ke IP China" +"DirectChinaIpDesc" = "Membentuk koneksi langsung ke rentang IP China." +"DirectChinaDomain" = "Koneksi Langsung ke Domain China" +"DirectChinaDomainDesc" = "Membentuk koneksi langsung ke domain China." +"DirectRussiaIp" = "Koneksi Langsung ke IP Rusia" +"DirectRussiaIpDesc" = "Membentuk koneksi langsung ke rentang IP Rusia." +"DirectRussiaDomain" = "Koneksi Langsung ke Domain Rusia" +"DirectRussiaDomainDesc" = "Membentuk koneksi langsung ke domain Rusia." +"DirectVNIp" = "Koneksi Langsung ke IP Vietnam" +"DirectVNIpDesc" = "Membentuk koneksi langsung ke rentang IP Vietnam." +"DirectVNDomain" = "Koneksi Langsung ke Domain Vietnam" +"DirectVNDomainDesc" = "Membentuk koneksi langsung ke domain Vietnam." +"GoogleIPv4" = "Google" +"GoogleIPv4Desc" = "Rute lalu lintas ke Google melalui IPv4." +"NetflixIPv4" = "Netflix" +"NetflixIPv4Desc" = "Rute lalu lintas ke Netflix melalui IPv4." +"GoogleWARP" = "Google" +"GoogleWARPDesc" = "Tambahkan pengalihan untuk Google melalui WARP." +"OpenAIWARP" = "ChatGPT" +"OpenAIWARPDesc" = "Rute lalu lintas ke ChatGPT melalui WARP." +"NetflixWARP" = "Netflix" +"NetflixWARPDesc" = "Rute lalu lintas ke Netflix melalui WARP." +"SpotifyWARP" = "Spotify" +"SpotifyWARPDesc" = "Rute lalu lintas ke Spotify melalui WARP." +"IRWARP" = "Domain Iran" +"IRWARPDesc" = "Rute lalu lintas ke domain Iran melalui WARP." +"Inbounds" = "Masuk" +"InboundsDesc" = "Menerima klien tertentu." +"Outbounds" = "Keluar" +"Balancers" = "Penyeimbang" +"OutboundsDesc" = "Atur jalur lalu lintas keluar." +"Routings" = "Aturan Pengalihan" +"RoutingsDesc" = "Prioritas setiap aturan penting!" +"completeTemplate" = "Semua" +"logLevel" = "Tingkat Log" +"logLevelDesc" = "Tingkat log untuk log kesalahan, menunjukkan informasi yang perlu dicatat." +"accessLog" = "Log Akses" +"accessLogDesc" = "Jalur file untuk log akses. Nilai khusus 'tidak ada' menonaktifkan log akses" + +[pages.xray.rules] +"first" = "Pertama" +"last" = "Terakhir" +"up" = "Naik" +"down" = "Turun" +"source" = "Sumber" +"dest" = "Tujuan" +"inbound" = "Masuk" +"outbound" = "Keluar" +"balancer" = "Pengimbang" +"info" = "Info" +"add" = "Tambahkan Aturan" +"edit" = "Edit Aturan" +"useComma" = "Item yang dipisahkan koma" + +[pages.xray.outbound] +"addOutbound" = "Tambahkan Keluar" +"addReverse" = "Tambahkan Revers" +"editOutbound" = "Edit Keluar" +"editReverse" = "Edit Revers" +"tag" = "Tag" +"tagDesc" = "Tag Unik" +"address" = "Alamat" +"reverse" = "Revers" +"domain" = "Domain" +"type" = "Tipe" +"bridge" = "Jembatan" +"portal" = "Portal" +"intercon" = "Interkoneksi" + +[pages.xray.balancer] +"addBalancer" = "Tambahkan Penyeimbang" +"editBalancer" = "Sunting Penyeimbang" +"balancerStrategy" = "Strategi" +"balancerSelectors" = "Penyeleksi" +"tag" = "Menandai" +"tagDesc" = "Label Unik" +"balancerDesc" = "BalancerTag dan outboundTag tidak dapat digunakan secara bersamaan. Jika digunakan secara bersamaan, hanya outboundTag yang akan berfungsi." + +[pages.xray.wireguard] +"secretKey" = "Kunci Rahasia" +"publicKey" = "Kunci Publik" +"allowedIPs" = "IP yang Diizinkan" +"endpoint" = "Titik Akhir" +"psk" = "Kunci Pra-Bagi" +"domainStrategy" = "Strategi Domain" + +[pages.settings.security] +"admin" = "Admin" +"secret" = "Token Rahasia" +"loginSecurity" = "Login Aman" +"loginSecurityDesc" = "Menambahkan lapisan otentikasi tambahan untuk memberikan keamanan lebih." +"secretToken" = "Token Rahasia" +"secretTokenDesc" = "Simpan token ini dengan aman di tempat yang aman. Token ini diperlukan untuk login dan tidak dapat dipulihkan." + +[pages.settings.toasts] +"modifySettings" = "Ubah Pengaturan" +"getSettings" = "Dapatkan Pengaturan" +"modifyUser" = "Ubah Admin" +"originalUserPassIncorrect" = "Username atau password saat ini tidak valid" +"userPassMustBeNotEmpty" = "Username dan password baru tidak boleh kosong" + +[tgbot] +"keyboardClosed" = "❌ Papan ketik kustom ditutup!" +"noResult" = "❗ Tidak ada hasil!" +"noQuery" = "❌ Permintaan tidak ditemukan! Harap gunakan perintah lagi!" +"wentWrong" = "❌ Ada yang salah!" +"noIpRecord" = "❗ Tidak ada Catatan IP!" +"noInbounds" = "❗ Tidak ada masuk ditemukan!" +"unlimited" = "♾ Tak terbatas" +"add" = "Tambah" +"month" = "Bulan" +"months" = "Bulan" +"day" = "Hari" +"days" = "Hari" +"hours" = "Jam" +"unknown" = "Tidak diketahui" +"inbounds" = "Masuk" +"clients" = "Klien" +"offline" = "🔴 Offline" +"online" = "🟢 Online" + +[tgbot.commands] +"unknown" = "❗ Perintah tidak dikenal." +"pleaseChoose" = "👇 Harap pilih:\r\n" +"help" = "🤖 Selamat datang di bot ini! Ini dirancang untuk menyediakan data tertentu dari panel web dan memungkinkan Anda melakukan modifikasi sesuai kebutuhan.\r\n\r\n" +"start" = "👋 Halo {{ .Firstname }}.\r\n" +"welcome" = "🤖 Selamat datang di {{.Hostname }} bot managemen.\r\n" +"status" = "✅ Bot dalam keadaan baik!" +"usage" = "❗ Harap berikan teks untuk mencari!" +"getID" = "🆔 ID Anda:{{.ID }}" +"helpAdminCommands" = "Untuk mencari email klien:\r\n/usage [Email]\r\n\r\nUntuk mencari masuk (dengan statistik klien):\r\n/inbound [Remark]" +"helpClientCommands" = "Untuk mencari statistik, gunakan perintah berikut:\r\n\r\n/usage [Email]" + +[tgbot.messages] +"cpuThreshold" = "🔴 Beban CPU {{ .Percent }}% melebihi batas {{ .Threshold }}%" +"selectUserFailed" = "❌ Kesalahan dalam pemilihan pengguna!" +"userSaved" = "✅ Pengguna Telegram tersimpan." +"loginSuccess" = "✅ Berhasil masuk ke panel.\r\n" +"loginFailed" = "❗️ Gagal masuk ke panel.\r\n" +"report" = "🕰 Laporan Terjadwal: {{ .RunTime }}\r\n" +"datetime" = "⏰ Tanggal & Waktu: {{ .DateTime }}\r\n" +"hostname" = "💻 Host: {{ .Hostname }}\r\n" +"version" = "🚀 Versi 3X-UI: {{ .Version }}\r\n" +"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n" +"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n" +"ip" = "🌐 IP: {{ .IP }}\r\n" +"ips" = "🔢 IP:\r\n{{ .IPs }}\r\n" +"serverUpTime" = "⏳ Waktu Aktif: {{ .UpTime }} {{ .Unit }}\r\n" +"serverLoad" = "📈 Beban Sistem: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n" +"serverMemory" = "📋 RAM: {{ .Current }}/{{ .Total }}\r\n" +"tcpCount" = "🔹 TCP: {{ .Count }}\r\n" +"udpCount" = "🔸 UDP: {{ .Count }}\r\n" +"traffic" = "🚦 Lalu Lintas: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n" +"xrayStatus" = "ℹ️ Status: {{ .State }}\r\n" +"username" = "👤 Nama Pengguna: {{ .Username }}\r\n" +"time" = "⏰ Waktu: {{ .Time }}\r\n" +"inbound" = "📍 Inbound: {{ .Remark }}\r\n" +"port" = "🔌 Port: {{ .Port }}\r\n" +"expire" = "📅 Tanggal Kadaluarsa: {{ .Time }}\r\n" +"expireIn" = "📅 Kadaluarsa Dalam: {{ .Time }}\r\n" +"active" = "💡 Aktif: {{ .Enable }}\r\n" +"enabled" = "🚨 Diaktifkan: {{ .Enable }}\r\n" +"online" = "🌐 Status Koneksi: {{ .Status }}\r\n" +"email" = "📧 Email: {{ .Email }}\r\n" +"upload" = "🔼 Unggah: ↑{{ .Upload }}\r\n" +"download" = "🔽 Unduh: ↓{{ .Download }}\r\n" +"total" = "📊 Total: ↑↓{{ .UpDown }} / {{ .Total }}\r\n" +"TGUser" = "👤 Pengguna Telegram: {{ .TelegramID }}\r\n" +"exhaustedMsg" = "🚨 Habis {{ .Type }}:\r\n" +"exhaustedCount" = "🚨 Jumlah Habis {{ .Type }}:\r\n" +"onlinesCount" = "🌐 Klien Online: {{ .Count }}\r\n" +"disabled" = "🛑 Dinonaktifkan: {{ .Disabled }}\r\n" +"depleteSoon" = "🔜 Habis Sebentar: {{ .Deplete }}\r\n\r\n" +"backupTime" = "🗄 Waktu Backup: {{ .Time }}\r\n" +"refreshedOn" = "\r\n📋🔄 Diperbarui Pada: {{ .Time }}\r\n\r\n" +"yes" = "✅ Ya" +"no" = "❌ Tidak" + +[tgbot.buttons] +"closeKeyboard" = "❌ Tutup Papan Ketik" +"cancel" = "❌ Batal" +"cancelReset" = "❌ Batal Reset" +"cancelIpLimit" = "❌ Batal Batas IP" +"confirmResetTraffic" = "✅ Konfirmasi Reset Lalu Lintas?" +"confirmClearIps" = "✅ Konfirmasi Hapus IPs?" +"confirmRemoveTGUser" = "✅ Konfirmasi Hapus Pengguna Telegram?" +"confirmToggle" = "✅ Konfirmasi Aktifkan/Nonaktifkan Pengguna?" +"dbBackup" = "Dapatkan Cadangan DB" +"serverUsage" = "Penggunaan Server" +"getInbounds" = "Dapatkan Inbounds" +"depleteSoon" = "Habis Sebentar" +"clientUsage" = "Dapatkan Penggunaan" +"onlines" = "Klien Online" +"commands" = "Perintah" +"refresh" = "🔄 Perbarui" +"clearIPs" = "❌ Hapus IPs" +"removeTGUser" = "❌ Hapus Pengguna Telegram" +"selectTGUser" = "👤 Pilih Pengguna Telegram" +"selectOneTGUser" = "👤 Pilih Pengguna Telegram:" +"resetTraffic" = "📈 Reset Lalu Lintas" +"resetExpire" = "📅 Ubah Tanggal Kadaluarsa" +"ipLog" = "🔢 Log IP" +"ipLimit" = "🔢 Batas IP" +"setTGUser" = "👤 Set Pengguna Telegram" +"toggle" = "🔘 Aktifkan / Nonaktifkan" +"custom" = "🔢 Kustom" +"confirmNumber" = "✅ Konfirmasi: {{ .Num }}" +"confirmNumberAdd" = "✅ Konfirmasi menambahkan: {{ .Num }}" +"limitTraffic" = "🚧 Batas Lalu Lintas" +"getBanLogs" = "Dapatkan Log Pemblokiran" + +[tgbot.answers] +"successfulOperation" = "✅ Operasi berhasil!" +"errorOperation" = "❗ Kesalahan dalam operasi." +"getInboundsFailed" = "❌ Gagal mendapatkan inbounds." +"canceled" = "❌ {{ .Email }}: Operasi dibatalkan." +"clientRefreshSuccess" = "✅ {{ .Email }}: Klien diperbarui dengan berhasil." +"IpRefreshSuccess" = "✅ {{ .Email }}: IP diperbarui dengan berhasil." +"TGIdRefreshSuccess" = "✅ {{ .Email }}: Pengguna Telegram Klien diperbarui dengan berhasil." +"resetTrafficSuccess" = "✅ {{ .Email }}: Lalu lintas direset dengan berhasil." +"setTrafficLimitSuccess" = "✅ {{ .Email }}: Batas lalu lintas disimpan dengan berhasil." +"expireResetSuccess" = "✅ {{ .Email }}: Hari kadaluarsa direset dengan berhasil." +"resetIpSuccess" = "✅ {{ .Email }}: Batas IP {{ .Count }} disimpan dengan berhasil." +"clearIpSuccess" = "✅ {{ .Email }}: IP dihapus dengan berhasil." +"getIpLog" = "✅ {{ .Email }}: Dapatkan Log IP." +"getUserInfo" = "✅ {{ .Email }}: Dapatkan Info Pengguna Telegram." +"removedTGUserSuccess" = "✅ {{ .Email }}: Pengguna Telegram dihapus dengan berhasil." +"enableSuccess" = "✅ {{ .Email }}: Diaktifkan dengan berhasil." +"disableSuccess" = "✅ {{ .Email }}: Dinonaktifkan dengan berhasil." +"askToAddUserId" = "Konfigurasi Anda tidak ditemukan!\r\nSilakan minta admin Anda untuk menggunakan ID Telegram Anda dalam konfigurasi Anda.\r\n\r\nID Pengguna Anda: {{ .TgUserID }}" diff --git a/web/translation/translate.ru_RU.toml b/web/translation/translate.ru_RU.toml index 2e663e4c..8badec04 100644 --- a/web/translation/translate.ru_RU.toml +++ b/web/translation/translate.ru_RU.toml @@ -76,7 +76,7 @@ "title" = "Статус системы" "memory" = "Память" "hard" = "Жесткий диск" -"xrayStatus" = "Статус" +"xrayStatus" = "Xray" "stopXray" = "Остановить" "restartXray" = "Перезапустить" "xraySwitch" = "Версия" @@ -388,10 +388,15 @@ "Inbounds" = "Входящие" "InboundsDesc" = "Изменение шаблона конфигурации для подключения определенных пользователей" "Outbounds" = "Исходящие" +"Balancers" = "Балансиры" "OutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие пути для этого сервера" "Routings" = "Правила маршрутизации" "RoutingsDesc" = "Важен приоритет каждого правила!" "completeTemplate" = "Все" +"logLevel" = "Уровень журнала" +"logLevelDesc" = "Уровень журнала для журналов ошибок, указывающий информацию, которую необходимо записать." +"accessLog" = "Журнал доступа" +"accessLogDesc" = "Путь к файлу журнала доступа. Специальное значение «none» отключило журналы доступа." [pages.xray.rules] "first" = "Первый" @@ -402,6 +407,7 @@ "dest" = "Пункт назначения" "inbound" = "Входящий" "outbound" = "Исходящий" +"balancer" = "балансир" "info" = "Информация" "add" = "Добавить правило" "edit" = "Редактировать правило" @@ -422,6 +428,15 @@ "portal" = "Портал" "intercon" = "Соединение" +[pages.xray.balancer] +"addBalancer" = "Добавить балансир" +"editBalancer" = "Редактировать балансир" +"balancerStrategy" = "Стратегия" +"balancerSelectors" = "Селекторы" +"tag" = "Тег" +"tagDesc" = "уникальный тег" +"balancerDesc" = "Невозможно одновременно использовать balancerTag и outboundTag. При одновременном использовании будет работать только outboundTag." + [pages.xray.wireguard] "secretKey" = "Секретный ключ" "publicKey" = "Открытый ключ" diff --git a/web/translation/translate.vi_VN.toml b/web/translation/translate.vi_VN.toml index 4c40adca..ad987cf3 100644 --- a/web/translation/translate.vi_VN.toml +++ b/web/translation/translate.vi_VN.toml @@ -20,7 +20,7 @@ "check" = "Kiểm tra" "indefinite" = "Không xác định" "unlimited" = "Không giới hạn" -"none" = "Không có" +"none" = "None" "qrCode" = "Mã QR" "info" = "Thông tin thêm" "edit" = "Chỉnh sửa" @@ -76,7 +76,7 @@ "title" = "Trạng thái hệ thống" "memory" = "Ram" "hard" = "Dung lượng" -"xrayStatus" = "Trạng thái Xray" +"xrayStatus" = "Xray" "stopXray" = "Dừng lại" "restartXray" = "Khởi động lại" "xraySwitch" = "Phiên bản" @@ -388,10 +388,15 @@ "Inbounds" = "Đầu vào" "InboundsDesc" = "Thay đổi mẫu cấu hình để chấp nhận các máy khách cụ thể." "Outbounds" = "Đầu ra" +"Balancers" = "Cân bằng" "OutboundsDesc" = "Thay đổi mẫu cấu hình để xác định các cách ra đi cho máy chủ này." "Routings" = "Quy tắc định tuyến" "RoutingsDesc" = "Mức độ ưu tiên của mỗi quy tắc đều quan trọng!" "completeTemplate" = "All" +"logLevel" = "Mức đăng nhập" +"logLevelDesc" = "Cấp độ nhật ký cho nhật ký lỗi, cho biết thông tin cần được ghi lại." +"accessLog" = "Nhật ký truy cập" +"accessLogDesc" = "Đường dẫn tệp cho nhật ký truy cập. Nhật ký truy cập bị vô hiệu hóa có giá trị đặc biệt 'không'" [pages.xray.rules] "first" = "Đầu tiên" @@ -402,6 +407,7 @@ "dest" = "Đích" "inbound" = "Vào" "outbound" = "Ra" +"balancer" = "Cân bằng" "info" = "Thông tin" "add" = "Thêm quy tắc" "edit" = "Chỉnh sửa quy tắc" @@ -422,6 +428,15 @@ "portal" = "Cổng thông tin" "intercon" = "Kết nối" +[pages.xray.balancer] +"addBalancer" = "Thêm cân bằng" +"editBalancer" = "Chỉnh sửa cân bằng" +"balancerStrategy" = "Chiến lược" +"balancerSelectors" = "Bộ chọn" +"tag" = "Thẻ" +"tagDesc" = "thẻ duy nhất" +"balancerDesc" = "Không thể sử dụng balancerTag và outboundTag cùng một lúc. Nếu sử dụng cùng lúc thì chỉ outboundTag mới hoạt động." + [pages.xray.wireguard] "secretKey" = "Khoá bí mật" "publicKey" = "Khóa công khai" diff --git a/web/translation/translate.zh_Hans.toml b/web/translation/translate.zh_Hans.toml index a8cd3aff..7a10e65d 100644 --- a/web/translation/translate.zh_Hans.toml +++ b/web/translation/translate.zh_Hans.toml @@ -76,7 +76,7 @@ "title" = "系统状态" "memory" = "内存" "hard" = "硬盘" -"xrayStatus" = "状态" +"xrayStatus" = "Xray" "stopXray" = "停止" "restartXray" = "重启" "xraySwitch" = "版本" @@ -388,10 +388,15 @@ "Inbounds" = "入站" "InboundsDesc" = "更改配置模板接受特殊客户端" "Outbounds" = "出站" +"Balancers" = "平衡器" "OutboundsDesc" = "更改配置模板定义此服务器的传出方式" "Routings" = "路由规则" "RoutingsDesc" = "每条规则的优先级都很重要" "completeTemplate" = "全部" +"logLevel" = "日志级别" +"logLevelDesc" = "错误日志的日志级别,表示需要记录的信息。" +"accessLog" = "访问日志" +"accessLogDesc" = "访问日志的文件路径。 特殊值“none”禁用访问日志" [pages.xray.rules] "first" = "第一个" @@ -402,6 +407,7 @@ "dest" = "目的地" "inbound" = "入站" "outbound" = "出站" +"balancer" = "平衡器" "info" = "信息" "add" = "添加规则" "edit" = "编辑规则" @@ -422,6 +428,15 @@ "portal" = "门户" "intercon" = "互连" +[pages.xray.balancer] +"addBalancer" = "添加平衡器" +"editBalancer" = "编辑平衡器" +"balancerStrategy" = "战略" +"balancerSelectors" = "选择器" +"tag" = "标签" +"tagDesc" = "唯一标记" +"balancerDesc" = "不能同时使用balancerTag和outboundTag。 如果同时使用,则只有outboundTag起作用。" + [pages.xray.wireguard] "secretKey" = "密钥" "publicKey" = "公钥" diff --git a/x-ui.sh b/x-ui.sh index ffb20f06..518b9232 100644 --- a/x-ui.sh +++ b/x-ui.sh @@ -150,6 +150,12 @@ custom_version() { eval $install_command } +# Function to handle the deletion of the script file +delete_script() { + rm "$0" # Remove the script file itself + exit 1 +} + uninstall() { confirm "Are you sure you want to uninstall the panel? xray will also uninstalled!" "n" if [[ $? != 0 ]]; then @@ -167,12 +173,13 @@ uninstall() { rm /usr/local/x-ui/ -rf echo "" - echo -e "Uninstalled Successfully, If you want to remove this script, then after exiting the script run ${green}rm /usr/bin/x-ui -f${plain} to delete it." + echo -e "Uninstalled Successfully.\n" + echo "If you need to install this panel again, you can use below command:" + echo -e "${green}bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)${plain}" echo "" - - if [[ $# == 0 ]]; then - before_show_menu - fi + # Trap the SIGTERM signal + trap delete_script SIGTERM + delete_script } reset_user() { @@ -483,6 +490,33 @@ show_xray_status() { fi } +firewall_menu() { + echo -e "${green}\t1.${plain} Install Firewall & open ports" + echo -e "${green}\t2.${plain} Allowed List" + echo -e "${green}\t3.${plain} Delete Ports from List" + echo -e "${green}\t4.${plain} Disable Firewall" + echo -e "${green}\t0.${plain} Back to Main Menu" + read -p "Choose an option: " choice + case "$choice" in + 0) + show_menu + ;; + 1) + open_ports + ;; + 2) + sudo ufw status + ;; + 3) + delete_ports + ;; + 4) + sudo ufw disable + ;; + *) echo "Invalid choice" ;; + esac +} + open_ports() { if ! command -v ufw &>/dev/null; then echo "ufw firewall is not installed. Installing now..." @@ -535,6 +569,37 @@ open_ports() { ufw status | grep $ports } +delete_ports() { + # Prompt the user to enter the ports they want to delete + read -p "Enter the ports you want to delete (e.g. 80,443,2053 or range 400-500): " ports + + # Check if the input is valid + if ! [[ $ports =~ ^([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))*$ ]]; then + echo "Error: Invalid input. Please enter a comma-separated list of ports or a range of ports (e.g. 80,443,2053 or 400-500)." >&2 + exit 1 + fi + + # Delete the specified ports using ufw + IFS=',' read -ra PORT_LIST <<<"$ports" + for port in "${PORT_LIST[@]}"; do + if [[ $port == *-* ]]; then + # Split the range into start and end ports + start_port=$(echo $port | cut -d'-' -f1) + end_port=$(echo $port | cut -d'-' -f2) + # Loop through the range and delete each port + for ((i = start_port; i <= end_port; i++)); do + ufw delete allow $i + done + else + ufw delete allow "$port" + fi + done + + # Confirm that the ports are deleted + echo "Deleted the specified ports:" + ufw status | grep $ports +} + update_geo() { local defaultBinFolder="/usr/local/x-ui/bin" read -p "Please enter x-ui bin folder path. Leave blank for default. (Default: '${defaultBinFolder}')" binFolder @@ -1124,10 +1189,10 @@ show_menu() { ${green}17.${plain} Cloudflare SSL Certificate ${green}18.${plain} IP Limit Management ${green}19.${plain} WARP Management + ${green}20.${plain} Firewall Management ———————————————— - ${green}20.${plain} Enable BBR - ${green}21.${plain} Update Geo Files - ${green}22.${plain} Active Firewall and open ports + ${green}21.${plain} Enable BBR + ${green}22.${plain} Update Geo Files ${green}23.${plain} Speedtest by Ookla " show_status @@ -1195,13 +1260,13 @@ show_menu() { warp_cloudflare ;; 20) - enable_bbr + firewall_menu ;; 21) - update_geo + enable_bbr ;; 22) - open_ports + update_geo ;; 23) run_speedtest diff --git a/xray/api.go b/xray/api.go index 36b19875..1ce5afa1 100644 --- a/xray/api.go +++ b/xray/api.go @@ -213,6 +213,7 @@ func (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) { continue } isInbound := matchs[1] == "inbound" + isOutbound := matchs[1] == "outbound" tag := matchs[2] isDown := matchs[3] == "downlink" if tag == "api" { @@ -221,8 +222,9 @@ func (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) { traffic, ok := tagTrafficMap[tag] if !ok { traffic = &Traffic{ - IsInbound: isInbound, - Tag: tag, + IsInbound: isInbound, + IsOutbound: isOutbound, + Tag: tag, } tagTrafficMap[tag] = traffic traffics = append(traffics, traffic) diff --git a/xray/log_writer.go b/xray/log_writer.go index 5fc6b3d1..53358ca2 100644 --- a/xray/log_writer.go +++ b/xray/log_writer.go @@ -31,7 +31,7 @@ func (lw *LogWriter) Write(m []byte) (n int, err error) { // Find level in [] startIndex := strings.Index(messageBody, "[") endIndex := strings.Index(messageBody, "]") - if startIndex != -1 && endIndex != -1 { + if startIndex != -1 && endIndex != -1 && startIndex < endIndex { level := strings.TrimSpace(messageBody[startIndex+1 : endIndex]) msgBody := "XRAY: " + strings.TrimSpace(messageBody[endIndex+1:]) diff --git a/xray/process.go b/xray/process.go index 093cf69d..e37a0649 100644 --- a/xray/process.go +++ b/xray/process.go @@ -41,10 +41,6 @@ func GetIPLimitLogPath() string { return config.GetLogFolder() + "/3xipl.log" } -func GetIPLimitPrevLogPath() string { - return config.GetLogFolder() + "/3xipl.prev.log" -} - func GetIPLimitBannedLogPath() string { return config.GetLogFolder() + "/3xipl-banned.log" } diff --git a/xray/traffic.go b/xray/traffic.go index a1ef5186..7b907bae 100644 --- a/xray/traffic.go +++ b/xray/traffic.go @@ -1,8 +1,9 @@ package xray type Traffic struct { - IsInbound bool - Tag string - Up int64 - Down int64 + IsInbound bool + IsOutbound bool + Tag string + Up int64 + Down int64 }