huge changes

This commit is contained in:
Ho3ein 2023-12-10 18:12:52 +03:30 committed by GitHub
commit e3f1d3c892
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
103 changed files with 52957 additions and 8027 deletions

View file

@ -311,6 +311,7 @@ Enter the user ID in input field number 4. The Telegram accounts with this id wi
| `POST` | `"/resetAllTraffics"` | Reset traffics of all inbounds |
| `POST` | `"/resetAllClientTraffics/:id"` | Reset traffics of all clients in an inbound |
| `POST` | `"/delDepletedClients/:id"` | Delete inbound depleted clients (-1: all) |
| `POST` | `"/onlines"` | Get Online users ( list of emails ) |
\*- The field `clientId` should be filled by:
@ -318,7 +319,9 @@ Enter the user ID in input field number 4. The Telegram accounts with this id wi
- `client.password` for TROJAN
- `client.email` for Shadowsocks
- [Postman Collection](https://gist.github.com/mehdikhody/9a862801a2e41f6b5fb6bbc7e1326044)
- [API Documentation](https://documenter.getpostman.com/view/16802678/2s9YkgD5jm)
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415%26entityType%3Dcollection%26workspaceId%3D2cd38c01-c851-4a15-a972-f181c23359d9)
</details>
# Environment Variables

View file

@ -112,3 +112,12 @@ func IsSQLiteDB(file io.ReaderAt) (bool, error) {
}
return bytes.Equal(buf, signature), nil
}
func Checkpoint() error {
// Update WAL
err := db.Exec("PRAGMA wal_checkpoint;").Error
if err != nil {
return err
}
return nil
}

View file

@ -83,4 +83,5 @@ type Client struct {
Enable bool `json:"enable" form:"enable"`
TgID string `json:"tgId" form:"tgId"`
SubID string `json:"subId" form:"subId"`
Reset int `json:"reset" form:"reset"`
}

View file

@ -1,5 +1,5 @@
---
version: "3.8"
version: "3"
services:
3x-ui:

3
go.mod
View file

@ -4,7 +4,7 @@ go 1.21.4
require (
github.com/Calidity/gin-sessions v1.3.1
github.com/Workiva/go-datastructures v1.1.1
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
@ -50,7 +50,6 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.2 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect
github.com/mattn/go-isatty v0.0.19 // indirect

48
go.sum
View file

@ -12,8 +12,6 @@ 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/Workiva/go-datastructures v1.1.1 h1:9G5u1UqKt6ABseAffHGNfbNQd7omRlWE5QaxNruzhE0=
github.com/Workiva/go-datastructures v1.1.1/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A=
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
@ -56,8 +54,11 @@ github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67d
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
@ -67,16 +68,21 @@ github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ4
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-playground/validator/v10 v10.15.5 h1:LEBecTWb/1j5TNY1YYG2RcOUN3R7NLylN+x8TTueE24=
github.com/go-playground/validator/v10 v10.15.5/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@ -137,6 +143,8 @@ github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/4
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@ -144,6 +152,7 @@ github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
@ -151,6 +160,7 @@ github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a h1:N9zuLhTvBSRt0g
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
@ -180,11 +190,12 @@ github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWEr
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@ -205,6 +216,8 @@ github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
@ -261,14 +274,14 @@ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
@ -288,7 +301,7 @@ github.com/xtls/reality v0.0.0-20231112171332-de1173cf2b19 h1:capMfFYRgH9BCLd6A3
github.com/xtls/reality v0.0.0-20231112171332-de1173cf2b19/go.mod h1:dm4y/1QwzjGaK17ofi0Vs6NpKAHegZky8qk6J2JJZAE=
github.com/xtls/xray-core v1.8.6 h1:tr3nk/fZnFfCsmgZv7B3RC72N5qUC88oMGVLlybDey8=
github.com/xtls/xray-core v1.8.6/go.mod h1:hj2EB8rtcLdlTC8zxiWm5xL+C0k2Aie9Pk0mXtDEP6U=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
@ -306,8 +319,7 @@ golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -316,7 +328,7 @@ golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUU
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -327,9 +339,8 @@ golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@ -342,7 +353,7 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -350,11 +361,9 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220804214406-8e32c043e418/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -366,6 +375,7 @@ golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -378,13 +388,11 @@ golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8=
golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20231022001213-2e0774f246fb h1:c5tyN8sSp8jSDxdCCDXVOpJwYXXhmTkNMt+g0zTSOic=
@ -411,11 +419,14 @@ google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
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=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@ -423,6 +434,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.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=

View file

@ -26,17 +26,15 @@ func InitLogger(level logging.Level) {
var format logging.Formatter
ppid := os.Getppid()
if ppid == 1 {
backend, err = logging.NewSyslogBackend("")
format = logging.MustStringFormatter(
`%{level} - %{message}`,
)
}
if err != nil || ppid != 1 {
backend, err = logging.NewSyslogBackend("")
if err != nil {
println(err)
backend = logging.NewLogBackend(os.Stderr, "", 0)
format = logging.MustStringFormatter(
`%{time:2006/01/02 15:04:05} %{level} - %{message}`,
)
}
if ppid > 0 && err != nil {
format = logging.MustStringFormatter(`%{time:2006/01/02 15:04:05} %{level} - %{message}`)
} else {
format = logging.MustStringFormatter(`%{level} - %{message}`)
}
backendFormatter := logging.NewBackendFormatter(backend, format)

View file

@ -19,8 +19,9 @@ import (
type SubService struct {
address string
showInfo bool
remarkModel string
inboundService service.InboundService
settingServics service.SettingService
settingService service.SettingService
}
func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string, []string, error) {
@ -34,6 +35,10 @@ func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string
if err != nil {
return nil, nil, err
}
s.remarkModel, err = s.settingService.GetRemarkModel()
if err != nil {
s.remarkModel = "-ieo"
}
for _, inbound := range inbounds {
clients, err := s.inboundService.GetClients(inbound)
if err != nil {
@ -53,6 +58,7 @@ func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string
json.Unmarshal([]byte(fallbackMaster.StreamSettings), &masterStream)
stream["security"] = masterStream["security"]
stream["tlsSettings"] = masterStream["tlsSettings"]
stream["externalProxy"] = masterStream["externalProxy"]
modifiedStream, _ := json.MarshalIndent(stream, "", " ")
inbound.StreamSettings = string(modifiedStream)
}
@ -87,7 +93,7 @@ func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string
}
}
headers = append(headers, fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000))
updateInterval, _ := s.settingServics.GetSubUpdates()
updateInterval, _ := s.settingService.GetSubUpdates()
headers = append(headers, fmt.Sprintf("%d", updateInterval))
headers = append(headers, subId)
return result, headers, nil
@ -96,7 +102,14 @@ func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
db := database.GetDB()
var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Preload("ClientStats").Where("settings like ? and enable = ?", fmt.Sprintf(`%%"subId": "%s"%%`, subId), true).Find(&inbounds).Error
err := db.Model(model.Inbound{}).Preload("ClientStats").Where(`id in (
SELECT DISTINCT inbounds.id
FROM inbounds,
JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client
WHERE
protocol in ('vmess','vless','trojan','shadowsocks')
AND JSON_EXTRACT(client.value, '$.subId') = ? AND enable = ?
)`, subId, true).Find(&inbounds).Error
if err != nil {
return nil, err
}
@ -196,7 +209,6 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
}
security, _ := stream["security"].(string)
var domains []interface{}
obj["tls"] = security
if security == "tls" {
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
@ -208,24 +220,18 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
}
obj["alpn"] = strings.Join(alpn, ",")
}
if sniValue, ok := searchKey(tlsSetting, "serverName"); ok {
obj["sni"], _ = sniValue.(string)
}
tlsSettings, _ := searchKey(tlsSetting, "settings")
if tlsSetting != nil {
if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
obj["sni"], _ = sniValue.(string)
}
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
obj["fp"], _ = fpValue.(string)
}
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
obj["allowInsecure"], _ = insecure.(bool)
}
if domainSettings, ok := searchKey(tlsSettings, "domains"); ok {
domains, _ = domainSettings.([]interface{})
}
}
serverName, _ := tlsSetting["serverName"].(string)
if serverName != "" {
obj["add"] = serverName
}
}
@ -239,16 +245,30 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
}
obj["id"] = clients[clientIndex].ID
if len(domains) > 0 {
externalProxies, _ := stream["externalProxy"].([]interface{})
if len(externalProxies) > 0 {
links := ""
for index, d := range domains {
domain := d.(map[string]interface{})
obj["ps"] = s.genRemark(inbound, email, domain["remark"].(string))
obj["add"] = domain["domain"].(string)
for index, externalProxy := range externalProxies {
ep, _ := externalProxy.(map[string]interface{})
newSecurity, _ := ep["forceTls"].(string)
newObj := map[string]interface{}{}
for key, value := range obj {
if !(newSecurity == "none" && (key == "alpn" || key == "sni" || key == "fp" || key == "allowInsecure")) {
newObj[key] = value
}
}
newObj["ps"] = s.genRemark(inbound, email, ep["remark"].(string))
newObj["add"] = ep["dest"].(string)
newObj["port"] = int(ep["port"].(float64))
if newSecurity != "same" {
newObj["tls"] = newSecurity
}
if index > 0 {
links += "\n"
}
jsonStr, _ := json.MarshalIndent(obj, "", " ")
jsonStr, _ := json.MarshalIndent(newObj, "", " ")
links += "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
}
return links
@ -323,7 +343,6 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
}
security, _ := stream["security"].(string)
var domains []interface{}
if security == "tls" {
params["security"] = "tls"
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
@ -335,11 +354,12 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
if len(alpn) > 0 {
params["alpn"] = strings.Join(alpn, ",")
}
if sniValue, ok := searchKey(tlsSetting, "serverName"); ok {
params["sni"], _ = sniValue.(string)
}
tlsSettings, _ := searchKey(tlsSetting, "settings")
if tlsSetting != nil {
if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
params["sni"], _ = sniValue.(string)
}
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
params["fp"], _ = fpValue.(string)
}
@ -348,19 +368,11 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
params["allowInsecure"] = "1"
}
}
if domainSettings, ok := searchKey(tlsSettings, "domains"); ok {
domains, _ = domainSettings.([]interface{})
}
}
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
params["flow"] = clients[clientIndex].Flow
}
serverName, _ := tlsSetting["serverName"].(string)
if serverName != "" {
address = serverName
}
}
if security == "reality" {
@ -389,11 +401,6 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
params["spx"] = spx
}
}
if serverName, ok := searchKey(realitySettings, "serverName"); ok {
if sname, ok := serverName.(string); ok && len(sname) > 0 {
address = sname
}
}
}
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
@ -412,7 +419,9 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
if len(alpn) > 0 {
params["alpn"] = strings.Join(alpn, ",")
}
if sniValue, ok := searchKey(xtlsSetting, "serverName"); ok {
params["sni"], _ = sniValue.(string)
}
xtlsSettings, _ := searchKey(xtlsSetting, "settings")
if xtlsSetting != nil {
if fpValue, ok := searchKey(xtlsSettings, "fingerprint"); ok {
@ -423,25 +432,55 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
params["allowInsecure"] = "1"
}
}
if sniValue, ok := searchKey(xtlsSettings, "serverName"); ok {
params["sni"], _ = sniValue.(string)
}
}
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
params["flow"] = clients[clientIndex].Flow
}
serverName, _ := xtlsSetting["serverName"].(string)
if serverName != "" {
address = serverName
}
}
if security != "tls" && security != "reality" && security != "xtls" {
params["security"] = "none"
}
externalProxies, _ := stream["externalProxy"].([]interface{})
if len(externalProxies) > 0 {
links := ""
for index, externalProxy := range externalProxies {
ep, _ := externalProxy.(map[string]interface{})
newSecurity, _ := ep["forceTls"].(string)
dest, _ := ep["dest"].(string)
port := int(ep["port"].(float64))
link := fmt.Sprintf("vless://%s@%s:%d", uuid, dest, port)
if newSecurity != "same" {
params["security"] = newSecurity
} else {
params["security"] = security
}
url, _ := url.Parse(link)
q := url.Query()
for k, v := range params {
if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp" || k == "allowInsecure")) {
q.Add(k, v)
}
}
// Set the new query values on the URL
url.RawQuery = q.Encode()
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
if index > 0 {
links += "\n"
}
links += url.String()
}
return links
}
link := fmt.Sprintf("vless://%s@%s:%d", uuid, address, port)
url, _ := url.Parse(link)
q := url.Query()
@ -453,20 +492,6 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
// Set the new query values on the URL
url.RawQuery = q.Encode()
if len(domains) > 0 {
links := ""
for index, d := range domains {
domain := d.(map[string]interface{})
url.Fragment = s.genRemark(inbound, email, domain["remark"].(string))
url.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port)
if index > 0 {
links += "\n"
}
links += url.String()
}
return links
}
url.Fragment = s.genRemark(inbound, email, "")
return url.String()
}
@ -534,7 +559,6 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
}
security, _ := stream["security"].(string)
var domains []interface{}
if security == "tls" {
params["security"] = "tls"
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
@ -546,11 +570,11 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
if len(alpn) > 0 {
params["alpn"] = strings.Join(alpn, ",")
}
if sniValue, ok := searchKey(tlsSetting, "serverName"); ok {
params["sni"], _ = sniValue.(string)
}
tlsSettings, _ := searchKey(tlsSetting, "settings")
if tlsSetting != nil {
if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
params["sni"], _ = sniValue.(string)
}
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
params["fp"], _ = fpValue.(string)
}
@ -559,14 +583,6 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
params["allowInsecure"] = "1"
}
}
if domainSettings, ok := searchKey(tlsSettings, "domains"); ok {
domains, _ = domainSettings.([]interface{})
}
}
serverName, _ := tlsSetting["serverName"].(string)
if serverName != "" {
address = serverName
}
}
@ -596,11 +612,6 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
params["spx"] = spx
}
}
if serverName, ok := searchKey(realitySettings, "serverName"); ok {
if sname, ok := serverName.(string); ok && len(sname) > 0 {
address = sname
}
}
}
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
@ -619,6 +630,9 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
if len(alpn) > 0 {
params["alpn"] = strings.Join(alpn, ",")
}
if sniValue, ok := searchKey(xtlsSetting, "serverName"); ok {
params["sni"], _ = sniValue.(string)
}
xtlsSettings, _ := searchKey(xtlsSetting, "settings")
if xtlsSetting != nil {
@ -630,25 +644,55 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
params["allowInsecure"] = "1"
}
}
if sniValue, ok := searchKey(xtlsSettings, "serverName"); ok {
params["sni"], _ = sniValue.(string)
}
}
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
params["flow"] = clients[clientIndex].Flow
}
serverName, _ := xtlsSetting["serverName"].(string)
if serverName != "" {
address = serverName
}
}
if security != "tls" && security != "reality" && security != "xtls" {
params["security"] = "none"
}
externalProxies, _ := stream["externalProxy"].([]interface{})
if len(externalProxies) > 0 {
links := ""
for index, externalProxy := range externalProxies {
ep, _ := externalProxy.(map[string]interface{})
newSecurity, _ := ep["forceTls"].(string)
dest, _ := ep["dest"].(string)
port := int(ep["port"].(float64))
link := fmt.Sprintf("trojan://%s@%s:%d", password, dest, port)
if newSecurity != "same" {
params["security"] = newSecurity
} else {
params["security"] = security
}
url, _ := url.Parse(link)
q := url.Query()
for k, v := range params {
if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp" || k == "allowInsecure")) {
q.Add(k, v)
}
}
// Set the new query values on the URL
url.RawQuery = q.Encode()
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
if index > 0 {
links += "\n"
}
links += url.String()
}
return links
}
link := fmt.Sprintf("trojan://%s@%s:%d", password, address, port)
url, _ := url.Parse(link)
@ -661,20 +705,6 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
// Set the new query values on the URL
url.RawQuery = q.Encode()
if len(domains) > 0 {
links := ""
for index, d := range domains {
domain := d.(map[string]interface{})
url.Fragment = s.genRemark(inbound, email, domain["remark"].(string))
url.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port)
if index > 0 {
links += "\n"
}
links += url.String()
}
return links
}
url.Fragment = s.genRemark(inbound, email, "")
return url.String()
}
@ -744,10 +774,78 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
}
}
security, _ := stream["security"].(string)
if security == "tls" {
params["security"] = "tls"
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
alpns, _ := tlsSetting["alpn"].([]interface{})
var alpn []string
for _, a := range alpns {
alpn = append(alpn, a.(string))
}
if len(alpn) > 0 {
params["alpn"] = strings.Join(alpn, ",")
}
if sniValue, ok := searchKey(tlsSetting, "serverName"); ok {
params["sni"], _ = sniValue.(string)
}
tlsSettings, _ := searchKey(tlsSetting, "settings")
if tlsSetting != nil {
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
params["fp"], _ = fpValue.(string)
}
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
if insecure.(bool) {
params["allowInsecure"] = "1"
}
}
}
}
encPart := fmt.Sprintf("%s:%s", method, clients[clientIndex].Password)
if method[0] == '2' {
encPart = fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password)
}
externalProxies, _ := stream["externalProxy"].([]interface{})
if len(externalProxies) > 0 {
links := ""
for index, externalProxy := range externalProxies {
ep, _ := externalProxy.(map[string]interface{})
newSecurity, _ := ep["forceTls"].(string)
dest, _ := ep["dest"].(string)
port := int(ep["port"].(float64))
link := fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), dest, port)
if newSecurity != "same" {
params["security"] = newSecurity
} else {
params["security"] = security
}
url, _ := url.Parse(link)
q := url.Query()
for k, v := range params {
if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp" || k == "allowInsecure")) {
q.Add(k, v)
}
}
// Set the new query values on the URL
url.RawQuery = q.Encode()
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
if index > 0 {
links += "\n"
}
links += url.String()
}
return links
}
link := fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port)
url, _ := url.Parse(link)
q := url.Query()
@ -758,22 +856,36 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
// Set the new query values on the URL
url.RawQuery = q.Encode()
url.Fragment = s.genRemark(inbound, email, "")
return url.String()
}
func (s *SubService) genRemark(inbound *model.Inbound, email string, extra string) string {
var remark []string
separationChar := string(s.remarkModel[0])
orderChars := s.remarkModel[1:]
orders := map[byte]string{
'i': "",
'e': "",
'o': "",
}
if len(email) > 0 {
if len(inbound.Remark) > 0 {
remark = append(remark, inbound.Remark)
orders['e'] = email
}
if len(inbound.Remark) > 0 {
orders['i'] = inbound.Remark
}
if len(extra) > 0 {
orders['e'] = extra
}
var remark []string
for i := 0; i < len(orderChars); i++ {
char := orderChars[i]
order, exists := orders[char]
if exists && order != "" {
remark = append(remark, order)
}
remark = append(remark, email)
if len(extra) > 0 {
remark = append(remark, extra)
}
} else {
return inbound.Remark
}
if s.showInfo {
@ -790,7 +902,7 @@ func (s *SubService) genRemark(inbound *model.Inbound, email string, extra strin
// Get remained days
if statsExist {
if !stats.Enable {
return fmt.Sprintf("⛔N/A-%s", strings.Join(remark, "-"))
return fmt.Sprintf("⛔N/A%s%s", separationChar, strings.Join(remark, separationChar))
}
if vol := stats.Total - (stats.Up + stats.Down); vol > 0 {
remark = append(remark, fmt.Sprintf("%s%s", common.FormatTraffic(vol), "📊"))
@ -804,7 +916,7 @@ func (s *SubService) genRemark(inbound *model.Inbound, email string, extra strin
}
}
}
return strings.Join(remark, " : ")
return strings.Join(remark, separationChar)
}
func searchKey(data interface{}, key string) (interface{}, bool) {

Binary file not shown.

File diff suppressed because one or more lines are too long

View file

@ -1,2 +0,0 @@
@import "../lib/style/index.less";
@import "../lib/style/components.less";

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,7 @@
@import "../lib/style/index.less";
@import "../lib/style/components.less";
@green-6: #008771;
@primary-color: @green-6;
@border-radius-base: 1rem;
@progress-remaining-color: #EDEDED;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,344 @@
/* BASICS */
.CodeMirror {
/* Set height, width, borders, and global font properties here */
font-family: monospace;
height: 300px;
color: black;
direction: ltr;
}
/* PADDING */
.CodeMirror-lines {
padding: 4px 0; /* Vertical padding around content */
}
.CodeMirror pre.CodeMirror-line,
.CodeMirror pre.CodeMirror-line-like {
padding: 0 4px; /* Horizontal padding of content */
}
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
background-color: white; /* The little square between H and V scrollbars */
}
/* GUTTER */
.CodeMirror-gutters {
border-right: 1px solid #ddd;
background-color: #f7f7f7;
white-space: nowrap;
}
.CodeMirror-linenumbers {}
.CodeMirror-linenumber {
padding: 0 3px 0 5px;
min-width: 20px;
text-align: right;
color: #999;
white-space: nowrap;
}
.CodeMirror-guttermarker { color: black; }
.CodeMirror-guttermarker-subtle { color: #999; }
/* CURSOR */
.CodeMirror-cursor {
border-left: 1px solid black;
border-right: none;
width: 0;
}
/* Shown when moving in bi-directional text */
.CodeMirror div.CodeMirror-secondarycursor {
border-left: 1px solid silver;
}
.cm-fat-cursor .CodeMirror-cursor {
width: auto;
border: 0 !important;
background: #7e7;
}
.cm-fat-cursor div.CodeMirror-cursors {
z-index: 1;
}
.cm-fat-cursor .CodeMirror-line::selection,
.cm-fat-cursor .CodeMirror-line > span::selection,
.cm-fat-cursor .CodeMirror-line > span > span::selection { background: transparent; }
.cm-fat-cursor .CodeMirror-line::-moz-selection,
.cm-fat-cursor .CodeMirror-line > span::-moz-selection,
.cm-fat-cursor .CodeMirror-line > span > span::-moz-selection { background: transparent; }
.cm-fat-cursor { caret-color: transparent; }
@-moz-keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
@-webkit-keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
@keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
/* Can style cursor different in overwrite (non-insert) mode */
.CodeMirror-overwrite .CodeMirror-cursor {}
.cm-tab { display: inline-block; text-decoration: inherit; }
.CodeMirror-rulers {
position: absolute;
left: 0; right: 0; top: -50px; bottom: 0;
overflow: hidden;
}
.CodeMirror-ruler {
border-left: 1px solid #ccc;
top: 0; bottom: 0;
position: absolute;
}
/* DEFAULT THEME */
.cm-s-default .cm-header {color: blue;}
.cm-s-default .cm-quote {color: #090;}
.cm-negative {color: #d44;}
.cm-positive {color: #292;}
.cm-header, .cm-strong {font-weight: bold;}
.cm-em {font-style: italic;}
.cm-link {text-decoration: underline;}
.cm-strikethrough {text-decoration: line-through;}
.cm-s-default .cm-keyword {color: #708;}
.cm-s-default .cm-atom {color: #219;}
.cm-s-default .cm-number {color: #164;}
.cm-s-default .cm-def {color: #00f;}
.cm-s-default .cm-variable,
.cm-s-default .cm-punctuation,
.cm-s-default .cm-property,
.cm-s-default .cm-operator {}
.cm-s-default .cm-variable-2 {color: #05a;}
.cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;}
.cm-s-default .cm-comment {color: #a50;}
.cm-s-default .cm-string {color: #a11;}
.cm-s-default .cm-string-2 {color: #f50;}
.cm-s-default .cm-meta {color: #555;}
.cm-s-default .cm-qualifier {color: #555;}
.cm-s-default .cm-builtin {color: #30a;}
.cm-s-default .cm-bracket {color: #997;}
.cm-s-default .cm-tag {color: #170;}
.cm-s-default .cm-attribute {color: #00c;}
.cm-s-default .cm-hr {color: #999;}
.cm-s-default .cm-link {color: #00c;}
.cm-s-default .cm-error {color: #f00;}
.cm-invalidchar {color: #f00;}
.CodeMirror-composing { border-bottom: 2px solid; }
/* Default styles for common addons */
div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;}
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;}
.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
.CodeMirror-activeline-background {background: #e8f2ff;}
/* STOP */
/* The rest of this file contains styles related to the mechanics of
the editor. You probably shouldn't touch them. */
.CodeMirror {
position: relative;
overflow: hidden;
background: white;
}
.CodeMirror-scroll {
overflow: scroll !important; /* Things will break if this is overridden */
/* 50px is the magic margin used to hide the element's real scrollbars */
/* See overflow: hidden in .CodeMirror */
margin-bottom: -50px; margin-right: -50px;
padding-bottom: 50px;
height: 100%;
outline: none; /* Prevent dragging from highlighting the element */
position: relative;
z-index: 0;
}
.CodeMirror-sizer {
position: relative;
border-right: 50px solid transparent;
}
/* The fake, visible scrollbars. Used to force redraw during scrolling
before actual scrolling happens, thus preventing shaking and
flickering artifacts. */
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
position: absolute;
z-index: 6;
display: none;
outline: none;
}
.CodeMirror-vscrollbar {
right: 0; top: 0;
overflow-x: hidden;
overflow-y: scroll;
}
.CodeMirror-hscrollbar {
bottom: 0; left: 0;
overflow-y: hidden;
overflow-x: scroll;
}
.CodeMirror-scrollbar-filler {
right: 0; bottom: 0;
}
.CodeMirror-gutter-filler {
left: 0; bottom: 0;
}
.CodeMirror-gutters {
position: absolute; left: 0; top: 0;
min-height: 100%;
z-index: 3;
}
.CodeMirror-gutter {
white-space: normal;
height: 100%;
display: inline-block;
vertical-align: top;
margin-bottom: -50px;
}
.CodeMirror-gutter-wrapper {
position: absolute;
z-index: 4;
background: none !important;
border: none !important;
}
.CodeMirror-gutter-background {
position: absolute;
top: 0; bottom: 0;
z-index: 4;
}
.CodeMirror-gutter-elt {
position: absolute;
cursor: default;
z-index: 4;
}
.CodeMirror-gutter-wrapper ::selection { background-color: transparent }
.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent }
.CodeMirror-lines {
cursor: text;
min-height: 1px; /* prevents collapsing before first draw */
}
.CodeMirror pre.CodeMirror-line,
.CodeMirror pre.CodeMirror-line-like {
/* Reset some styles that the rest of the page might have set */
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
border-width: 0;
background: transparent;
font-family: inherit;
font-size: inherit;
margin: 0;
white-space: pre;
word-wrap: normal;
line-height: inherit;
color: inherit;
z-index: 2;
position: relative;
overflow: visible;
-webkit-tap-highlight-color: transparent;
-webkit-font-variant-ligatures: contextual;
font-variant-ligatures: contextual;
}
.CodeMirror-wrap pre.CodeMirror-line,
.CodeMirror-wrap pre.CodeMirror-line-like {
word-wrap: break-word;
white-space: pre-wrap;
word-break: normal;
}
.CodeMirror-linebackground {
position: absolute;
left: 0; right: 0; top: 0; bottom: 0;
z-index: 0;
}
.CodeMirror-linewidget {
position: relative;
z-index: 2;
padding: 0.1px; /* Force widget margins to stay inside of the container */
}
.CodeMirror-widget {}
.CodeMirror-rtl pre { direction: rtl; }
.CodeMirror-code {
outline: none;
}
/* Force content-box sizing for the elements where we expect it */
.CodeMirror-scroll,
.CodeMirror-sizer,
.CodeMirror-gutter,
.CodeMirror-gutters,
.CodeMirror-linenumber {
-moz-box-sizing: content-box;
box-sizing: content-box;
}
.CodeMirror-measure {
position: absolute;
width: 100%;
height: 0;
overflow: hidden;
visibility: hidden;
}
.CodeMirror-cursor {
position: absolute;
pointer-events: none;
}
.CodeMirror-measure pre { position: static; }
div.CodeMirror-cursors {
visibility: hidden;
position: relative;
z-index: 3;
}
div.CodeMirror-dragcursors {
visibility: visible;
}
.CodeMirror-focused div.CodeMirror-cursors {
visibility: visible;
}
.CodeMirror-selected { background: #d9d9d9; }
.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
.CodeMirror-crosshair { cursor: crosshair; }
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
.cm-searching {
background-color: #ffa;
background-color: rgba(255, 255, 0, .4);
}
/* Used to force a border model for a node */
.cm-force-border { padding-right: .1px; }
@media print {
/* Hide the cursor when printing */
.CodeMirror div.CodeMirror-cursors {
visibility: hidden;
}
}
/* See issue #2901 */
.cm-tab-wrap-hack:after { content: ''; }
/* Help users use markselection to safely style text background */
span.CodeMirror-selectedtext { background: none; }

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,119 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
function bracketFolding(pairs) {
return function(cm, start) {
var line = start.line, lineText = cm.getLine(line);
function findOpening(pair) {
var tokenType;
for (var at = start.ch, pass = 0;;) {
var found = at <= 0 ? -1 : lineText.lastIndexOf(pair[0], at - 1);
if (found == -1) {
if (pass == 1) break;
pass = 1;
at = lineText.length;
continue;
}
if (pass == 1 && found < start.ch) break;
tokenType = cm.getTokenTypeAt(CodeMirror.Pos(line, found + 1));
if (!/^(comment|string)/.test(tokenType)) return {ch: found + 1, tokenType: tokenType, pair: pair};
at = found - 1;
}
}
function findRange(found) {
var count = 1, lastLine = cm.lastLine(), end, startCh = found.ch, endCh
outer: for (var i = line; i <= lastLine; ++i) {
var text = cm.getLine(i), pos = i == line ? startCh : 0;
for (;;) {
var nextOpen = text.indexOf(found.pair[0], pos), nextClose = text.indexOf(found.pair[1], pos);
if (nextOpen < 0) nextOpen = text.length;
if (nextClose < 0) nextClose = text.length;
pos = Math.min(nextOpen, nextClose);
if (pos == text.length) break;
if (cm.getTokenTypeAt(CodeMirror.Pos(i, pos + 1)) == found.tokenType) {
if (pos == nextOpen) ++count;
else if (!--count) { end = i; endCh = pos; break outer; }
}
++pos;
}
}
if (end == null || line == end) return null
return {from: CodeMirror.Pos(line, startCh),
to: CodeMirror.Pos(end, endCh)};
}
var found = []
for (var i = 0; i < pairs.length; i++) {
var open = findOpening(pairs[i])
if (open) found.push(open)
}
found.sort(function(a, b) { return a.ch - b.ch })
for (var i = 0; i < found.length; i++) {
var range = findRange(found[i])
if (range) return range
}
return null
}
}
CodeMirror.registerHelper("fold", "brace", bracketFolding([["{", "}"], ["[", "]"]]));
CodeMirror.registerHelper("fold", "brace-paren", bracketFolding([["{", "}"], ["[", "]"], ["(", ")"]]));
CodeMirror.registerHelper("fold", "import", function(cm, start) {
function hasImport(line) {
if (line < cm.firstLine() || line > cm.lastLine()) return null;
var start = cm.getTokenAt(CodeMirror.Pos(line, 1));
if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1));
if (start.type != "keyword" || start.string != "import") return null;
// Now find closing semicolon, return its position
for (var i = line, e = Math.min(cm.lastLine(), line + 10); i <= e; ++i) {
var text = cm.getLine(i), semi = text.indexOf(";");
if (semi != -1) return {startCh: start.end, end: CodeMirror.Pos(i, semi)};
}
}
var startLine = start.line, has = hasImport(startLine), prev;
if (!has || hasImport(startLine - 1) || ((prev = hasImport(startLine - 2)) && prev.end.line == startLine - 1))
return null;
for (var end = has.end;;) {
var next = hasImport(end.line + 1);
if (next == null) break;
end = next.end;
}
return {from: cm.clipPos(CodeMirror.Pos(startLine, has.startCh + 1)), to: end};
});
CodeMirror.registerHelper("fold", "include", function(cm, start) {
function hasInclude(line) {
if (line < cm.firstLine() || line > cm.lastLine()) return null;
var start = cm.getTokenAt(CodeMirror.Pos(line, 1));
if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1));
if (start.type == "meta" && start.string.slice(0, 8) == "#include") return start.start + 8;
}
var startLine = start.line, has = hasInclude(startLine);
if (has == null || hasInclude(startLine - 1) != null) return null;
for (var end = startLine;;) {
var next = hasInclude(end + 1);
if (next == null) break;
++end;
}
return {from: CodeMirror.Pos(startLine, has + 1),
to: cm.clipPos(CodeMirror.Pos(end))};
});
});

View file

@ -0,0 +1,159 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
function doFold(cm, pos, options, force) {
if (options && options.call) {
var finder = options;
options = null;
} else {
var finder = getOption(cm, options, "rangeFinder");
}
if (typeof pos == "number") pos = CodeMirror.Pos(pos, 0);
var minSize = getOption(cm, options, "minFoldSize");
function getRange(allowFolded) {
var range = finder(cm, pos);
if (!range || range.to.line - range.from.line < minSize) return null;
if (force === "fold") return range;
var marks = cm.findMarksAt(range.from);
for (var i = 0; i < marks.length; ++i) {
if (marks[i].__isFold) {
if (!allowFolded) return null;
range.cleared = true;
marks[i].clear();
}
}
return range;
}
var range = getRange(true);
if (getOption(cm, options, "scanUp")) while (!range && pos.line > cm.firstLine()) {
pos = CodeMirror.Pos(pos.line - 1, 0);
range = getRange(false);
}
if (!range || range.cleared || force === "unfold") return;
var myWidget = makeWidget(cm, options, range);
CodeMirror.on(myWidget, "mousedown", function(e) {
myRange.clear();
CodeMirror.e_preventDefault(e);
});
var myRange = cm.markText(range.from, range.to, {
replacedWith: myWidget,
clearOnEnter: getOption(cm, options, "clearOnEnter"),
__isFold: true
});
myRange.on("clear", function(from, to) {
CodeMirror.signal(cm, "unfold", cm, from, to);
});
CodeMirror.signal(cm, "fold", cm, range.from, range.to);
}
function makeWidget(cm, options, range) {
var widget = getOption(cm, options, "widget");
if (typeof widget == "function") {
widget = widget(range.from, range.to);
}
if (typeof widget == "string") {
var text = document.createTextNode(widget);
widget = document.createElement("span");
widget.appendChild(text);
widget.className = "CodeMirror-foldmarker";
} else if (widget) {
widget = widget.cloneNode(true)
}
return widget;
}
// Clumsy backwards-compatible interface
CodeMirror.newFoldFunction = function(rangeFinder, widget) {
return function(cm, pos) { doFold(cm, pos, {rangeFinder: rangeFinder, widget: widget}); };
};
// New-style interface
CodeMirror.defineExtension("foldCode", function(pos, options, force) {
doFold(this, pos, options, force);
});
CodeMirror.defineExtension("isFolded", function(pos) {
var marks = this.findMarksAt(pos);
for (var i = 0; i < marks.length; ++i)
if (marks[i].__isFold) return true;
});
CodeMirror.commands.toggleFold = function(cm) {
cm.foldCode(cm.getCursor());
};
CodeMirror.commands.fold = function(cm) {
cm.foldCode(cm.getCursor(), null, "fold");
};
CodeMirror.commands.unfold = function(cm) {
cm.foldCode(cm.getCursor(), { scanUp: false }, "unfold");
};
CodeMirror.commands.foldAll = function(cm) {
cm.operation(function() {
for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++)
cm.foldCode(CodeMirror.Pos(i, 0), { scanUp: false }, "fold");
});
};
CodeMirror.commands.unfoldAll = function(cm) {
cm.operation(function() {
for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++)
cm.foldCode(CodeMirror.Pos(i, 0), { scanUp: false }, "unfold");
});
};
CodeMirror.registerHelper("fold", "combine", function() {
var funcs = Array.prototype.slice.call(arguments, 0);
return function(cm, start) {
for (var i = 0; i < funcs.length; ++i) {
var found = funcs[i](cm, start);
if (found) return found;
}
};
});
CodeMirror.registerHelper("fold", "auto", function(cm, start) {
var helpers = cm.getHelpers(start, "fold");
for (var i = 0; i < helpers.length; i++) {
var cur = helpers[i](cm, start);
if (cur) return cur;
}
});
var defaultOptions = {
rangeFinder: CodeMirror.fold.auto,
widget: "\u2194",
minFoldSize: 0,
scanUp: false,
clearOnEnter: true
};
CodeMirror.defineOption("foldOptions", null);
function getOption(cm, options, name) {
if (options && options[name] !== undefined)
return options[name];
var editorOptions = cm.options.foldOptions;
if (editorOptions && editorOptions[name] !== undefined)
return editorOptions[name];
return defaultOptions[name];
}
CodeMirror.defineExtension("foldOption", function(options, name) {
return getOption(this, options, name);
});
});

View file

@ -0,0 +1,20 @@
.CodeMirror-foldmarker {
color: blue;
text-shadow: #b9f 1px 1px 2px, #b9f -1px -1px 2px, #b9f 1px -1px 2px, #b9f -1px 1px 2px;
font-family: arial;
line-height: .3;
cursor: pointer;
}
.CodeMirror-foldgutter {
width: .7em;
}
.CodeMirror-foldgutter-open,
.CodeMirror-foldgutter-folded {
cursor: pointer;
}
.CodeMirror-foldgutter-open:after {
content: "\25BE";
}
.CodeMirror-foldgutter-folded:after {
content: "\25B8";
}

View file

@ -0,0 +1,169 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"), require("./foldcode"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror", "./foldcode"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
CodeMirror.defineOption("foldGutter", false, function(cm, val, old) {
if (old && old != CodeMirror.Init) {
cm.clearGutter(cm.state.foldGutter.options.gutter);
cm.state.foldGutter = null;
cm.off("gutterClick", onGutterClick);
cm.off("changes", onChange);
cm.off("viewportChange", onViewportChange);
cm.off("fold", onFold);
cm.off("unfold", onFold);
cm.off("swapDoc", onChange);
cm.off("optionChange", optionChange);
}
if (val) {
cm.state.foldGutter = new State(parseOptions(val));
updateInViewport(cm);
cm.on("gutterClick", onGutterClick);
cm.on("changes", onChange);
cm.on("viewportChange", onViewportChange);
cm.on("fold", onFold);
cm.on("unfold", onFold);
cm.on("swapDoc", onChange);
cm.on("optionChange", optionChange);
}
});
var Pos = CodeMirror.Pos;
function State(options) {
this.options = options;
this.from = this.to = 0;
}
function parseOptions(opts) {
if (opts === true) opts = {};
if (opts.gutter == null) opts.gutter = "CodeMirror-foldgutter";
if (opts.indicatorOpen == null) opts.indicatorOpen = "CodeMirror-foldgutter-open";
if (opts.indicatorFolded == null) opts.indicatorFolded = "CodeMirror-foldgutter-folded";
return opts;
}
function isFolded(cm, line) {
var marks = cm.findMarks(Pos(line, 0), Pos(line + 1, 0));
for (var i = 0; i < marks.length; ++i) {
if (marks[i].__isFold) {
var fromPos = marks[i].find(-1);
if (fromPos && fromPos.line === line)
return marks[i];
}
}
}
function marker(spec) {
if (typeof spec == "string") {
var elt = document.createElement("div");
elt.className = spec + " CodeMirror-guttermarker-subtle";
return elt;
} else {
return spec.cloneNode(true);
}
}
function updateFoldInfo(cm, from, to) {
var opts = cm.state.foldGutter.options, cur = from - 1;
var minSize = cm.foldOption(opts, "minFoldSize");
var func = cm.foldOption(opts, "rangeFinder");
// we can reuse the built-in indicator element if its className matches the new state
var clsFolded = typeof opts.indicatorFolded == "string" && classTest(opts.indicatorFolded);
var clsOpen = typeof opts.indicatorOpen == "string" && classTest(opts.indicatorOpen);
cm.eachLine(from, to, function(line) {
++cur;
var mark = null;
var old = line.gutterMarkers;
if (old) old = old[opts.gutter];
if (isFolded(cm, cur)) {
if (clsFolded && old && clsFolded.test(old.className)) return;
mark = marker(opts.indicatorFolded);
} else {
var pos = Pos(cur, 0);
var range = func && func(cm, pos);
if (range && range.to.line - range.from.line >= minSize) {
if (clsOpen && old && clsOpen.test(old.className)) return;
mark = marker(opts.indicatorOpen);
}
}
if (!mark && !old) return;
cm.setGutterMarker(line, opts.gutter, mark);
});
}
// copied from CodeMirror/src/util/dom.js
function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*") }
function updateInViewport(cm) {
var vp = cm.getViewport(), state = cm.state.foldGutter;
if (!state) return;
cm.operation(function() {
updateFoldInfo(cm, vp.from, vp.to);
});
state.from = vp.from; state.to = vp.to;
}
function onGutterClick(cm, line, gutter) {
var state = cm.state.foldGutter;
if (!state) return;
var opts = state.options;
if (gutter != opts.gutter) return;
var folded = isFolded(cm, line);
if (folded) folded.clear();
else cm.foldCode(Pos(line, 0), opts);
}
function optionChange(cm, option) {
if (option == "mode") onChange(cm)
}
function onChange(cm) {
var state = cm.state.foldGutter;
if (!state) return;
var opts = state.options;
state.from = state.to = 0;
clearTimeout(state.changeUpdate);
state.changeUpdate = setTimeout(function() { updateInViewport(cm); }, opts.foldOnChangeTimeSpan || 600);
}
function onViewportChange(cm) {
var state = cm.state.foldGutter;
if (!state) return;
var opts = state.options;
clearTimeout(state.changeUpdate);
state.changeUpdate = setTimeout(function() {
var vp = cm.getViewport();
if (state.from == state.to || vp.from - state.to > 20 || state.from - vp.to > 20) {
updateInViewport(cm);
} else {
cm.operation(function() {
if (vp.from < state.from) {
updateFoldInfo(cm, vp.from, state.from);
state.from = vp.from;
}
if (vp.to > state.to) {
updateFoldInfo(cm, state.to, vp.to);
state.to = vp.to;
}
});
}
}, opts.updateViewportTimeSpan || 400);
}
function onFold(cm, from) {
var state = cm.state.foldGutter;
if (!state) return;
var line = from.line;
if (line >= state.from && line < state.to)
updateFoldInfo(cm, line, line + 1);
}
});

View file

@ -0,0 +1,162 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
var Pos = CodeMirror.Pos;
function forEach(arr, f) {
for (var i = 0, e = arr.length; i < e; ++i) f(arr[i]);
}
function arrayContains(arr, item) {
if (!Array.prototype.indexOf) {
var i = arr.length;
while (i--) {
if (arr[i] === item) {
return true;
}
}
return false;
}
return arr.indexOf(item) != -1;
}
function scriptHint(editor, keywords, getToken, options) {
// Find the token at the cursor
var cur = editor.getCursor(), token = getToken(editor, cur);
if (/\b(?:string|comment)\b/.test(token.type)) return;
var innerMode = CodeMirror.innerMode(editor.getMode(), token.state);
if (innerMode.mode.helperType === "json") return;
token.state = innerMode.state;
// If it's not a 'word-style' token, ignore the token.
if (!/^[\w$_]*$/.test(token.string)) {
token = {start: cur.ch, end: cur.ch, string: "", state: token.state,
type: token.string == "." ? "property" : null};
} else if (token.end > cur.ch) {
token.end = cur.ch;
token.string = token.string.slice(0, cur.ch - token.start);
}
var tprop = token;
// If it is a property, find out what it is a property of.
while (tprop.type == "property") {
tprop = getToken(editor, Pos(cur.line, tprop.start));
if (tprop.string != ".") return;
tprop = getToken(editor, Pos(cur.line, tprop.start));
if (!context) var context = [];
context.push(tprop);
}
return {list: getCompletions(token, context, keywords, options),
from: Pos(cur.line, token.start),
to: Pos(cur.line, token.end)};
}
function javascriptHint(editor, options) {
return scriptHint(editor, javascriptKeywords,
function (e, cur) {return e.getTokenAt(cur);},
options);
};
CodeMirror.registerHelper("hint", "javascript", javascriptHint);
function getCoffeeScriptToken(editor, cur) {
// This getToken, it is for coffeescript, imitates the behavior of
// getTokenAt method in javascript.js, that is, returning "property"
// type and treat "." as independent token.
var token = editor.getTokenAt(cur);
if (cur.ch == token.start + 1 && token.string.charAt(0) == '.') {
token.end = token.start;
token.string = '.';
token.type = "property";
}
else if (/^\.[\w$_]*$/.test(token.string)) {
token.type = "property";
token.start++;
token.string = token.string.replace(/\./, '');
}
return token;
}
function coffeescriptHint(editor, options) {
return scriptHint(editor, coffeescriptKeywords, getCoffeeScriptToken, options);
}
CodeMirror.registerHelper("hint", "coffeescript", coffeescriptHint);
var stringProps = ("charAt charCodeAt indexOf lastIndexOf substring substr slice trim trimLeft trimRight " +
"toUpperCase toLowerCase split concat match replace search").split(" ");
var arrayProps = ("length concat join splice push pop shift unshift slice reverse sort indexOf " +
"lastIndexOf every some filter forEach map reduce reduceRight ").split(" ");
var funcProps = "prototype apply call bind".split(" ");
var javascriptKeywords = ("break case catch class const continue debugger default delete do else export extends false finally for function " +
"if in import instanceof new null return super switch this throw true try typeof var void while with yield").split(" ");
var coffeescriptKeywords = ("and break catch class continue delete do else extends false finally for " +
"if in instanceof isnt new no not null of off on or return switch then throw true try typeof until void while with yes").split(" ");
function forAllProps(obj, callback) {
if (!Object.getOwnPropertyNames || !Object.getPrototypeOf) {
for (var name in obj) callback(name)
} else {
for (var o = obj; o; o = Object.getPrototypeOf(o))
Object.getOwnPropertyNames(o).forEach(callback)
}
}
function getCompletions(token, context, keywords, options) {
var found = [], start = token.string, global = options && options.globalScope || window;
function maybeAdd(str) {
if (str.lastIndexOf(start, 0) == 0 && !arrayContains(found, str)) found.push(str);
}
function gatherCompletions(obj) {
if (typeof obj == "string") forEach(stringProps, maybeAdd);
else if (obj instanceof Array) forEach(arrayProps, maybeAdd);
else if (obj instanceof Function) forEach(funcProps, maybeAdd);
forAllProps(obj, maybeAdd)
}
if (context && context.length) {
// If this is a property, see if it belongs to some object we can
// find in the current environment.
var obj = context.pop(), base;
if (obj.type && obj.type.indexOf("variable") === 0) {
if (options && options.additionalContext)
base = options.additionalContext[obj.string];
if (!options || options.useGlobalScope !== false)
base = base || global[obj.string];
} else if (obj.type == "string") {
base = "";
} else if (obj.type == "atom") {
base = 1;
} else if (obj.type == "function") {
if (global.jQuery != null && (obj.string == '$' || obj.string == 'jQuery') &&
(typeof global.jQuery == 'function'))
base = global.jQuery();
else if (global._ != null && (obj.string == '_') && (typeof global._ == 'function'))
base = global._();
}
while (base != null && context.length)
base = base[context.pop().string];
if (base != null) gatherCompletions(base);
} else {
// If not, just look in the global object, any local scope, and optional additional-context
// (reading into JS mode internals to get at the local and global variables)
for (var v = token.state.localVars; v; v = v.next) maybeAdd(v.name);
for (var c = token.state.context; c; c = c.prev)
for (var v = c.vars; v; v = v.next) maybeAdd(v.name)
for (var v = token.state.globalVars; v; v = v.next) maybeAdd(v.name);
if (options && options.additionalContext != null)
for (var key in options.additionalContext)
maybeAdd(key);
if (!options || options.useGlobalScope !== false)
gatherCompletions(global);
forEach(keywords, maybeAdd);
}
return found;
}
});

View file

@ -0,0 +1,960 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
CodeMirror.defineMode("javascript", function(config, parserConfig) {
var indentUnit = config.indentUnit;
var statementIndent = parserConfig.statementIndent;
var jsonldMode = parserConfig.jsonld;
var jsonMode = parserConfig.json || jsonldMode;
var trackScope = parserConfig.trackScope !== false
var isTS = parserConfig.typescript;
var wordRE = parserConfig.wordCharacters || /[\w$\xa1-\uffff]/;
// Tokenizer
var keywords = function(){
function kw(type) {return {type: type, style: "keyword"};}
var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c"), D = kw("keyword d");
var operator = kw("operator"), atom = {type: "atom", style: "atom"};
return {
"if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B,
"return": D, "break": D, "continue": D, "new": kw("new"), "delete": C, "void": C, "throw": C,
"debugger": kw("debugger"), "var": kw("var"), "const": kw("var"), "let": kw("var"),
"function": kw("function"), "catch": kw("catch"),
"for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"),
"in": operator, "typeof": operator, "instanceof": operator,
"true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom,
"this": kw("this"), "class": kw("class"), "super": kw("atom"),
"yield": C, "export": kw("export"), "import": kw("import"), "extends": C,
"await": C
};
}();
var isOperatorChar = /[+\-*&%=<>!?|~^@]/;
var isJsonldKeyword = /^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/;
function readRegexp(stream) {
var escaped = false, next, inSet = false;
while ((next = stream.next()) != null) {
if (!escaped) {
if (next == "/" && !inSet) return;
if (next == "[") inSet = true;
else if (inSet && next == "]") inSet = false;
}
escaped = !escaped && next == "\\";
}
}
// Used as scratch variables to communicate multiple values without
// consing up tons of objects.
var type, content;
function ret(tp, style, cont) {
type = tp; content = cont;
return style;
}
function tokenBase(stream, state) {
var ch = stream.next();
if (ch == '"' || ch == "'") {
state.tokenize = tokenString(ch);
return state.tokenize(stream, state);
} else if (ch == "." && stream.match(/^\d[\d_]*(?:[eE][+\-]?[\d_]+)?/)) {
return ret("number", "number");
} else if (ch == "." && stream.match("..")) {
return ret("spread", "meta");
} else if (/[\[\]{}\(\),;\:\.]/.test(ch)) {
return ret(ch);
} else if (ch == "=" && stream.eat(">")) {
return ret("=>", "operator");
} else if (ch == "0" && stream.match(/^(?:x[\dA-Fa-f_]+|o[0-7_]+|b[01_]+)n?/)) {
return ret("number", "number");
} else if (/\d/.test(ch)) {
stream.match(/^[\d_]*(?:n|(?:\.[\d_]*)?(?:[eE][+\-]?[\d_]+)?)?/);
return ret("number", "number");
} else if (ch == "/") {
if (stream.eat("*")) {
state.tokenize = tokenComment;
return tokenComment(stream, state);
} else if (stream.eat("/")) {
stream.skipToEnd();
return ret("comment", "comment");
} else if (expressionAllowed(stream, state, 1)) {
readRegexp(stream);
stream.match(/^\b(([gimyus])(?![gimyus]*\2))+\b/);
return ret("regexp", "string-2");
} else {
stream.eat("=");
return ret("operator", "operator", stream.current());
}
} else if (ch == "`") {
state.tokenize = tokenQuasi;
return tokenQuasi(stream, state);
} else if (ch == "#" && stream.peek() == "!") {
stream.skipToEnd();
return ret("meta", "meta");
} else if (ch == "#" && stream.eatWhile(wordRE)) {
return ret("variable", "property")
} else if (ch == "<" && stream.match("!--") ||
(ch == "-" && stream.match("->") && !/\S/.test(stream.string.slice(0, stream.start)))) {
stream.skipToEnd()
return ret("comment", "comment")
} else if (isOperatorChar.test(ch)) {
if (ch != ">" || !state.lexical || state.lexical.type != ">") {
if (stream.eat("=")) {
if (ch == "!" || ch == "=") stream.eat("=")
} else if (/[<>*+\-|&?]/.test(ch)) {
stream.eat(ch)
if (ch == ">") stream.eat(ch)
}
}
if (ch == "?" && stream.eat(".")) return ret(".")
return ret("operator", "operator", stream.current());
} else if (wordRE.test(ch)) {
stream.eatWhile(wordRE);
var word = stream.current()
if (state.lastType != ".") {
if (keywords.propertyIsEnumerable(word)) {
var kw = keywords[word]
return ret(kw.type, kw.style, word)
}
if (word == "async" && stream.match(/^(\s|\/\*([^*]|\*(?!\/))*?\*\/)*[\[\(\w]/, false))
return ret("async", "keyword", word)
}
return ret("variable", "variable", word)
}
}
function tokenString(quote) {
return function(stream, state) {
var escaped = false, next;
if (jsonldMode && stream.peek() == "@" && stream.match(isJsonldKeyword)){
state.tokenize = tokenBase;
return ret("jsonld-keyword", "meta");
}
while ((next = stream.next()) != null) {
if (next == quote && !escaped) break;
escaped = !escaped && next == "\\";
}
if (!escaped) state.tokenize = tokenBase;
return ret("string", "string");
};
}
function tokenComment(stream, state) {
var maybeEnd = false, ch;
while (ch = stream.next()) {
if (ch == "/" && maybeEnd) {
state.tokenize = tokenBase;
break;
}
maybeEnd = (ch == "*");
}
return ret("comment", "comment");
}
function tokenQuasi(stream, state) {
var escaped = false, next;
while ((next = stream.next()) != null) {
if (!escaped && (next == "`" || next == "$" && stream.eat("{"))) {
state.tokenize = tokenBase;
break;
}
escaped = !escaped && next == "\\";
}
return ret("quasi", "string-2", stream.current());
}
var brackets = "([{}])";
// This is a crude lookahead trick to try and notice that we're
// parsing the argument patterns for a fat-arrow function before we
// actually hit the arrow token. It only works if the arrow is on
// the same line as the arguments and there's no strange noise
// (comments) in between. Fallback is to only notice when we hit the
// arrow, and not declare the arguments as locals for the arrow
// body.
function findFatArrow(stream, state) {
if (state.fatArrowAt) state.fatArrowAt = null;
var arrow = stream.string.indexOf("=>", stream.start);
if (arrow < 0) return;
if (isTS) { // Try to skip TypeScript return type declarations after the arguments
var m = /:\s*(?:\w+(?:<[^>]*>|\[\])?|\{[^}]*\})\s*$/.exec(stream.string.slice(stream.start, arrow))
if (m) arrow = m.index
}
var depth = 0, sawSomething = false;
for (var pos = arrow - 1; pos >= 0; --pos) {
var ch = stream.string.charAt(pos);
var bracket = brackets.indexOf(ch);
if (bracket >= 0 && bracket < 3) {
if (!depth) { ++pos; break; }
if (--depth == 0) { if (ch == "(") sawSomething = true; break; }
} else if (bracket >= 3 && bracket < 6) {
++depth;
} else if (wordRE.test(ch)) {
sawSomething = true;
} else if (/["'\/`]/.test(ch)) {
for (;; --pos) {
if (pos == 0) return
var next = stream.string.charAt(pos - 1)
if (next == ch && stream.string.charAt(pos - 2) != "\\") { pos--; break }
}
} else if (sawSomething && !depth) {
++pos;
break;
}
}
if (sawSomething && !depth) state.fatArrowAt = pos;
}
// Parser
var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true,
"regexp": true, "this": true, "import": true, "jsonld-keyword": true};
function JSLexical(indented, column, type, align, prev, info) {
this.indented = indented;
this.column = column;
this.type = type;
this.prev = prev;
this.info = info;
if (align != null) this.align = align;
}
function inScope(state, varname) {
if (!trackScope) return false
for (var v = state.localVars; v; v = v.next)
if (v.name == varname) return true;
for (var cx = state.context; cx; cx = cx.prev) {
for (var v = cx.vars; v; v = v.next)
if (v.name == varname) return true;
}
}
function parseJS(state, style, type, content, stream) {
var cc = state.cc;
// Communicate our context to the combinators.
// (Less wasteful than consing up a hundred closures on every call.)
cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; cx.style = style;
if (!state.lexical.hasOwnProperty("align"))
state.lexical.align = true;
while(true) {
var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement;
if (combinator(type, content)) {
while(cc.length && cc[cc.length - 1].lex)
cc.pop()();
if (cx.marked) return cx.marked;
if (type == "variable" && inScope(state, content)) return "variable-2";
return style;
}
}
}
// Combinator utils
var cx = {state: null, column: null, marked: null, cc: null};
function pass() {
for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]);
}
function cont() {
pass.apply(null, arguments);
return true;
}
function inList(name, list) {
for (var v = list; v; v = v.next) if (v.name == name) return true
return false;
}
function register(varname) {
var state = cx.state;
cx.marked = "def";
if (!trackScope) return
if (state.context) {
if (state.lexical.info == "var" && state.context && state.context.block) {
// FIXME function decls are also not block scoped
var newContext = registerVarScoped(varname, state.context)
if (newContext != null) {
state.context = newContext
return
}
} else if (!inList(varname, state.localVars)) {
state.localVars = new Var(varname, state.localVars)
return
}
}
// Fall through means this is global
if (parserConfig.globalVars && !inList(varname, state.globalVars))
state.globalVars = new Var(varname, state.globalVars)
}
function registerVarScoped(varname, context) {
if (!context) {
return null
} else if (context.block) {
var inner = registerVarScoped(varname, context.prev)
if (!inner) return null
if (inner == context.prev) return context
return new Context(inner, context.vars, true)
} else if (inList(varname, context.vars)) {
return context
} else {
return new Context(context.prev, new Var(varname, context.vars), false)
}
}
function isModifier(name) {
return name == "public" || name == "private" || name == "protected" || name == "abstract" || name == "readonly"
}
// Combinators
function Context(prev, vars, block) { this.prev = prev; this.vars = vars; this.block = block }
function Var(name, next) { this.name = name; this.next = next }
var defaultVars = new Var("this", new Var("arguments", null))
function pushcontext() {
cx.state.context = new Context(cx.state.context, cx.state.localVars, false)
cx.state.localVars = defaultVars
}
function pushblockcontext() {
cx.state.context = new Context(cx.state.context, cx.state.localVars, true)
cx.state.localVars = null
}
pushcontext.lex = pushblockcontext.lex = true
function popcontext() {
cx.state.localVars = cx.state.context.vars
cx.state.context = cx.state.context.prev
}
popcontext.lex = true
function pushlex(type, info) {
var result = function() {
var state = cx.state, indent = state.indented;
if (state.lexical.type == "stat") indent = state.lexical.indented;
else for (var outer = state.lexical; outer && outer.type == ")" && outer.align; outer = outer.prev)
indent = outer.indented;
state.lexical = new JSLexical(indent, cx.stream.column(), type, null, state.lexical, info);
};
result.lex = true;
return result;
}
function poplex() {
var state = cx.state;
if (state.lexical.prev) {
if (state.lexical.type == ")")
state.indented = state.lexical.indented;
state.lexical = state.lexical.prev;
}
}
poplex.lex = true;
function expect(wanted) {
function exp(type) {
if (type == wanted) return cont();
else if (wanted == ";" || type == "}" || type == ")" || type == "]") return pass();
else return cont(exp);
};
return exp;
}
function statement(type, value) {
if (type == "var") return cont(pushlex("vardef", value), vardef, expect(";"), poplex);
if (type == "keyword a") return cont(pushlex("form"), parenExpr, statement, poplex);
if (type == "keyword b") return cont(pushlex("form"), statement, poplex);
if (type == "keyword d") return cx.stream.match(/^\s*$/, false) ? cont() : cont(pushlex("stat"), maybeexpression, expect(";"), poplex);
if (type == "debugger") return cont(expect(";"));
if (type == "{") return cont(pushlex("}"), pushblockcontext, block, poplex, popcontext);
if (type == ";") return cont();
if (type == "if") {
if (cx.state.lexical.info == "else" && cx.state.cc[cx.state.cc.length - 1] == poplex)
cx.state.cc.pop()();
return cont(pushlex("form"), parenExpr, statement, poplex, maybeelse);
}
if (type == "function") return cont(functiondef);
if (type == "for") return cont(pushlex("form"), pushblockcontext, forspec, statement, popcontext, poplex);
if (type == "class" || (isTS && value == "interface")) {
cx.marked = "keyword"
return cont(pushlex("form", type == "class" ? type : value), className, poplex)
}
if (type == "variable") {
if (isTS && value == "declare") {
cx.marked = "keyword"
return cont(statement)
} else if (isTS && (value == "module" || value == "enum" || value == "type") && cx.stream.match(/^\s*\w/, false)) {
cx.marked = "keyword"
if (value == "enum") return cont(enumdef);
else if (value == "type") return cont(typename, expect("operator"), typeexpr, expect(";"));
else return cont(pushlex("form"), pattern, expect("{"), pushlex("}"), block, poplex, poplex)
} else if (isTS && value == "namespace") {
cx.marked = "keyword"
return cont(pushlex("form"), expression, statement, poplex)
} else if (isTS && value == "abstract") {
cx.marked = "keyword"
return cont(statement)
} else {
return cont(pushlex("stat"), maybelabel);
}
}
if (type == "switch") return cont(pushlex("form"), parenExpr, expect("{"), pushlex("}", "switch"), pushblockcontext,
block, poplex, poplex, popcontext);
if (type == "case") return cont(expression, expect(":"));
if (type == "default") return cont(expect(":"));
if (type == "catch") return cont(pushlex("form"), pushcontext, maybeCatchBinding, statement, poplex, popcontext);
if (type == "export") return cont(pushlex("stat"), afterExport, poplex);
if (type == "import") return cont(pushlex("stat"), afterImport, poplex);
if (type == "async") return cont(statement)
if (value == "@") return cont(expression, statement)
return pass(pushlex("stat"), expression, expect(";"), poplex);
}
function maybeCatchBinding(type) {
if (type == "(") return cont(funarg, expect(")"))
}
function expression(type, value) {
return expressionInner(type, value, false);
}
function expressionNoComma(type, value) {
return expressionInner(type, value, true);
}
function parenExpr(type) {
if (type != "(") return pass()
return cont(pushlex(")"), maybeexpression, expect(")"), poplex)
}
function expressionInner(type, value, noComma) {
if (cx.state.fatArrowAt == cx.stream.start) {
var body = noComma ? arrowBodyNoComma : arrowBody;
if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, expect("=>"), body, popcontext);
else if (type == "variable") return pass(pushcontext, pattern, expect("=>"), body, popcontext);
}
var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma;
if (atomicTypes.hasOwnProperty(type)) return cont(maybeop);
if (type == "function") return cont(functiondef, maybeop);
if (type == "class" || (isTS && value == "interface")) { cx.marked = "keyword"; return cont(pushlex("form"), classExpression, poplex); }
if (type == "keyword c" || type == "async") return cont(noComma ? expressionNoComma : expression);
if (type == "(") return cont(pushlex(")"), maybeexpression, expect(")"), poplex, maybeop);
if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression);
if (type == "[") return cont(pushlex("]"), arrayLiteral, poplex, maybeop);
if (type == "{") return contCommasep(objprop, "}", null, maybeop);
if (type == "quasi") return pass(quasi, maybeop);
if (type == "new") return cont(maybeTarget(noComma));
return cont();
}
function maybeexpression(type) {
if (type.match(/[;\}\)\],]/)) return pass();
return pass(expression);
}
function maybeoperatorComma(type, value) {
if (type == ",") return cont(maybeexpression);
return maybeoperatorNoComma(type, value, false);
}
function maybeoperatorNoComma(type, value, noComma) {
var me = noComma == false ? maybeoperatorComma : maybeoperatorNoComma;
var expr = noComma == false ? expression : expressionNoComma;
if (type == "=>") return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext);
if (type == "operator") {
if (/\+\+|--/.test(value) || isTS && value == "!") return cont(me);
if (isTS && value == "<" && cx.stream.match(/^([^<>]|<[^<>]*>)*>\s*\(/, false))
return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, me);
if (value == "?") return cont(expression, expect(":"), expr);
return cont(expr);
}
if (type == "quasi") { return pass(quasi, me); }
if (type == ";") return;
if (type == "(") return contCommasep(expressionNoComma, ")", "call", me);
if (type == ".") return cont(property, me);
if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me);
if (isTS && value == "as") { cx.marked = "keyword"; return cont(typeexpr, me) }
if (type == "regexp") {
cx.state.lastType = cx.marked = "operator"
cx.stream.backUp(cx.stream.pos - cx.stream.start - 1)
return cont(expr)
}
}
function quasi(type, value) {
if (type != "quasi") return pass();
if (value.slice(value.length - 2) != "${") return cont(quasi);
return cont(maybeexpression, continueQuasi);
}
function continueQuasi(type) {
if (type == "}") {
cx.marked = "string-2";
cx.state.tokenize = tokenQuasi;
return cont(quasi);
}
}
function arrowBody(type) {
findFatArrow(cx.stream, cx.state);
return pass(type == "{" ? statement : expression);
}
function arrowBodyNoComma(type) {
findFatArrow(cx.stream, cx.state);
return pass(type == "{" ? statement : expressionNoComma);
}
function maybeTarget(noComma) {
return function(type) {
if (type == ".") return cont(noComma ? targetNoComma : target);
else if (type == "variable" && isTS) return cont(maybeTypeArgs, noComma ? maybeoperatorNoComma : maybeoperatorComma)
else return pass(noComma ? expressionNoComma : expression);
};
}
function target(_, value) {
if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorComma); }
}
function targetNoComma(_, value) {
if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorNoComma); }
}
function maybelabel(type) {
if (type == ":") return cont(poplex, statement);
return pass(maybeoperatorComma, expect(";"), poplex);
}
function property(type) {
if (type == "variable") {cx.marked = "property"; return cont();}
}
function objprop(type, value) {
if (type == "async") {
cx.marked = "property";
return cont(objprop);
} else if (type == "variable" || cx.style == "keyword") {
cx.marked = "property";
if (value == "get" || value == "set") return cont(getterSetter);
var m // Work around fat-arrow-detection complication for detecting typescript typed arrow params
if (isTS && cx.state.fatArrowAt == cx.stream.start && (m = cx.stream.match(/^\s*:\s*/, false)))
cx.state.fatArrowAt = cx.stream.pos + m[0].length
return cont(afterprop);
} else if (type == "number" || type == "string") {
cx.marked = jsonldMode ? "property" : (cx.style + " property");
return cont(afterprop);
} else if (type == "jsonld-keyword") {
return cont(afterprop);
} else if (isTS && isModifier(value)) {
cx.marked = "keyword"
return cont(objprop)
} else if (type == "[") {
return cont(expression, maybetype, expect("]"), afterprop);
} else if (type == "spread") {
return cont(expressionNoComma, afterprop);
} else if (value == "*") {
cx.marked = "keyword";
return cont(objprop);
} else if (type == ":") {
return pass(afterprop)
}
}
function getterSetter(type) {
if (type != "variable") return pass(afterprop);
cx.marked = "property";
return cont(functiondef);
}
function afterprop(type) {
if (type == ":") return cont(expressionNoComma);
if (type == "(") return pass(functiondef);
}
function commasep(what, end, sep) {
function proceed(type, value) {
if (sep ? sep.indexOf(type) > -1 : type == ",") {
var lex = cx.state.lexical;
if (lex.info == "call") lex.pos = (lex.pos || 0) + 1;
return cont(function(type, value) {
if (type == end || value == end) return pass()
return pass(what)
}, proceed);
}
if (type == end || value == end) return cont();
if (sep && sep.indexOf(";") > -1) return pass(what)
return cont(expect(end));
}
return function(type, value) {
if (type == end || value == end) return cont();
return pass(what, proceed);
};
}
function contCommasep(what, end, info) {
for (var i = 3; i < arguments.length; i++)
cx.cc.push(arguments[i]);
return cont(pushlex(end, info), commasep(what, end), poplex);
}
function block(type) {
if (type == "}") return cont();
return pass(statement, block);
}
function maybetype(type, value) {
if (isTS) {
if (type == ":") return cont(typeexpr);
if (value == "?") return cont(maybetype);
}
}
function maybetypeOrIn(type, value) {
if (isTS && (type == ":" || value == "in")) return cont(typeexpr)
}
function mayberettype(type) {
if (isTS && type == ":") {
if (cx.stream.match(/^\s*\w+\s+is\b/, false)) return cont(expression, isKW, typeexpr)
else return cont(typeexpr)
}
}
function isKW(_, value) {
if (value == "is") {
cx.marked = "keyword"
return cont()
}
}
function typeexpr(type, value) {
if (value == "keyof" || value == "typeof" || value == "infer" || value == "readonly") {
cx.marked = "keyword"
return cont(value == "typeof" ? expressionNoComma : typeexpr)
}
if (type == "variable" || value == "void") {
cx.marked = "type"
return cont(afterType)
}
if (value == "|" || value == "&") return cont(typeexpr)
if (type == "string" || type == "number" || type == "atom") return cont(afterType);
if (type == "[") return cont(pushlex("]"), commasep(typeexpr, "]", ","), poplex, afterType)
if (type == "{") return cont(pushlex("}"), typeprops, poplex, afterType)
if (type == "(") return cont(commasep(typearg, ")"), maybeReturnType, afterType)
if (type == "<") return cont(commasep(typeexpr, ">"), typeexpr)
if (type == "quasi") { return pass(quasiType, afterType); }
}
function maybeReturnType(type) {
if (type == "=>") return cont(typeexpr)
}
function typeprops(type) {
if (type.match(/[\}\)\]]/)) return cont()
if (type == "," || type == ";") return cont(typeprops)
return pass(typeprop, typeprops)
}
function typeprop(type, value) {
if (type == "variable" || cx.style == "keyword") {
cx.marked = "property"
return cont(typeprop)
} else if (value == "?" || type == "number" || type == "string") {
return cont(typeprop)
} else if (type == ":") {
return cont(typeexpr)
} else if (type == "[") {
return cont(expect("variable"), maybetypeOrIn, expect("]"), typeprop)
} else if (type == "(") {
return pass(functiondecl, typeprop)
} else if (!type.match(/[;\}\)\],]/)) {
return cont()
}
}
function quasiType(type, value) {
if (type != "quasi") return pass();
if (value.slice(value.length - 2) != "${") return cont(quasiType);
return cont(typeexpr, continueQuasiType);
}
function continueQuasiType(type) {
if (type == "}") {
cx.marked = "string-2";
cx.state.tokenize = tokenQuasi;
return cont(quasiType);
}
}
function typearg(type, value) {
if (type == "variable" && cx.stream.match(/^\s*[?:]/, false) || value == "?") return cont(typearg)
if (type == ":") return cont(typeexpr)
if (type == "spread") return cont(typearg)
return pass(typeexpr)
}
function afterType(type, value) {
if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType)
if (value == "|" || type == "." || value == "&") return cont(typeexpr)
if (type == "[") return cont(typeexpr, expect("]"), afterType)
if (value == "extends" || value == "implements") { cx.marked = "keyword"; return cont(typeexpr) }
if (value == "?") return cont(typeexpr, expect(":"), typeexpr)
}
function maybeTypeArgs(_, value) {
if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType)
}
function typeparam() {
return pass(typeexpr, maybeTypeDefault)
}
function maybeTypeDefault(_, value) {
if (value == "=") return cont(typeexpr)
}
function vardef(_, value) {
if (value == "enum") {cx.marked = "keyword"; return cont(enumdef)}
return pass(pattern, maybetype, maybeAssign, vardefCont);
}
function pattern(type, value) {
if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(pattern) }
if (type == "variable") { register(value); return cont(); }
if (type == "spread") return cont(pattern);
if (type == "[") return contCommasep(eltpattern, "]");
if (type == "{") return contCommasep(proppattern, "}");
}
function proppattern(type, value) {
if (type == "variable" && !cx.stream.match(/^\s*:/, false)) {
register(value);
return cont(maybeAssign);
}
if (type == "variable") cx.marked = "property";
if (type == "spread") return cont(pattern);
if (type == "}") return pass();
if (type == "[") return cont(expression, expect(']'), expect(':'), proppattern);
return cont(expect(":"), pattern, maybeAssign);
}
function eltpattern() {
return pass(pattern, maybeAssign)
}
function maybeAssign(_type, value) {
if (value == "=") return cont(expressionNoComma);
}
function vardefCont(type) {
if (type == ",") return cont(vardef);
}
function maybeelse(type, value) {
if (type == "keyword b" && value == "else") return cont(pushlex("form", "else"), statement, poplex);
}
function forspec(type, value) {
if (value == "await") return cont(forspec);
if (type == "(") return cont(pushlex(")"), forspec1, poplex);
}
function forspec1(type) {
if (type == "var") return cont(vardef, forspec2);
if (type == "variable") return cont(forspec2);
return pass(forspec2)
}
function forspec2(type, value) {
if (type == ")") return cont()
if (type == ";") return cont(forspec2)
if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression, forspec2) }
return pass(expression, forspec2)
}
function functiondef(type, value) {
if (value == "*") {cx.marked = "keyword"; return cont(functiondef);}
if (type == "variable") {register(value); return cont(functiondef);}
if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, statement, popcontext);
if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondef)
}
function functiondecl(type, value) {
if (value == "*") {cx.marked = "keyword"; return cont(functiondecl);}
if (type == "variable") {register(value); return cont(functiondecl);}
if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, popcontext);
if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondecl)
}
function typename(type, value) {
if (type == "keyword" || type == "variable") {
cx.marked = "type"
return cont(typename)
} else if (value == "<") {
return cont(pushlex(">"), commasep(typeparam, ">"), poplex)
}
}
function funarg(type, value) {
if (value == "@") cont(expression, funarg)
if (type == "spread") return cont(funarg);
if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(funarg); }
if (isTS && type == "this") return cont(maybetype, maybeAssign)
return pass(pattern, maybetype, maybeAssign);
}
function classExpression(type, value) {
// Class expressions may have an optional name.
if (type == "variable") return className(type, value);
return classNameAfter(type, value);
}
function className(type, value) {
if (type == "variable") {register(value); return cont(classNameAfter);}
}
function classNameAfter(type, value) {
if (value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, classNameAfter)
if (value == "extends" || value == "implements" || (isTS && type == ",")) {
if (value == "implements") cx.marked = "keyword";
return cont(isTS ? typeexpr : expression, classNameAfter);
}
if (type == "{") return cont(pushlex("}"), classBody, poplex);
}
function classBody(type, value) {
if (type == "async" ||
(type == "variable" &&
(value == "static" || value == "get" || value == "set" || (isTS && isModifier(value))) &&
cx.stream.match(/^\s+#?[\w$\xa1-\uffff]/, false))) {
cx.marked = "keyword";
return cont(classBody);
}
if (type == "variable" || cx.style == "keyword") {
cx.marked = "property";
return cont(classfield, classBody);
}
if (type == "number" || type == "string") return cont(classfield, classBody);
if (type == "[")
return cont(expression, maybetype, expect("]"), classfield, classBody)
if (value == "*") {
cx.marked = "keyword";
return cont(classBody);
}
if (isTS && type == "(") return pass(functiondecl, classBody)
if (type == ";" || type == ",") return cont(classBody);
if (type == "}") return cont();
if (value == "@") return cont(expression, classBody)
}
function classfield(type, value) {
if (value == "!") return cont(classfield)
if (value == "?") return cont(classfield)
if (type == ":") return cont(typeexpr, maybeAssign)
if (value == "=") return cont(expressionNoComma)
var context = cx.state.lexical.prev, isInterface = context && context.info == "interface"
return pass(isInterface ? functiondecl : functiondef)
}
function afterExport(type, value) {
if (value == "*") { cx.marked = "keyword"; return cont(maybeFrom, expect(";")); }
if (value == "default") { cx.marked = "keyword"; return cont(expression, expect(";")); }
if (type == "{") return cont(commasep(exportField, "}"), maybeFrom, expect(";"));
return pass(statement);
}
function exportField(type, value) {
if (value == "as") { cx.marked = "keyword"; return cont(expect("variable")); }
if (type == "variable") return pass(expressionNoComma, exportField);
}
function afterImport(type) {
if (type == "string") return cont();
if (type == "(") return pass(expression);
if (type == ".") return pass(maybeoperatorComma);
return pass(importSpec, maybeMoreImports, maybeFrom);
}
function importSpec(type, value) {
if (type == "{") return contCommasep(importSpec, "}");
if (type == "variable") register(value);
if (value == "*") cx.marked = "keyword";
return cont(maybeAs);
}
function maybeMoreImports(type) {
if (type == ",") return cont(importSpec, maybeMoreImports)
}
function maybeAs(_type, value) {
if (value == "as") { cx.marked = "keyword"; return cont(importSpec); }
}
function maybeFrom(_type, value) {
if (value == "from") { cx.marked = "keyword"; return cont(expression); }
}
function arrayLiteral(type) {
if (type == "]") return cont();
return pass(commasep(expressionNoComma, "]"));
}
function enumdef() {
return pass(pushlex("form"), pattern, expect("{"), pushlex("}"), commasep(enummember, "}"), poplex, poplex)
}
function enummember() {
return pass(pattern, maybeAssign);
}
function isContinuedStatement(state, textAfter) {
return state.lastType == "operator" || state.lastType == "," ||
isOperatorChar.test(textAfter.charAt(0)) ||
/[,.]/.test(textAfter.charAt(0));
}
function expressionAllowed(stream, state, backUp) {
return state.tokenize == tokenBase &&
/^(?:operator|sof|keyword [bcd]|case|new|export|default|spread|[\[{}\(,;:]|=>)$/.test(state.lastType) ||
(state.lastType == "quasi" && /\{\s*$/.test(stream.string.slice(0, stream.pos - (backUp || 0))))
}
// Interface
return {
startState: function(basecolumn) {
var state = {
tokenize: tokenBase,
lastType: "sof",
cc: [],
lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false),
localVars: parserConfig.localVars,
context: parserConfig.localVars && new Context(null, null, false),
indented: basecolumn || 0
};
if (parserConfig.globalVars && typeof parserConfig.globalVars == "object")
state.globalVars = parserConfig.globalVars;
return state;
},
token: function(stream, state) {
if (stream.sol()) {
if (!state.lexical.hasOwnProperty("align"))
state.lexical.align = false;
state.indented = stream.indentation();
findFatArrow(stream, state);
}
if (state.tokenize != tokenComment && stream.eatSpace()) return null;
var style = state.tokenize(stream, state);
if (type == "comment") return style;
state.lastType = type == "operator" && (content == "++" || content == "--") ? "incdec" : type;
return parseJS(state, style, type, content, stream);
},
indent: function(state, textAfter) {
if (state.tokenize == tokenComment || state.tokenize == tokenQuasi) return CodeMirror.Pass;
if (state.tokenize != tokenBase) return 0;
var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical, top
// Kludge to prevent 'maybelse' from blocking lexical scope pops
if (!/^\s*else\b/.test(textAfter)) for (var i = state.cc.length - 1; i >= 0; --i) {
var c = state.cc[i];
if (c == poplex) lexical = lexical.prev;
else if (c != maybeelse && c != popcontext) break;
}
while ((lexical.type == "stat" || lexical.type == "form") &&
(firstChar == "}" || ((top = state.cc[state.cc.length - 1]) &&
(top == maybeoperatorComma || top == maybeoperatorNoComma) &&
!/^[,\.=+\-*:?[\(]/.test(textAfter))))
lexical = lexical.prev;
if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat")
lexical = lexical.prev;
var type = lexical.type, closing = firstChar == type;
if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info.length + 1 : 0);
else if (type == "form" && firstChar == "{") return lexical.indented;
else if (type == "form") return lexical.indented + indentUnit;
else if (type == "stat")
return lexical.indented + (isContinuedStatement(state, textAfter) ? statementIndent || indentUnit : 0);
else if (lexical.info == "switch" && !closing && parserConfig.doubleIndentSwitch != false)
return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit);
else if (lexical.align) return lexical.column + (closing ? 0 : 1);
else return lexical.indented + (closing ? 0 : indentUnit);
},
electricInput: /^\s*(?:case .*?:|default:|\{|\})$/,
blockCommentStart: jsonMode ? null : "/*",
blockCommentEnd: jsonMode ? null : "*/",
blockCommentContinue: jsonMode ? null : " * ",
lineComment: jsonMode ? null : "//",
fold: "brace",
closeBrackets: "()[]{}''\"\"``",
helperType: jsonMode ? "json" : "javascript",
jsonldMode: jsonldMode,
jsonMode: jsonMode,
expressionAllowed: expressionAllowed,
skipExpression: function(state) {
parseJS(state, "atom", "atom", "true", new CodeMirror.StringStream("", 2, null))
}
};
});
CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/);
CodeMirror.defineMIME("text/javascript", "javascript");
CodeMirror.defineMIME("text/ecmascript", "javascript");
CodeMirror.defineMIME("application/javascript", "javascript");
CodeMirror.defineMIME("application/x-javascript", "javascript");
CodeMirror.defineMIME("application/ecmascript", "javascript");
CodeMirror.defineMIME("application/json", { name: "javascript", json: true });
CodeMirror.defineMIME("application/x-json", { name: "javascript", json: true });
CodeMirror.defineMIME("application/manifest+json", { name: "javascript", json: true })
CodeMirror.defineMIME("application/ld+json", { name: "javascript", jsonld: true });
CodeMirror.defineMIME("text/typescript", { name: "javascript", typescript: true });
CodeMirror.defineMIME("application/typescript", { name: "javascript", typescript: true });
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,65 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
// Depends on jshint.js from https://github.com/jshint/jshint
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
// declare global: JSHINT
function validator(text, options) {
if (!window.JSHINT) {
if (window.console) {
window.console.error("Error: window.JSHINT not defined, CodeMirror JavaScript linting cannot run.");
}
return [];
}
if (!options.indent) // JSHint error.character actually is a column index, this fixes underlining on lines using tabs for indentation
options.indent = 1; // JSHint default value is 4
JSHINT(text, options, options.globals);
var errors = JSHINT.data().errors, result = [];
if (errors) parseErrors(errors, result);
return result;
}
CodeMirror.registerHelper("lint", "javascript", validator);
function parseErrors(errors, output) {
for ( var i = 0; i < errors.length; i++) {
var error = errors[i];
if (error) {
if (error.line <= 0) {
if (window.console) {
window.console.warn("Cannot display JSHint error (invalid line " + error.line + ")", error);
}
continue;
}
var start = error.character - 1, end = start + 1;
if (error.evidence) {
var index = error.evidence.substring(start).search(/.\b/);
if (index > -1) {
end += index;
}
}
// Convert to format expected by validation service
var hint = {
message: error.reason,
severity: error.code ? (error.code.startsWith('W') ? "warning" : "error") : "error",
from: CodeMirror.Pos(error.line - 1, start),
to: CodeMirror.Pos(error.line - 1, end)
};
output.push(hint);
}
}
}
});

View file

@ -0,0 +1,79 @@
/* The lint marker gutter */
.CodeMirror-lint-markers {
width: 16px;
}
.CodeMirror-lint-tooltip {
background-color: #ffd;
border: 1px solid black;
border-radius: 4px 4px 4px 4px;
color: black;
font-family: monospace;
font-size: 10pt;
overflow: hidden;
padding: 2px 5px;
position: fixed;
white-space: pre;
white-space: pre-wrap;
z-index: 100;
max-width: 600px;
opacity: 0;
transition: opacity .4s;
-moz-transition: opacity .4s;
-webkit-transition: opacity .4s;
-o-transition: opacity .4s;
-ms-transition: opacity .4s;
}
.CodeMirror-lint-mark {
background-position: left bottom;
background-repeat: repeat-x;
}
.CodeMirror-lint-mark-warning {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII=");
}
.CodeMirror-lint-mark-error {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==");
}
.CodeMirror-lint-marker {
background-position: center center;
background-repeat: no-repeat;
cursor: pointer;
display: inline-block;
height: 16px;
width: 16px;
vertical-align: middle;
position: relative;
}
.CodeMirror-lint-message {
padding-left: 18px;
background-position: top left;
background-repeat: no-repeat;
}
.CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII=");
}
.CodeMirror-lint-marker-error, .CodeMirror-lint-message-error {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII=");
}
.CodeMirror-lint-marker-multiple {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC");
background-repeat: no-repeat;
background-position: right bottom;
width: 100%; height: 100%;
}
.CodeMirror-lint-line-error {
background-color: rgba(183, 76, 81, 0.08);
}
.CodeMirror-lint-line-warning {
background-color: rgba(255, 211, 0, 0.1);
}

View file

@ -0,0 +1,288 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
var GUTTER_ID = "CodeMirror-lint-markers";
var LINT_LINE_ID = "CodeMirror-lint-line-";
function showTooltip(cm, e, content) {
var tt = document.createElement("div");
tt.className = "CodeMirror-lint-tooltip cm-s-" + cm.options.theme;
tt.appendChild(content.cloneNode(true));
if (cm.state.lint.options.selfContain)
cm.getWrapperElement().appendChild(tt);
else
document.body.appendChild(tt);
function position(e) {
if (!tt.parentNode) return CodeMirror.off(document, "mousemove", position);
var top = Math.max(0, e.clientY - tt.offsetHeight - 5);
var left = Math.max(0, Math.min(e.clientX + 5, tt.ownerDocument.defaultView.innerWidth - tt.offsetWidth));
tt.style.top = top + "px"
tt.style.left = left + "px";
}
CodeMirror.on(document, "mousemove", position);
position(e);
if (tt.style.opacity != null) tt.style.opacity = 1;
return tt;
}
function rm(elt) {
if (elt.parentNode) elt.parentNode.removeChild(elt);
}
function hideTooltip(tt) {
if (!tt.parentNode) return;
if (tt.style.opacity == null) rm(tt);
tt.style.opacity = 0;
setTimeout(function() { rm(tt); }, 600);
}
function showTooltipFor(cm, e, content, node) {
var tooltip = showTooltip(cm, e, content);
function hide() {
CodeMirror.off(node, "mouseout", hide);
if (tooltip) { hideTooltip(tooltip); tooltip = null; }
}
var poll = setInterval(function() {
if (tooltip) for (var n = node;; n = n.parentNode) {
if (n && n.nodeType == 11) n = n.host;
if (n == document.body) return;
if (!n) { hide(); break; }
}
if (!tooltip) return clearInterval(poll);
}, 400);
CodeMirror.on(node, "mouseout", hide);
}
function LintState(cm, conf, hasGutter) {
this.marked = [];
if (conf instanceof Function) conf = {getAnnotations: conf};
if (!conf || conf === true) conf = {};
this.options = {};
this.linterOptions = conf.options || {};
for (var prop in defaults) this.options[prop] = defaults[prop];
for (var prop in conf) {
if (defaults.hasOwnProperty(prop)) {
if (conf[prop] != null) this.options[prop] = conf[prop];
} else if (!conf.options) {
this.linterOptions[prop] = conf[prop];
}
}
this.timeout = null;
this.hasGutter = hasGutter;
this.onMouseOver = function(e) { onMouseOver(cm, e); };
this.waitingFor = 0
}
var defaults = {
highlightLines: false,
tooltips: true,
delay: 500,
lintOnChange: true,
getAnnotations: null,
async: false,
selfContain: null,
formatAnnotation: null,
onUpdateLinting: null
}
function clearMarks(cm) {
var state = cm.state.lint;
if (state.hasGutter) cm.clearGutter(GUTTER_ID);
if (state.options.highlightLines) clearErrorLines(cm);
for (var i = 0; i < state.marked.length; ++i)
state.marked[i].clear();
state.marked.length = 0;
}
function clearErrorLines(cm) {
cm.eachLine(function(line) {
var has = line.wrapClass && /\bCodeMirror-lint-line-\w+\b/.exec(line.wrapClass);
if (has) cm.removeLineClass(line, "wrap", has[0]);
})
}
function makeMarker(cm, labels, severity, multiple, tooltips) {
var marker = document.createElement("div"), inner = marker;
marker.className = "CodeMirror-lint-marker CodeMirror-lint-marker-" + severity;
if (multiple) {
inner = marker.appendChild(document.createElement("div"));
inner.className = "CodeMirror-lint-marker CodeMirror-lint-marker-multiple";
}
if (tooltips != false) CodeMirror.on(inner, "mouseover", function(e) {
showTooltipFor(cm, e, labels, inner);
});
return marker;
}
function getMaxSeverity(a, b) {
if (a == "error") return a;
else return b;
}
function groupByLine(annotations) {
var lines = [];
for (var i = 0; i < annotations.length; ++i) {
var ann = annotations[i], line = ann.from.line;
(lines[line] || (lines[line] = [])).push(ann);
}
return lines;
}
function annotationTooltip(ann) {
var severity = ann.severity;
if (!severity) severity = "error";
var tip = document.createElement("div");
tip.className = "CodeMirror-lint-message CodeMirror-lint-message-" + severity;
if (typeof ann.messageHTML != 'undefined') {
tip.innerHTML = ann.messageHTML;
} else {
tip.appendChild(document.createTextNode(ann.message));
}
return tip;
}
function lintAsync(cm, getAnnotations) {
var state = cm.state.lint
var id = ++state.waitingFor
function abort() {
id = -1
cm.off("change", abort)
}
cm.on("change", abort)
getAnnotations(cm.getValue(), function(annotations, arg2) {
cm.off("change", abort)
if (state.waitingFor != id) return
if (arg2 && annotations instanceof CodeMirror) annotations = arg2
cm.operation(function() {updateLinting(cm, annotations)})
}, state.linterOptions, cm);
}
function startLinting(cm) {
var state = cm.state.lint;
if (!state) return;
var options = state.options;
/*
* Passing rules in `options` property prevents JSHint (and other linters) from complaining
* about unrecognized rules like `onUpdateLinting`, `delay`, `lintOnChange`, etc.
*/
var getAnnotations = options.getAnnotations || cm.getHelper(CodeMirror.Pos(0, 0), "lint");
if (!getAnnotations) return;
if (options.async || getAnnotations.async) {
lintAsync(cm, getAnnotations)
} else {
var annotations = getAnnotations(cm.getValue(), state.linterOptions, cm);
if (!annotations) return;
if (annotations.then) annotations.then(function(issues) {
cm.operation(function() {updateLinting(cm, issues)})
});
else cm.operation(function() {updateLinting(cm, annotations)})
}
}
function updateLinting(cm, annotationsNotSorted) {
var state = cm.state.lint;
if (!state) return;
var options = state.options;
clearMarks(cm);
var annotations = groupByLine(annotationsNotSorted);
for (var line = 0; line < annotations.length; ++line) {
var anns = annotations[line];
if (!anns) continue;
var maxSeverity = null;
var tipLabel = state.hasGutter && document.createDocumentFragment();
for (var i = 0; i < anns.length; ++i) {
var ann = anns[i];
var severity = ann.severity;
if (!severity) severity = "error";
maxSeverity = getMaxSeverity(maxSeverity, severity);
if (options.formatAnnotation) ann = options.formatAnnotation(ann);
if (state.hasGutter) tipLabel.appendChild(annotationTooltip(ann));
if (ann.to) state.marked.push(cm.markText(ann.from, ann.to, {
className: "CodeMirror-lint-mark CodeMirror-lint-mark-" + severity,
__annotation: ann
}));
}
if (state.hasGutter)
cm.setGutterMarker(line, GUTTER_ID, makeMarker(cm, tipLabel, maxSeverity, anns.length > 1,
options.tooltips));
if (options.highlightLines)
cm.addLineClass(line, "wrap", LINT_LINE_ID + maxSeverity);
}
if (options.onUpdateLinting) options.onUpdateLinting(annotationsNotSorted, annotations, cm);
}
function onChange(cm) {
var state = cm.state.lint;
if (!state) return;
clearTimeout(state.timeout);
state.timeout = setTimeout(function(){startLinting(cm);}, state.options.delay);
}
function popupTooltips(cm, annotations, e) {
var target = e.target || e.srcElement;
var tooltip = document.createDocumentFragment();
for (var i = 0; i < annotations.length; i++) {
var ann = annotations[i];
tooltip.appendChild(annotationTooltip(ann));
}
showTooltipFor(cm, e, tooltip, target);
}
function onMouseOver(cm, e) {
var target = e.target || e.srcElement;
if (!/\bCodeMirror-lint-mark-/.test(target.className)) return;
var box = target.getBoundingClientRect(), x = (box.left + box.right) / 2, y = (box.top + box.bottom) / 2;
var spans = cm.findMarksAt(cm.coordsChar({left: x, top: y}, "client"));
var annotations = [];
for (var i = 0; i < spans.length; ++i) {
var ann = spans[i].__annotation;
if (ann) annotations.push(ann);
}
if (annotations.length) popupTooltips(cm, annotations, e);
}
CodeMirror.defineOption("lint", false, function(cm, val, old) {
if (old && old != CodeMirror.Init) {
clearMarks(cm);
if (cm.state.lint.options.lintOnChange !== false)
cm.off("change", onChange);
CodeMirror.off(cm.getWrapperElement(), "mouseover", cm.state.lint.onMouseOver);
clearTimeout(cm.state.lint.timeout);
delete cm.state.lint;
}
if (val) {
var gutters = cm.getOption("gutters"), hasLintGutter = false;
for (var i = 0; i < gutters.length; ++i) if (gutters[i] == GUTTER_ID) hasLintGutter = true;
var state = cm.state.lint = new LintState(cm, val, hasLintGutter);
if (state.options.lintOnChange)
cm.on("change", onChange);
if (state.options.tooltips != false && state.options.tooltips != "gutter")
CodeMirror.on(cm.getWrapperElement(), "mouseover", state.onMouseOver);
startLinting(cm);
}
});
CodeMirror.defineExtension("performLint", function() {
startLinting(this);
});
});

View file

@ -0,0 +1,86 @@
/*
Copyright (C) 2011 by MarkLogic Corporation
Author: Mike Brevoort <mike@brevoort.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
.cm-s-xq.CodeMirror { border-radius: 1.5rem; border: 1px solid #d9d9d9; height: auto; }
.cm-s-xq.CodeMirror:hover { background-color: rgb(232 244 242); border-color: #18947b; transition: all .3s; }
.cm-s-xq .CodeMirror-gutters { border-right: 1px solid #ddd; background-color: rgb(221 221 221 / 20%); white-space: nowrap; }
.cm-s-xq span.cm-keyword { line-height: 1em; font-weight: bold; color: #5A5CAD; }
.cm-s-xq span.cm-atom { color: #7A316F; font-weight:bold; }
.cm-s-xq span.cm-number { color: #e36209; }
.cm-s-xq span.cm-def { text-decoration:underline; }
.cm-s-xq span.cm-variable { color: black; }
.cm-s-xq span.cm-variable-2 { color:black; }
.cm-s-xq span.cm-variable-3, .cm-s-xq span.cm-type { color: black; }
.cm-s-xq span.cm-property { color: #008771; }
.cm-s-xq span.cm-operator {}
.cm-s-xq span.cm-comment { color: #bbbbbb; font-style: italic; }
.cm-s-xq span.cm-string {}
.cm-s-xq span.cm-meta { color: yellow; }
.cm-s-xq span.cm-qualifier { color: grey; }
.cm-s-xq span.cm-builtin { color: #7EA656; }
.cm-s-xq span.cm-bracket { color: #cc7; }
.cm-s-xq span.cm-tag { color: #3F7F7F; }
.cm-s-xq span.cm-attribute { color: #7F007F; }
.cm-s-xq span.cm-error { color: #e04141; }
.cm-s-xq .CodeMirror-activeline-background { background: #e8f2ff; }
.cm-s-xq .CodeMirror-matchingbracket { outline:1px solid grey;color:black !important;background:yellow; }
.dark .cm-s-xq.CodeMirror { background-color: #222D42; border-color: #2c3950; color: rgb(255 255 255 / 65%); }
.dark .cm-s-xq.CodeMirror:hover { background-color: rgb(0 50 42 / 30%); border-color: #008771; transition: all .3s; }
.dark .cm-s-xq div.CodeMirror-selected { background: rgba(0, 0, 0, 0.5); }
.dark .cm-s-xq .CodeMirror-line::selection, .dark .cm-s-xq .CodeMirror-line > span::selection, .dark .cm-s-xq .CodeMirror-line > span > span::selection { background: rgba(39, 0, 122, 0.99); }
.dark .cm-s-xq .CodeMirror-line::-moz-selection, .dark .cm-s-xq .CodeMirror-line > span::-moz-selection, .dark .cm-s-xq .CodeMirror-line > span > span::-moz-selection { background: rgba(39, 0, 122, 0.99); }
.dark .cm-s-xq .CodeMirror-gutters { background: rgb(0 0 0 / 30%); border-right: 1px solid #2c3950; }
.dark .cm-s-xq .CodeMirror-guttermarker { color: #FFBD40; }
.dark .cm-s-xq .CodeMirror-guttermarker-subtle { color: rgb(255 255 255 / 70%); }
.dark .cm-s-xq .CodeMirror-linenumber { color: rgb(255 255 255 / 50%); }
.dark .cm-s-xq .CodeMirror-cursor { border-left: 1px solid white; }
.dark .cm-s-xq span.cm-keyword { color: #FFBD40; }
.dark .cm-s-xq span.cm-atom { color: #c099ff; }
.dark .cm-s-xq span.cm-number { color: #9ccfd8; }
.dark .cm-s-xq span.cm-def { color: #FFF; text-decoration:underline; }
.dark .cm-s-xq span.cm-variable { color: #FFF; }
.dark .cm-s-xq span.cm-variable-2 { color: #EEE; }
.dark .cm-s-xq span.cm-variable-3, .dark .cm-s-xq span.cm-type { color: #DDD; }
.dark .cm-s-xq span.cm-property { color: #f6c177; }
.dark .cm-s-xq span.cm-operator {}
.dark .cm-s-xq span.cm-comment { color: gray; }
.dark .cm-s-xq span.cm-string {}
.dark .cm-s-xq span.cm-meta { color: yellow; }
.dark .cm-s-xq span.cm-qualifier { color: #FFF700; }
.dark .cm-s-xq span.cm-builtin { color: #30a; }
.dark .cm-s-xq span.cm-bracket { color: #cc7; }
.dark .cm-s-xq span.cm-tag { color: #FFBD40; }
.dark .cm-s-xq span.cm-attribute { color: #FFF700; }
.dark .cm-s-xq span.cm-error { color: #e04141; }
.dark .cm-s-xq .CodeMirror-activeline-background { background: #27282E; }
.dark .cm-s-xq .CodeMirror-matchingbracket { outline:1px solid grey; color:white !important; }
.Line-Hover{transition: all .2s;}
.Line-Hover:hover{ background-color: rgba(0, 102, 85, 0.05) !important; }
.dark .Line-Hover:hover{ background-color: rgb(0 0 0 / 20%) !important; }
.CodeMirror-foldmarker { color: #fc8800; text-shadow: #ffd8aa 1px 1px 2px, #ffd8aa -1px -1px 2px, #ffd8aa 1px -1px 2px, #ffd8aa -1px 1px 2px; font-family: arial; line-height: .3; cursor: pointer; }
.dark .CodeMirror-foldmarker { color: #ffffff; text-shadow: #bbb 1px 1px 2px, #bbb -1px -1px 2px, #bbb 1px -1px 2px, #bbb -1px 1px 2px; font-family: arial; line-height: .3; cursor: pointer; }

File diff suppressed because it is too large Load diff

View file

@ -1,31 +1,3 @@
class User {
constructor() {
this.username = "";
this.password = "";
this.LoginSecret = "";
}
}
class Msg {
constructor(success, msg, obj) {
this.success = false;
this.msg = "";
this.obj = null;
if (success != null) {
this.success = success;
}
if (msg != null) {
this.msg = msg;
}
if (obj != null) {
this.obj = obj;
}
}
}
class DBInbound {
constructor(data) {
@ -37,7 +9,6 @@ class DBInbound {
this.remark = "";
this.enable = true;
this.expiryTime = 0;
this.limitIp = 0;
this.listen = "";
this.port = 0;
@ -141,6 +112,19 @@ class DBInbound {
return Inbound.fromJson(config);
}
isMultiUser() {
switch (this.protocol) {
case Protocols.VMESS:
case Protocols.VLESS:
case Protocols.TROJAN:
return true;
case Protocols.SHADOWSOCKS:
return this.toInbound().isSSMultiUser;
default:
return false;
}
}
hasLink() {
switch (this.protocol) {
case Protocols.VMESS:
@ -153,59 +137,8 @@ class DBInbound {
}
}
genLink(address=this.address, remark=this.remark, clientIndex=0) {
genInboundLinks() {
const inbound = this.toInbound();
return inbound.genLink(address, remark, clientIndex);
}
get genInboundLinks() {
const inbound = this.toInbound();
return inbound.genInboundLinks(this.address, this.remark);
}
}
class AllSetting {
constructor(data) {
this.webListen = "";
this.webDomain = "";
this.webPort = 2053;
this.webCertFile = "";
this.webKeyFile = "";
this.webBasePath = "/";
this.sessionMaxAge = "";
this.expireDiff = "";
this.trafficDiff = "";
this.tgBotEnable = false;
this.tgBotToken = "";
this.tgBotChatId = "";
this.tgRunTime = "@daily";
this.tgBotBackup = false;
this.tgBotLoginNotify = true;
this.tgCpu = "";
this.tgLang = "en-US";
this.xrayTemplateConfig = "";
this.secretEnable = false;
this.subEnable = false;
this.subListen = "";
this.subPort = "2096";
this.subPath = "/sub/";
this.subDomain = "";
this.subCertFile = "";
this.subKeyFile = "";
this.subUpdates = 0;
this.subEncrypt = true;
this.subShowInfo = true;
this.timeLocation = "Asia/Tehran";
if (data == null) {
return
}
ObjectUtil.cloneProps(this, data);
}
equals(other) {
return ObjectUtil.equals(this, other);
return inbound.genInboundLinks(this.remark,remarkModel);
}
}

View file

@ -0,0 +1,900 @@
const Protocols = {
Freedom: "freedom",
Blackhole: "blackhole",
DNS: "dns",
VMess: "vmess",
VLESS: "vless",
Trojan: "trojan",
Shadowsocks: "shadowsocks",
Socks: "socks",
HTTP: "http",
};
const SSMethods = {
AES_256_GCM: 'aes-256-gcm',
AES_128_GCM: 'aes-128-gcm',
CHACHA20_POLY1305: 'chacha20-poly1305',
CHACHA20_IETF_POLY1305: 'chacha20-ietf-poly1305',
XCHACHA20_POLY1305: 'xchacha20-poly1305',
XCHACHA20_IETF_POLY1305: 'xchacha20-ietf-poly1305',
BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',
BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',
BLAKE3_CHACHA20_POLY1305: '2022-blake3-chacha20-poly1305',
};
const TLS_FLOW_CONTROL = {
VISION: "xtls-rprx-vision",
VISION_UDP443: "xtls-rprx-vision-udp443",
};
const UTLS_FINGERPRINT = {
UTLS_CHROME: "chrome",
UTLS_FIREFOX: "firefox",
UTLS_SAFARI: "safari",
UTLS_IOS: "ios",
UTLS_android: "android",
UTLS_EDGE: "edge",
UTLS_360: "360",
UTLS_QQ: "qq",
UTLS_RANDOM: "random",
UTLS_RANDOMIZED: "randomized",
};
const ALPN_OPTION = {
H3: "h3",
H2: "h2",
HTTP1: "http/1.1",
};
const outboundDomainStrategies = [
"AsIs",
"UseIP",
"UseIPv4",
"UseIPv6"
]
Object.freeze(Protocols);
Object.freeze(SSMethods);
Object.freeze(TLS_FLOW_CONTROL);
Object.freeze(ALPN_OPTION);
Object.freeze(outboundDomainStrategies);
class CommonClass {
static toJsonArray(arr) {
return arr.map(obj => obj.toJson());
}
static fromJson() {
return new CommonClass();
}
toJson() {
return this;
}
toString(format=true) {
return format ? JSON.stringify(this.toJson(), null, 2) : JSON.stringify(this.toJson());
}
}
class TcpStreamSettings extends CommonClass {
constructor(type='none', host, path) {
super();
this.type = type;
this.host = host;
this.path = path;
}
static fromJson(json={}) {
let header = json.header;
if (!header) return new TcpStreamSettings();
if(header.type == 'http' && header.request){
return new TcpStreamSettings(
header.type,
header.request.headers.Host.join(','),
header.request.path.join(','),
);
}
return new TcpStreamSettings(header.type,'','');
}
toJson() {
return {
header: {
type: this.type,
request: this.type === 'http' ? {
headers: {
Host: ObjectUtil.isEmpty(this.host) ? [] : this.host.split(',')
},
path: ObjectUtil.isEmpty(this.path) ? ["/"] : this.path.split(',')
} : undefined,
}
};
}
}
class KcpStreamSettings extends CommonClass {
constructor(mtu=1350, tti=20,
uplinkCapacity=5,
downlinkCapacity=20,
congestion=false,
readBufferSize=2,
writeBufferSize=2,
type='none',
seed='',
) {
super();
this.mtu = mtu;
this.tti = tti;
this.upCap = uplinkCapacity;
this.downCap = downlinkCapacity;
this.congestion = congestion;
this.readBuffer = readBufferSize;
this.writeBuffer = writeBufferSize;
this.type = type;
this.seed = seed;
}
static fromJson(json={}) {
return new KcpStreamSettings(
json.mtu,
json.tti,
json.uplinkCapacity,
json.downlinkCapacity,
json.congestion,
json.readBufferSize,
json.writeBufferSize,
ObjectUtil.isEmpty(json.header) ? 'none' : json.header.type,
json.seed,
);
}
toJson() {
return {
mtu: this.mtu,
tti: this.tti,
uplinkCapacity: this.upCap,
downlinkCapacity: this.downCap,
congestion: this.congestion,
readBufferSize: this.readBuffer,
writeBufferSize: this.writeBuffer,
header: {
type: this.type,
},
seed: this.seed,
};
}
}
class WsStreamSettings extends CommonClass {
constructor(path='/', host='') {
super();
this.path = path;
this.host = host;
}
static fromJson(json={}) {
return new WsStreamSettings(
json.path,
json.headers && !ObjectUtil.isEmpty(json.headers.Host) ? json.headers.Host : '',
);
}
toJson() {
return {
path: this.path,
headers: ObjectUtil.isEmpty(this.host) ? undefined : {Host: this.host},
};
}
}
class HttpStreamSettings extends CommonClass {
constructor(path='/', host='') {
super();
this.path = path;
this.host = host;
}
static fromJson(json={}) {
return new HttpStreamSettings(
json.path,
json.host ? json.host.join(',') : '',
);
}
toJson() {
return {
path: this.path,
host: ObjectUtil.isEmpty(this.host) ? [''] : this.host.split(','),
}
}
}
class QuicStreamSettings extends CommonClass {
constructor(security='none',
key='', type='none') {
super();
this.security = security;
this.key = key;
this.type = type;
}
static fromJson(json={}) {
return new QuicStreamSettings(
json.security,
json.key,
json.header ? json.header.type : 'none',
);
}
toJson() {
return {
security: this.security,
key: this.key,
header: {
type: this.type,
}
}
}
}
class GrpcStreamSettings extends CommonClass {
constructor(serviceName="", multiMode=false) {
super();
this.serviceName = serviceName;
this.multiMode = multiMode;
}
static fromJson(json={}) {
return new GrpcStreamSettings(json.serviceName, json.multiMode);
}
toJson() {
return {
serviceName: this.serviceName,
multiMode: this.multiMode,
}
}
}
class TlsStreamSettings extends CommonClass {
constructor(serverName='',
alpn=[],
fingerprint = '',
allowInsecure = false) {
super();
this.serverName = serverName;
this.alpn = alpn;
this.fingerprint = fingerprint;
this.allowInsecure = allowInsecure;
}
static fromJson(json={}) {
return new TlsStreamSettings(
json.serverName,
json.alpn,
json.fingerprint,
json.allowInsecure,
);
}
toJson() {
return {
serverName: this.serverName,
alpn: this.alpn,
fingerprint: this.fingerprint,
allowInsecure: this.allowInsecure,
};
}
}
class RealityStreamSettings extends CommonClass {
constructor(publicKey = '', fingerprint = '', serverName = '', shortId = '', spiderX = '/') {
super();
this.publicKey = publicKey;
this.fingerprint = fingerprint;
this.serverName = serverName;
this.shortId = shortId
this.spiderX = spiderX;
}
static fromJson(json = {}) {
return new RealityStreamSettings(
json.publicKey,
json.fingerprint,
json.serverName,
json.shortId,
json.spiderX,
);
}
toJson() {
return {
publicKey: this.publicKey,
fingerprint: this.fingerprint,
serverName: this.serverName,
shortId: this.shortId,
spiderX: this.spiderX,
};
}
};
class StreamSettings extends CommonClass {
constructor(network='tcp',
security='none',
tlsSettings=new TlsStreamSettings(),
realitySettings = new RealityStreamSettings(),
tcpSettings=new TcpStreamSettings(),
kcpSettings=new KcpStreamSettings(),
wsSettings=new WsStreamSettings(),
httpSettings=new HttpStreamSettings(),
quicSettings=new QuicStreamSettings(),
grpcSettings=new GrpcStreamSettings(),
) {
super();
this.network = network;
this.security = security;
this.tls = tlsSettings;
this.reality = realitySettings;
this.tcp = tcpSettings;
this.kcp = kcpSettings;
this.ws = wsSettings;
this.http = httpSettings;
this.quic = quicSettings;
this.grpc = grpcSettings;
}
get isTls() {
return this.security === 'tls';
}
get isReality() {
return this.security === "reality";
}
static fromJson(json={}) {
return new StreamSettings(
json.network,
json.security,
TlsStreamSettings.fromJson(json.tlsSettings),
RealityStreamSettings.fromJson(json.realitySettings),
TcpStreamSettings.fromJson(json.tcpSettings),
KcpStreamSettings.fromJson(json.kcpSettings),
WsStreamSettings.fromJson(json.wsSettings),
HttpStreamSettings.fromJson(json.httpSettings),
QuicStreamSettings.fromJson(json.quicSettings),
GrpcStreamSettings.fromJson(json.grpcSettings),
);
}
toJson() {
const network = this.network;
return {
network: network,
security: this.security,
tlsSettings: this.security == 'tls' ? this.tls.toJson() : undefined,
realitySettings: this.security == 'reality' ? this.reality.toJson() : undefined,
tcpSettings: network === 'tcp' ? this.tcp.toJson() : undefined,
kcpSettings: network === 'kcp' ? this.kcp.toJson() : undefined,
wsSettings: network === 'ws' ? this.ws.toJson() : undefined,
httpSettings: network === 'http' ? this.http.toJson() : undefined,
quicSettings: network === 'quic' ? this.quic.toJson() : undefined,
grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined,
};
}
}
class Outbound extends CommonClass {
constructor(
tag='',
protocol=Protocols.VMess,
settings=null,
streamSettings = new StreamSettings(),
) {
super();
this.tag = tag;
this._protocol = protocol;
this.settings = settings == null ? Outbound.Settings.getSettings(protocol) : settings;
this.stream = streamSettings;
}
get protocol() {
return this._protocol;
}
set protocol(protocol) {
this._protocol = protocol;
this.settings = Outbound.Settings.getSettings(protocol);
this.stream = new StreamSettings();
}
canEnableTls() {
if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan].includes(this.protocol)) return false;
return ["tcp", "ws", "http", "quic", "grpc"].includes(this.stream.network);
}
//this is used for xtls-rprx-vision
canEnableTlsFlow() {
if ((this.stream.security != 'none') && (this.stream.network === "tcp")) {
return this.protocol === Protocols.VLESS;
}
return false;
}
canEnableReality() {
if (![Protocols.VLESS, Protocols.Trojan].includes(this.protocol)) return false;
return ["tcp", "http", "grpc"].includes(this.stream.network);
}
canEnableStream() {
return [Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(this.protocol);
}
hasVnext() {
return [Protocols.VMess, Protocols.VLESS].includes(this.protocol);
}
hasServers() {
return [Protocols.Trojan, Protocols.Shadowsocks, Protocols.Socks, Protocols.HTTP].includes(this.protocol);
}
hasAddressPort() {
return [
Protocols.DNS,
Protocols.VMess,
Protocols.VLESS,
Protocols.Trojan,
Protocols.Shadowsocks,
Protocols.Socks,
Protocols.HTTP
].includes(this.protocol);
}
hasUsername() {
return [Protocols.Socks, Protocols.HTTP].includes(this.protocol);
}
static fromJson(json={}) {
return new Outbound(
json.tag,
json.protocol,
Outbound.Settings.fromJson(json.protocol, json.settings),
StreamSettings.fromJson(json.streamSettings),
)
}
toJson() {
return {
tag: this.tag == '' ? undefined : this.tag,
protocol: this.protocol,
settings: this.settings instanceof CommonClass ? this.settings.toJson() : this.settings,
streamSettings: this.canEnableStream() ? this.stream.toJson() : undefined,
};
}
static fromLink(link) {
data = link.split('://');
if(data.length !=2) return null;
switch(data[0].toLowerCase()){
case Protocols.VMess:
return this.fromVmessLink(JSON.parse(atob(data[1])));
case Protocols.VLESS:
case Protocols.Trojan:
case 'ss':
return this.fromParamLink(link);
default:
return null;
}
}
static fromVmessLink(json={}){
let stream = new StreamSettings(json.net, json.tls);
let network = json.net;
if (network === 'tcp') {
stream.tcp = new TcpStreamSettings(
json.type,
json.host ? json.host.split(','): [],
json.path ? json.path.split(','): []);
} else if (network === 'kcp') {
stream.kcp = new KcpStreamSettings();
stream.type = json.type;
stream.seed = json.path;
} else if (network === 'ws') {
stream.ws = new WsStreamSettings(json.path,json.host);
} else if (network === 'http' || network == 'h2') {
stream.network = 'http'
stream.http = new HttpStreamSettings(
json.path,
json.host ? json.host.split(',') : []);
} else if (network === 'quic') {
stream.quic = new QuicStreamSettings(
json.host ? json.host : 'none',
json.path,
json.type ? json.type : 'none');
} else if (network === 'grpc') {
stream.grpc = new GrpcStreamSettings(json.path, json.type == 'multi');
}
if(json.tls && json.tls == 'tls'){
stream.tls = new TlsStreamSettings(
json.sni,
json.alpn ? json.alpn.split(',') : [],
json.fp,
json.allowInsecure);
}
return new Outbound(json.ps, Protocols.VMess, new Outbound.VmessSettings(json.add, json.port, json.id), stream);
}
static fromParamLink(link){
const url = new URL(link);
let type = url.searchParams.get('type');
let security = url.searchParams.get('security') ?? 'none';
let stream = new StreamSettings(type, security);
let headerType = url.searchParams.get('headerType');
let host = url.searchParams.get('host');
let path = url.searchParams.get('path');
if (type === 'tcp') {
stream.tcp = new TcpStreamSettings(headerType ?? 'none', host, path);
} else if (type === 'kcp') {
stream.kcp = new KcpStreamSettings();
stream.kcp.type = headerType ?? 'none';
stream.kcp.seed = path;
} else if (type === 'ws') {
stream.ws = new WsStreamSettings(path,host);
} else if (type === 'http' || type == 'h2') {
stream.http = new HttpStreamSettings(path,host);
} else if (type === 'quic') {
stream.quic = new QuicStreamSettings(
url.searchParams.get('quicSecurity') ?? 'none',
url.searchParams.get('key') ?? '',
headerType ?? 'none');
} else if (type === 'grpc') {
stream.grpc = new GrpcStreamSettings(url.searchParams.get('serviceName') ?? '', url.searchParams.get('mode') == 'multi');
}
if(security == 'tls'){
let fp=url.searchParams.get('fp') ?? 'none';
let alpn=url.searchParams.get('alpn');
let allowInsecure=url.searchParams.get('allowInsecure');
let sni=url.searchParams.get('sni') ?? '';
stream.tls = new TlsStreamSettings(sni, alpn ? alpn.split(',') : [], fp, allowInsecure == 1);
}
if(security == 'reality'){
let pbk=url.searchParams.get('pbk');
let fp=url.searchParams.get('fp');
let sni=url.searchParams.get('sni') ?? '';
let sid=url.searchParams.get('sid') ?? '';
let spx=url.searchParams.get('spx') ?? '';
stream.tls = new RealityStreamSettings(pbk, fp, sni, sid, spx);
}
let data = link.split('?');
if(data.length != 2) return null;
const regex = /([^@]+):\/\/([^@]+)@([^:]+):(\d+)\?(.*)$/;
const match = link.match(regex);
if (!match) return null;
let [, protocol, userData, address, port, ] = match;
port *= 1;
if(protocol == 'ss') {
protocol = 'shadowsocks';
userData = atob(userData).split(':');
}
var settings;
switch(protocol){
case Protocols.VLESS:
settings = new Outbound.VLESSSettings(address, port, userData, url.searchParams.get('flow') ?? '');
break;
case Protocols.Trojan:
settings = new Outbound.TrojanSettings(address, port, userData);
break;
case Protocols.Shadowsocks:
let method = userData.splice(0,1)[0];
settings = new Outbound.ShadowsocksSettings(address, port, userData.join(":"), method, true);
break;
default:
return null;
}
let remark = decodeURIComponent(url.hash);
// Remove '#' from url.hash
remark = remark.length > 0 ? remark.substring(1) : 'out-' + protocol + '-' + port;
return new Outbound(remark, protocol, settings, stream);
}
}
Outbound.Settings = class extends CommonClass {
constructor(protocol) {
super();
this.protocol = protocol;
}
static getSettings(protocol) {
switch (protocol) {
case Protocols.Freedom: return new Outbound.FreedomSettings();
case Protocols.Blackhole: return new Outbound.BlackholeSettings();
case Protocols.DNS: return new Outbound.DNSSettings();
case Protocols.VMess: return new Outbound.VmessSettings();
case Protocols.VLESS: return new Outbound.VLESSSettings();
case Protocols.Trojan: return new Outbound.TrojanSettings();
case Protocols.Shadowsocks: return new Outbound.ShadowsocksSettings();
case Protocols.Socks: return new Outbound.SocksSettings();
case Protocols.HTTP: return new Outbound.HttpSettings();
default: return null;
}
}
static fromJson(protocol, json) {
switch (protocol) {
case Protocols.Freedom: return Outbound.FreedomSettings.fromJson(json);
case Protocols.Blackhole: return Outbound.BlackholeSettings.fromJson(json);
case Protocols.DNS: return Outbound.DNSSettings.fromJson(json);
case Protocols.VMess: return Outbound.VmessSettings.fromJson(json);
case Protocols.VLESS: return Outbound.VLESSSettings.fromJson(json);
case Protocols.Trojan: return Outbound.TrojanSettings.fromJson(json);
case Protocols.Shadowsocks: return Outbound.ShadowsocksSettings.fromJson(json);
case Protocols.Socks: return Outbound.SocksSettings.fromJson(json);
case Protocols.HTTP: return Outbound.HttpSettings.fromJson(json);
default: return null;
}
}
toJson() {
return {};
}
};
Outbound.FreedomSettings = class extends CommonClass {
constructor(domainStrategy='', fragment={}) {
super();
this.domainStrategy = domainStrategy;
this.fragment = fragment;
}
static fromJson(json={}) {
return new Outbound.FreedomSettings(
json.domainStrategy,
json.fragment ? Outbound.FreedomSettings.Fragment.fromJson(json.fragment) : undefined,
);
}
toJson() {
return {
domainStrategy: ObjectUtil.isEmpty(this.domainStrategy) ? undefined : this.domainStrategy,
fragment: Object.keys(this.fragment).length === 0 ? undefined : this.fragment,
};
}
};
Outbound.FreedomSettings.Fragment = class extends CommonClass {
constructor(packets='1-3',length='',interval=''){
super();
this.packets = packets;
this.length = length;
this.interval = interval;
}
static fromJson(json={}) {
return new Outbound.FreedomSettings.Fragment(
json.packets,
json.length,
json.interval,
);
}
};
Outbound.BlackholeSettings = class extends CommonClass {
constructor(type) {
super();
this.type;
}
static fromJson(json={}) {
return new Outbound.BlackholeSettings(
json.response ? json.response.type : undefined,
);
}
toJson() {
return {
response: ObjectUtil.isEmpty(this.type) ? undefined : {type: this.type},
};
}
};
Outbound.DNSSettings = class extends CommonClass {
constructor(network='udp', address='1.1.1.1', port=53) {
super();
this.network = network;
this.address = address;
this.port = port;
}
static fromJson(json={}){
return new Outbound.DNSSettings(
json.network,
json.address,
json.port,
);
}
};
Outbound.VmessSettings = class extends CommonClass {
constructor(address, port, id) {
super();
this.address = address;
this.port = port;
this.id = id;
}
static fromJson(json={}) {
if(ObjectUtil.isArrEmpty(json.vnext)) return new Outbound.VmessSettings();
return new Outbound.VmessSettings(
json.vnext[0].address,
json.vnext[0].port,
json.vnext[0].users[0].id,
);
}
toJson() {
return {
vnext: [{
address: this.address,
port: this.port,
users: [{id: this.id}],
}],
};
}
};
Outbound.VLESSSettings = class extends CommonClass {
constructor(address, port, id, flow, encryption='none') {
super();
this.address = address;
this.port = port;
this.id = id;
this.flow = flow;
this.encryption = encryption
}
static fromJson(json={}) {
if(ObjectUtil.isArrEmpty(json.vnext)) return new Outbound.VLESSSettings();
return new Outbound.VLESSSettings(
json.vnext[0].address,
json.vnext[0].port,
json.vnext[0].users[0].id,
json.vnext[0].users[0].flow,
json.vnext[0].users[0].encryption,
);
}
toJson() {
return {
vnext: [{
address: this.address,
port: this.port,
users: [{id: this.id, flow: this.flow, encryption: 'none',}],
}],
};
}
};
Outbound.TrojanSettings = class extends CommonClass {
constructor(address, port, password) {
super();
this.address = address;
this.port = port;
this.password = password;
}
static fromJson(json={}) {
if(ObjectUtil.isArrEmpty(json.servers)) return new Outbound.TrojanSettings();
return new Outbound.TrojanSettings(
json.servers[0].address,
json.servers[0].port,
json.servers[0].password,
);
}
toJson() {
return {
servers: [{
address: this.address,
port: this.port,
password: this.password,
}],
};
}
};
Outbound.ShadowsocksSettings = class extends CommonClass {
constructor(address, port, password, method, uot) {
super();
this.address = address;
this.port = port;
this.password = password;
this.method = method;
this.uot = uot;
}
static fromJson(json={}) {
let servers = json.servers;
if(ObjectUtil.isArrEmpty(servers)) servers=[{}];
return new Outbound.ShadowsocksSettings(
servers[0].address,
servers[0].port,
servers[0].password,
servers[0].method,
servers[0].uot,
);
}
toJson() {
return {
servers: [{
address: this.address,
port: this.port,
password: this.password,
method: this.method,
uot: this.uot,
}],
};
}
};
Outbound.SocksSettings = class extends CommonClass {
constructor(address, port, user, password) {
super();
this.address = address;
this.port = port;
this.user = user;
this.password = password;
}
static fromJson(json={}) {
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].password) ? '' : servers[0].users[0].password,
);
}
toJson() {
return {
servers: [{
address: this.address,
port: this.port,
users: ObjectUtil.isEmpty(this.user) ? [] : [{user: this.user, password: this.password}],
}],
};
}
};
Outbound.HttpSettings = class extends CommonClass {
constructor(address, port, user, password) {
super();
this.address = address;
this.port = port;
this.user = user;
this.password = password;
}
static fromJson(json={}) {
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].password) ? '' : servers[0].users[0].password,
);
}
toJson() {
return {
servers: [{
address: this.address,
port: this.port,
users: ObjectUtil.isEmpty(this.user) ? [] : [{user: this.user, password: this.password}],
}],
};
}
};

View file

@ -0,0 +1,48 @@
class AllSetting {
constructor(data) {
this.webListen = "";
this.webDomain = "";
this.webPort = 2053;
this.webCertFile = "";
this.webKeyFile = "";
this.webBasePath = "/";
this.sessionMaxAge = "";
this.pageSize = 0;
this.expireDiff = "";
this.trafficDiff = "";
this.remarkModel = "-ieo";
this.tgBotEnable = false;
this.tgBotToken = "";
this.tgBotChatId = "";
this.tgRunTime = "@daily";
this.tgBotBackup = false;
this.tgBotLoginNotify = false;
this.tgCpu = "";
this.tgLang = "en-US";
this.xrayTemplateConfig = "";
this.secretEnable = false;
this.subEnable = false;
this.subListen = "";
this.subPort = "2096";
this.subPath = "/sub/";
this.subDomain = "";
this.subCertFile = "";
this.subKeyFile = "";
this.subUpdates = 0;
this.subEncrypt = true;
this.subShowInfo = false;
this.subURI = '';
this.timeLocation = "Asia/Tehran";
if (data == null) {
return
}
ObjectUtil.cloneProps(this, data);
}
equals(other) {
return ObjectUtil.equals(this, other);
}
}

View file

@ -8,13 +8,6 @@ const Protocols = {
HTTP: 'http',
};
const VmessMethods = {
AES_128_GCM: 'aes-128-gcm',
CHACHA20_POLY1305: 'chacha20-poly1305',
AUTO: 'auto',
NONE: 'none',
};
const SSMethods = {
AES_256_GCM: 'aes-256-gcm',
AES_128_GCM: 'aes-128-gcm',
@ -91,7 +84,6 @@ const SNIFFING_OPTION = {
};
Object.freeze(Protocols);
Object.freeze(VmessMethods);
Object.freeze(SSMethods);
Object.freeze(XTLS_FLOW_CONTROL);
Object.freeze(TLS_FLOW_CONTROL);
@ -422,7 +414,7 @@ class HttpStreamSettings extends XrayCommonClass {
}
class QuicStreamSettings extends XrayCommonClass {
constructor(security=VmessMethods.NONE,
constructor(security='none',
key=RandomUtil.randomSeq(10), type='none') {
super();
this.security = security;
@ -586,27 +578,21 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
};
TlsStreamSettings.Settings = class extends XrayCommonClass {
constructor(allowInsecure = false, fingerprint = '', serverName = '', domains = []) {
constructor(allowInsecure = false, fingerprint = '') {
super();
this.allowInsecure = allowInsecure;
this.fingerprint = fingerprint;
this.serverName = serverName;
this.domains = domains;
}
static fromJson(json = {}) {
return new TlsStreamSettings.Settings(
json.allowInsecure,
json.fingerprint,
json.serverName,
json.domains,
);
}
toJson() {
return {
allowInsecure: this.allowInsecure,
fingerprint: this.fingerprint,
serverName: this.serverName,
domains: this.domains,
};
}
};
@ -700,21 +686,18 @@ XtlsStreamSettings.Cert = class extends XrayCommonClass {
};
XtlsStreamSettings.Settings = class extends XrayCommonClass {
constructor(allowInsecure = false, serverName = '') {
constructor(allowInsecure = false) {
super();
this.allowInsecure = allowInsecure;
this.serverName = serverName;
}
static fromJson(json = {}) {
return new XtlsStreamSettings.Settings(
json.allowInsecure,
json.servername,
);
}
toJson() {
return {
allowInsecure: this.allowInsecure,
serverName: this.serverName,
};
}
};
@ -781,18 +764,16 @@ class RealityStreamSettings extends XrayCommonClass {
}
RealityStreamSettings.Settings = class extends XrayCommonClass {
constructor(publicKey = '', fingerprint = UTLS_FINGERPRINT.UTLS_FIREFOX, serverName = '', spiderX= '/') {
constructor(publicKey = '', fingerprint = UTLS_FINGERPRINT.UTLS_FIREFOX, spiderX= '/') {
super();
this.publicKey = publicKey;
this.fingerprint = fingerprint;
this.serverName = serverName;
this.spiderX = spiderX;
}
static fromJson(json = {}) {
return new RealityStreamSettings.Settings(
json.publicKey,
json.fingerprint,
json.serverName,
json.spiderX,
);
}
@ -800,7 +781,6 @@ RealityStreamSettings.Settings = class extends XrayCommonClass {
return {
publicKey: this.publicKey,
fingerprint: this.fingerprint,
serverName: this.serverName,
spiderX: this.spiderX,
};
}
@ -837,6 +817,7 @@ class SockoptStreamSettings extends XrayCommonClass {
class StreamSettings extends XrayCommonClass {
constructor(network='tcp',
security='none',
externalProxy = [],
tlsSettings=new TlsStreamSettings(),
xtlsSettings=new XtlsStreamSettings(),
realitySettings = new RealityStreamSettings(),
@ -851,6 +832,7 @@ class StreamSettings extends XrayCommonClass {
super();
this.network = network;
this.security = security;
this.externalProxy = externalProxy;
this.tls = tlsSettings;
this.xtls = xtlsSettings;
this.reality = realitySettings;
@ -909,10 +891,10 @@ class StreamSettings extends XrayCommonClass {
}
static fromJson(json={}) {
return new StreamSettings(
json.network,
json.security,
json.externalProxy,
TlsStreamSettings.fromJson(json.tlsSettings),
XtlsStreamSettings.fromJson(json.xtlsSettings),
RealityStreamSettings.fromJson(json.realitySettings),
@ -931,6 +913,7 @@ class StreamSettings extends XrayCommonClass {
return {
network: network,
security: this.security,
externalProxy: this.externalProxy,
tlsSettings: this.isTls ? this.tls.toJson() : undefined,
xtlsSettings: this.isXtls ? this.xtls.toJson() : undefined,
realitySettings: this.isReality ? this.reality.toJson() : undefined,
@ -990,6 +973,16 @@ class Inbound extends XrayCommonClass {
return this.clientStats;
}
get clients() {
switch (this.protocol) {
case Protocols.VMESS: return this.settings.vmesses;
case Protocols.VLESS: return this.settings.vlesses;
case Protocols.TROJAN: return this.settings.trojans;
case Protocols.SHADOWSOCKS: return this.isSSMultiUser ? this.settings.shadowsockses : null;
default: return null;
}
}
get protocol() {
return this._protocol;
}
@ -1087,9 +1080,9 @@ class Inbound extends XrayCommonClass {
}
get serverName() {
if (this.stream.isTls || this.stream.isXtls || this.stream.isReality) {
return this.stream.tls.server;
}
if (this.stream.isTls) return this.stream.tls.server;
if (this.stream.isXtls) return this.stream.xtls.server;
if (this.stream.isReality) return this.stream.reality.serverNames;
return "";
}
@ -1140,118 +1133,39 @@ class Inbound extends XrayCommonClass {
}
isExpiry(index) {
switch (this.protocol) {
case Protocols.VMESS:
if(this.settings.vmesses[index].expiryTime > 0)
return this.settings.vmesses[index].expiryTime < new Date().getTime();
return false
case Protocols.VLESS:
if(this.settings.vlesses[index].expiryTime > 0)
return this.settings.vlesses[index].expiryTime < new Date().getTime();
return false
case Protocols.TROJAN:
if(this.settings.trojans[index].expiryTime > 0)
return this.settings.trojans[index].expiryTime < new Date().getTime();
return false
case Protocols.SHADOWSOCKS:
if(this.settings.shadowsockses.length > 0 && this.settings.shadowsockses[index].expiryTime > 0)
return this.settings.shadowsockses[index].expiryTime < new Date().getTime();
return false
default:
return false;
}
let exp = this.clients[index].expiryTime;
return exp > 0 ? exp < new Date().getTime() : false;
}
canEnableTls() {
switch (this.protocol) {
case Protocols.VMESS:
case Protocols.VLESS:
case Protocols.TROJAN:
break;
default:
return false;
}
switch (this.network) {
case "tcp":
case "ws":
case "http":
case "quic":
case "grpc":
return true;
default:
return false;
}
if(![Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(this.protocol)) return false;
return ["tcp", "ws", "http", "quic", "grpc"].includes(this.network);
}
canEnableReality() {
switch (this.protocol) {
case Protocols.VLESS:
case Protocols.TROJAN:
break;
default:
return false;
}
switch (this.network) {
case "tcp":
case "http":
case "grpc":
return true;
default:
return false;
}
if(![Protocols.VLESS, Protocols.TROJAN].includes(this.protocol)) return false;
return ["tcp", "http", "grpc"].includes(this.network);
}
//this is used for xtls-rprx-vision
canEnableTlsFlow() {
if (((this.stream.security === 'tls') || (this.stream.security === 'reality')) && (this.network === "tcp")) {
switch (this.protocol) {
case Protocols.VLESS:
return true;
default:
return false;
}
return this.protocol === Protocols.VLESS;
}
return false;
}
canSetTls() {
return this.canEnableTls();
}
canEnableXtls() {
switch (this.protocol) {
case Protocols.VLESS:
case Protocols.TROJAN:
break;
default:
return false;
}
if(![Protocols.VLESS, Protocols.TROJAN].includes(this.protocol)) return false;
return this.network === "tcp";
}
canEnableStream() {
switch (this.protocol) {
case Protocols.VMESS:
case Protocols.VLESS:
case Protocols.TROJAN:
case Protocols.SHADOWSOCKS:
return true;
default:
return false;
}
return [Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(this.protocol);
}
canSniffing() {
switch (this.protocol) {
case Protocols.VMESS:
case Protocols.VLESS:
case Protocols.TROJAN:
case Protocols.SHADOWSOCKS:
return true;
default:
return false;
}
return [Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(this.protocol);
}
reset() {
@ -1264,19 +1178,20 @@ class Inbound extends XrayCommonClass {
this.sniffing = new Sniffing();
}
genVmessLink(address='', remark='', clientIndex=0) {
genVmessLink(address='', port=this.port, forceTls, remark='', clientId) {
if (this.protocol !== Protocols.VMESS) {
return '';
}
const security = forceTls == 'same' ? this.stream.security : forceTls;
let obj = {
v: '2',
ps: remark,
add: address,
port: this.port,
id: this.settings.vmesses[clientIndex].id,
port: port,
id: clientId,
net: this.stream.network,
type: 'none',
tls: this.stream.security,
tls: security,
};
let network = this.stream.network;
if (network === 'tcp') {
@ -1316,12 +1231,9 @@ class Inbound extends XrayCommonClass {
}
}
if (this.stream.security === 'tls') {
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
obj.add = this.stream.tls.server;
}
if (!ObjectUtil.isEmpty(this.stream.tls.settings.serverName)){
obj.sni = this.stream.tls.settings.serverName;
if (security === 'tls') {
if (!ObjectUtil.isEmpty(this.stream.tls.sni)){
obj.sni = this.stream.tls.server;
}
if (!ObjectUtil.isEmpty(this.stream.tls.settings.fingerprint)){
obj.fp = this.stream.tls.settings.fingerprint;
@ -1337,11 +1249,10 @@ class Inbound extends XrayCommonClass {
return 'vmess://' + base64(JSON.stringify(obj, null, 2));
}
genVLESSLink(address = '', remark='', clientIndex=0) {
const settings = this.settings;
const uuid = settings.vlesses[clientIndex].id;
const port = this.port;
genVLESSLink(address = '', port=this.port, forceTls, remark='', clientId, flow) {
const uuid = clientId;
const type = this.stream.network;
const security = forceTls == 'same' ? this.stream.security : forceTls;
const params = new Map();
params.set("type", this.stream.network);
switch (type) {
@ -1392,58 +1303,51 @@ class Inbound extends XrayCommonClass {
break;
}
if (this.tls) {
if (security === 'tls') {
params.set("security", "tls");
params.set("fp" , this.stream.tls.settings.fingerprint);
params.set("alpn", this.stream.tls.alpn);
if(this.stream.tls.settings.allowInsecure){
params.set("allowInsecure", "1");
}
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
address = this.stream.tls.server;
}
if (this.stream.tls.settings.serverName !== ''){
params.set("sni", this.stream.tls.settings.serverName);
}
if (type === "tcp" && this.settings.vlesses[clientIndex].flow.length > 0) {
params.set("flow", this.settings.vlesses[clientIndex].flow);
if (this.stream.isTls){
params.set("fp" , this.stream.tls.settings.fingerprint);
params.set("alpn", this.stream.tls.alpn);
if(this.stream.tls.settings.allowInsecure){
params.set("allowInsecure", "1");
}
if (!ObjectUtil.isEmpty(this.stream.tls.server)){
params.set("sni", this.stream.tls.server);
}
if (type == "tcp" && !ObjectUtil.isEmpty(flow)) {
params.set("flow", flow);
}
}
}
else if (this.xtls) {
else if (security === 'xtls') {
params.set("security", "xtls");
params.set("alpn", this.stream.xtls.alpn);
if(this.stream.xtls.settings.allowInsecure){
params.set("allowInsecure", "1");
}
if (!ObjectUtil.isEmpty(this.stream.xtls.server)) {
address = this.stream.xtls.server;
if (!ObjectUtil.isEmpty(this.stream.xtls.server)){
params.set("sni", this.stream.xtls.server);
}
if (this.stream.xtls.settings.serverName !== ''){
params.set("sni", this.stream.xtls.settings.serverName);
}
params.set("flow", this.settings.vlesses[clientIndex].flow);
params.set("flow", flow);
}
else if (this.reality) {
else if (security === 'reality') {
params.set("security", "reality");
params.set("fp", this.stream.reality.settings.fingerprint);
params.set("pbk", this.stream.reality.settings.publicKey);
params.set("fp", this.stream.reality.settings.fingerprint);
if (!ObjectUtil.isArrEmpty(this.stream.reality.serverNames)) {
params.set("sni", this.stream.reality.serverNames.split(",")[0]);
}
if (this.stream.network === 'tcp' && !ObjectUtil.isEmpty(this.settings.vlesses[clientIndex].flow)) {
params.set("flow", this.settings.vlesses[clientIndex].flow);
}
if (this.stream.reality.shortIds.length > 0) {
params.set("sid", this.stream.reality.shortIds.split(",")[0]);
}
if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) {
address = this.stream.reality.settings.serverName;
}
if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) {
params.set("spx", this.stream.reality.settings.spiderX);
}
if (type == 'tcp' && !ObjectUtil.isEmpty(flow)) {
params.set("flow", flow);
}
}
else {
@ -1459,10 +1363,10 @@ class Inbound extends XrayCommonClass {
return url.toString();
}
genSSLink(address='', remark='', clientIndex = 0) {
genSSLink(address='', port=this.port, forceTls, remark='', clientPassword) {
let settings = this.settings;
const port = this.port;
const type = this.stream.network;
const security = forceTls == 'same' ? this.stream.security : forceTls;
const params = new Map();
params.set("type", this.stream.network);
switch (type) {
@ -1513,11 +1417,26 @@ class Inbound extends XrayCommonClass {
break;
}
if (security === 'tls') {
params.set("security", "tls");
if (this.stream.isTls){
params.set("fp" , this.stream.tls.settings.fingerprint);
params.set("alpn", this.stream.tls.alpn);
if(this.stream.tls.settings.allowInsecure){
params.set("allowInsecure", "1");
}
if (!ObjectUtil.isEmpty(this.stream.tls.server)){
params.set("sni", this.stream.tls.server);
}
}
}
let password = new Array();
if (this.isSS2022) password.push(settings.password);
if (this.isSSMultiUser) password.push(settings.shadowsockses[clientIndex].password);
if (this.isSSMultiUser) password.push(clientPassword);
let link = `ss://${safeBase64(settings.method + ':' + password.join(':'))}@${address}:${this.port}`;
let link = `ss://${safeBase64(settings.method + ':' + password.join(':'))}@${address}:${port}`;
const url = new URL(link);
for (const [key, value] of params) {
url.searchParams.set(key, value)
@ -1526,9 +1445,8 @@ class Inbound extends XrayCommonClass {
return url.toString();
}
genTrojanLink(address = '', remark = '', clientIndex = 0) {
let settings = this.settings;
const port = this.port;
genTrojanLink(address = '', port=this.port, forceTls, remark = '', clientPassword) {
const security = forceTls == 'same' ? this.stream.security : forceTls;
const type = this.stream.network;
const params = new Map();
params.set("type", this.stream.network);
@ -1580,59 +1498,52 @@ class Inbound extends XrayCommonClass {
break;
}
if (this.tls) {
if (security === 'tls') {
params.set("security", "tls");
params.set("fp" , this.stream.tls.settings.fingerprint);
params.set("alpn", this.stream.tls.alpn);
if(this.stream.tls.settings.allowInsecure){
params.set("allowInsecure", "1");
if (this.stream.isTls){
params.set("fp" , this.stream.tls.settings.fingerprint);
params.set("alpn", this.stream.tls.alpn);
if(this.stream.tls.settings.allowInsecure){
params.set("allowInsecure", "1");
}
if (!ObjectUtil.isEmpty(this.stream.tls.server)){
params.set("sni", this.stream.tls.server);
}
}
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
address = this.stream.tls.server;
}
if (this.stream.tls.settings.serverName !== ''){
params.set("sni", this.stream.tls.settings.serverName);
}
}
else if (this.reality) {
else if (security === 'reality') {
params.set("security", "reality");
params.set("fp", this.stream.reality.settings.fingerprint);
params.set("pbk", this.stream.reality.settings.publicKey);
params.set("fp", this.stream.reality.settings.fingerprint);
if (!ObjectUtil.isArrEmpty(this.stream.reality.serverNames)) {
params.set("sni", this.stream.reality.serverNames.split(",")[0]);
}
if (this.stream.reality.shortIds.length > 0) {
params.set("sid", this.stream.reality.shortIds.split(",")[0]);
}
if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) {
address = this.stream.reality.settings.serverName;
}
if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) {
params.set("spx", this.stream.reality.settings.spiderX);
}
}
else if (this.xtls) {
else if (security === 'xtls') {
params.set("security", "xtls");
params.set("alpn", this.stream.xtls.alpn);
if(this.stream.xtls.settings.allowInsecure){
params.set("allowInsecure", "1");
}
if (!ObjectUtil.isEmpty(this.stream.xtls.server)) {
address = this.stream.xtls.server;
}
if (this.stream.xtls.settings.serverName !== ''){
params.set("sni", this.stream.xtls.settings.serverName);
}
params.set("flow", this.settings.trojans[clientIndex].flow);
params.set("flow", flow);
}
else {
params.set("security", "none");
}
const link = `trojan://${settings.trojans[clientIndex].password}@${address}:${this.port}`;
const link = `trojan://${clientPassword}@${address}:${port}`;
const url = new URL(link);
for (const [key, value] of params) {
url.searchParams.set(key, value)
@ -1641,38 +1552,63 @@ class Inbound extends XrayCommonClass {
return url.toString();
}
genLink(address='', remark='', clientIndex=0) {
genLink(address='', port=this.port, forceTls='same', remark='', client) {
switch (this.protocol) {
case Protocols.VMESS:
return this.genVmessLink(address, remark, clientIndex);
return this.genVmessLink(address, port, forceTls, remark, client.id);
case Protocols.VLESS:
return this.genVLESSLink(address, remark, clientIndex);
return this.genVLESSLink(address, port, forceTls, remark, client.id, client.flow);
case Protocols.SHADOWSOCKS:
return this.genSSLink(address, remark, clientIndex);
return this.genSSLink(address, port, forceTls, remark, this.isSSMultiUser ? client.password : '');
case Protocols.TROJAN:
return this.genTrojanLink(address, remark, clientIndex);
return this.genTrojanLink(address, port, forceTls, remark, client.password);
default: return '';
}
}
genInboundLinks(address = '', remark = '') {
let link = '';
switch (this.protocol) {
case Protocols.VMESS:
case Protocols.VLESS:
case Protocols.TROJAN:
case Protocols.SHADOWSOCKS:
JSON.parse(this.settings).clients.forEach((client,index) => {
if(this.tls && !ObjectUtil.isArrEmpty(this.stream.tls.settings.domains)){
this.stream.tls.settings.domains.forEach((domain) => {
link += this.genLink(domain.domain, [remark, client.email, domain.remark].filter(x => x.length > 0).join('-'), index) + '\r\n';
});
} else {
link += this.genLink(address, [remark, client.email].filter(x => x.length > 0).join('-'), index) + '\r\n';
}
genAllLinks(remark='', remarkModel = '-ieo', client){
let result = [];
let email = client ? client.email : '';
let addr = !ObjectUtil.isEmpty(this.listen) && this.listen !== "0.0.0.0" ? this.listen : location.hostname;
let port = this.port;
const separationChar = remarkModel.charAt(0);
const orderChars = remarkModel.slice(1);
let orders = {
'i': remark,
'e': client ? client.email : '',
'o': '',
};
if(ObjectUtil.isArrEmpty(this.stream.externalProxy)){
let r = orderChars.split('').map(char => orders[char]).filter(x => x.length > 0).join(separationChar);
result.push({
remark: r,
link: this.genLink(addr, port, 'same', r, client)
});
} else {
this.stream.externalProxy.forEach((ep) => {
orders['o'] = ep.remark;
let r = orderChars.split('').map(char => orders[char]).filter(x => x.length > 0).join(separationChar);
result.push({
remark: r,
link: this.genLink(ep.dest, ep.port, ep.forceTls, r, client)
});
return link;
default: return '';
});
}
return result;
}
genInboundLinks(remark = '', remarkModel = '-ieo') {
if(this.clients){
let links = [];
this.clients.forEach((client) => {
genAllLinks(remark,remarkModel,client).forEach(l => {
links.push(l.link);
})
});
return links.join('\r\n');
} else {
if(this.protocol == Protocols.SHADOWSOCKS && !this.isSSMultiUser) return this.genSSLink(this.listen, this.port, remark);
return '';
}
}
@ -1691,7 +1627,7 @@ class Inbound extends XrayCommonClass {
toJson() {
let streamSettings;
if (this.canEnableStream() || this.protocol === Protocols.TROJAN) {
if (this.canEnableStream()) {
streamSettings = this.stream.toJson();
}
return {
@ -1783,7 +1719,7 @@ Inbound.VmessSettings = class extends Inbound.Settings {
}
};
Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
constructor(id=RandomUtil.randomUUID(), email=RandomUtil.randomLowerAndNum(8),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16)) {
constructor(id=RandomUtil.randomUUID(), email=RandomUtil.randomLowerAndNum(8),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16), reset=0) {
super();
this.id = id;
this.email = email;
@ -1793,6 +1729,7 @@ Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
this.enable = enable;
this.tgId = tgId;
this.subId = subId;
this.reset = reset;
}
static fromJson(json={}) {
@ -1805,6 +1742,7 @@ Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
json.enable,
json.tgId,
json.subId,
json.reset,
);
}
get _expiryTime() {
@ -1838,10 +1776,10 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
constructor(protocol,
vlesses=[new Inbound.VLESSSettings.VLESS()],
decryption='none',
fallbacks=[],) {
fallbacks=[]) {
super(protocol);
this.vlesses = vlesses;
this.decryption = 'none'; // Using decryption is not implemented here
this.decryption = decryption;
this.fallbacks = fallbacks;
}
@ -1858,22 +1796,22 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
return new Inbound.VLESSSettings(
Protocols.VLESS,
json.clients.map(client => Inbound.VLESSSettings.VLESS.fromJson(client)),
'none',
Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks),
json.decryption || 'none',
json.fallbacks.map(fallback => Inbound.VLESSSettings.Fallback.fromJson(fallback)),
);
}
toJson() {
return {
clients: Inbound.VLESSSettings.toJsonArray(this.vlesses),
decryption: 'none',
decryption: this.decryption,
fallbacks: Inbound.VLESSSettings.toJsonArray(this.fallbacks),
};
}
};
Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
constructor(id=RandomUtil.randomUUID(), flow='', email=RandomUtil.randomLowerAndNum(8),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16)) {
constructor(id=RandomUtil.randomUUID(), flow='', email=RandomUtil.randomLowerAndNum(8),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16), reset=0) {
super();
this.id = id;
this.flow = flow;
@ -1884,6 +1822,7 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
this.enable = enable;
this.tgId = tgId;
this.subId = subId;
this.reset = reset;
}
static fromJson(json={}) {
@ -1897,6 +1836,7 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
json.enable,
json.tgId,
json.subId,
json.reset,
);
}
@ -1973,11 +1913,11 @@ Inbound.TrojanSettings = class extends Inbound.Settings {
this.fallbacks = fallbacks;
}
addTrojanFallback() {
addFallback() {
this.fallbacks.push(new Inbound.TrojanSettings.Fallback());
}
delTrojanFallback(index) {
delFallback(index) {
this.fallbacks.splice(index, 1);
}
@ -1985,18 +1925,19 @@ Inbound.TrojanSettings = class extends Inbound.Settings {
return new Inbound.TrojanSettings(
Protocols.TROJAN,
json.clients.map(client => Inbound.TrojanSettings.Trojan.fromJson(client)),
Inbound.TrojanSettings.Fallback.fromJson(json.fallbacks),);
}
json.fallbacks.map(fallback => Inbound.TrojanSettings.Fallback.fromJson(fallback))
);
}
toJson() {
return {
clients: Inbound.TrojanSettings.toJsonArray(this.trojans),
fallbacks: Inbound.TrojanSettings.toJsonArray(this.fallbacks),
fallbacks: Inbound.TrojanSettings.toJsonArray(this.fallbacks)
};
}
};
Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
constructor(password=RandomUtil.randomSeq(10), flow='', email=RandomUtil.randomLowerAndNum(8),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16)) {
constructor(password=RandomUtil.randomSeq(10), flow='', email=RandomUtil.randomLowerAndNum(8),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16), reset=0) {
super();
this.password = password;
this.flow = flow;
@ -2007,6 +1948,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
this.enable = enable;
this.tgId = tgId;
this.subId = subId;
this.reset = reset;
}
toJson() {
@ -2020,6 +1962,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
enable: this.enable,
tgId: this.tgId,
subId: this.subId,
reset: this.reset,
};
}
@ -2034,6 +1977,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
json.enable,
json.tgId,
json.subId,
json.reset,
);
}
@ -2138,7 +2082,7 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings {
};
Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
constructor(method='', password=RandomUtil.randomShadowsocksPassword(), email=RandomUtil.randomLowerAndNum(8),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16)) {
constructor(method='', password=RandomUtil.randomShadowsocksPassword(), email=RandomUtil.randomLowerAndNum(8),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16), reset=0) {
super();
this.method = method;
this.password = password;
@ -2149,6 +2093,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
this.enable = enable;
this.tgId = tgId;
this.subId = subId;
this.reset = reset;
}
toJson() {
@ -2162,6 +2107,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
enable: this.enable,
tgId: this.tgId,
subId: this.subId,
reset: this.reset,
};
}
@ -2176,6 +2122,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
json.enable,
json.tgId,
json.subId,
json.reset,
);
}

View file

@ -52,13 +52,15 @@ function safeBase64(str) {
function formatSecond(second) {
if (second < 60) {
return second.toFixed(0) + ' s';
return second.toFixed(0) + 's';
} else if (second < 3600) {
return (second / 60).toFixed(0) + ' m';
return (second / 60).toFixed(0) + 'm';
} else if (second < 3600 * 24) {
return (second / 3600).toFixed(0) + ' h';
return (second / 3600).toFixed(0) + 'h';
} else {
return (second / 3600 / 24).toFixed(0) + ' d';
day = Math.floor(second / 3600 / 24);
remain = ((second/3600) - (day*24)).toFixed(0);
return day + 'd' + (remain > 0 ? ' ' + remain + 'h' : '');
}
}
@ -72,7 +74,7 @@ function addZero(num) {
function toFixed(num, n) {
n = Math.pow(10, n);
return Math.round(num * n) / n;
return Math.floor(num * n) / n;
}
function debounce(fn, delay) {
@ -115,15 +117,52 @@ function setCookie(cname, cvalue, exdays) {
function usageColor(data, threshold, total) {
switch (true) {
case data === null:
return 'blue';
case total <= 0:
return 'blue';
return "purple";
case total < 0:
return "green";
case total == 0:
return "purple";
case data < total - threshold:
return 'cyan';
return "green";
case data < total:
return 'orange';
return "orange";
default:
return 'red';
return "red";
}
}
function clientUsageColor(clientStats, trafficDiff) {
switch (true) {
case !clientStats || clientStats.total == 0:
return "#7a316f"; // purple
case clientStats.up + clientStats.down < clientStats.total - trafficDiff:
return "#008771"; // Green
case clientStats.up + clientStats.down < clientStats.total:
return "#f37b24"; // Orange
default:
return "#cf3c3c"; // Red
}
}
function userExpiryColor(threshold, client, isDark = false) {
if (!client.enable) {
return isDark ? '#2c3950' : '#bcbcbc';
}
now = new Date().getTime(),
expiry = client.expiryTime;
switch (true) {
case expiry === null:
return "#7a316f"; // purple
case expiry < 0:
return "#008771"; // Green
case expiry == 0:
return "#7a316f"; // purple
case now < expiry - threshold:
return "#008771"; // Green
case now < expiry:
return "#f37b24"; // Orange
default:
return "#cf3c3c"; // Red
}
}

View file

@ -1,3 +1,21 @@
class Msg {
constructor(success, msg, obj) {
this.success = false;
this.msg = "";
this.obj = null;
if (success != null) {
this.success = success;
}
if (msg != null) {
this.msg = msg;
}
if (obj != null) {
this.obj = obj;
}
}
}
class HttpUtil {
static _handleMsg(msg) {
if (!(msg instanceof Msg)) {
@ -158,7 +176,7 @@ class ObjectUtil {
}
}
} else {
return obj.toString().toLowerCase().indexOf(key.toLowerCase()) >= 0;
return this.isEmpty(obj) ? false : obj.toString().toLowerCase().indexOf(key.toLowerCase()) >= 0;
}
return false;
}

View file

@ -38,6 +38,8 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
g.POST("/delDepletedClients/:id", a.delDepletedClients)
g.GET("/createbackup", a.createBackup)
g.POST("/onlines", a.onlines)
a.inboundController = NewInboundController(g)
}
@ -104,3 +106,7 @@ func (a *APIController) delDepletedClients(c *gin.Context) {
func (a *APIController) createBackup(c *gin.Context) {
a.Tgbot.SendBackupToAdmins()
}
func (a *APIController) onlines(c *gin.Context) {
a.inboundController.onlines(c)
}

View file

@ -1,6 +1,7 @@
package controller
import (
"encoding/json"
"fmt"
"strconv"
"x-ui/database/model"
@ -37,6 +38,8 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.POST("/resetAllTraffics", a.resetAllTraffics)
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
g.POST("/delDepletedClients/:id", a.delDepletedClients)
g.POST("/import", a.importInbound)
g.POST("/onlines", a.onlines)
}
func (a *InboundController) getInbounds(c *gin.Context) {
@ -265,6 +268,31 @@ func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
jsonMsg(c, "All traffics of client reseted", nil)
}
func (a *InboundController) importInbound(c *gin.Context) {
inbound := &model.Inbound{}
err := json.Unmarshal([]byte(c.PostForm("data")), inbound)
if err != nil {
jsonMsg(c, "Something went wrong!", err)
return
}
user := session.GetLoginUser(c)
inbound.Id = 0
inbound.UserId = user.Id
inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
for index := range inbound.ClientStats {
inbound.ClientStats[index].Id = 0
inbound.ClientStats[index].Enable = true
}
needRestart := false
inbound, needRestart, err = a.inboundService.AddInbound(inbound)
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.create"), inbound, err)
if err == nil && needRestart {
a.xrayService.SetToNeedRestart()
}
}
func (a *InboundController) delDepletedClients(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
@ -278,3 +306,7 @@ func (a *InboundController) delDepletedClients(c *gin.Context) {
}
jsonMsg(c, "All delpeted clients are deleted", nil)
}
func (a *InboundController) onlines(c *gin.Context) {
jsonObj(c, a.inboundService.GetOnlineClinets(), nil)
}

View file

@ -41,7 +41,7 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
g.POST("/update", a.updateSetting)
g.POST("/updateUser", a.updateUser)
g.POST("/restartPanel", a.restartPanel)
g.GET("/getDefaultJsonConfig", a.getDefaultJsonConfig)
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
g.POST("/updateUserSecret", a.updateSecret)
g.POST("/getUserSecret", a.getUserSecret)
}
@ -55,54 +55,12 @@ func (a *SettingController) getAllSetting(c *gin.Context) {
jsonObj(c, allSetting, nil)
}
func (a *SettingController) getDefaultJsonConfig(c *gin.Context) {
defaultJsonConfig, err := a.settingService.GetDefaultJsonConfig()
func (a *SettingController) getDefaultSettings(c *gin.Context) {
result, err := a.settingService.GetDefaultSettings(c.Request.Host)
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
jsonObj(c, defaultJsonConfig, nil)
}
func (a *SettingController) getDefaultSettings(c *gin.Context) {
type settingFunc func() (interface{}, error)
settings := map[string]settingFunc{
"expireDiff": func() (interface{}, error) { return a.settingService.GetExpireDiff() },
"trafficDiff": func() (interface{}, error) { return a.settingService.GetTrafficDiff() },
"defaultCert": func() (interface{}, error) { return a.settingService.GetCertFile() },
"defaultKey": func() (interface{}, error) { return a.settingService.GetKeyFile() },
"tgBotEnable": func() (interface{}, error) { return a.settingService.GetTgbotenabled() },
"subEnable": func() (interface{}, error) { return a.settingService.GetSubEnable() },
"subPort": func() (interface{}, error) { return a.settingService.GetSubPort() },
"subPath": func() (interface{}, error) { return a.settingService.GetSubPath() },
"subDomain": func() (interface{}, error) { return a.settingService.GetSubDomain() },
"subKeyFile": func() (interface{}, error) { return a.settingService.GetSubKeyFile() },
"subCertFile": func() (interface{}, error) { return a.settingService.GetSubCertFile() },
"subEncrypt": func() (interface{}, error) { return a.settingService.GetSubEncrypt() },
"subShowInfo": func() (interface{}, error) { return a.settingService.GetSubShowInfo() },
}
result := make(map[string]interface{})
for key, fn := range settings {
value, err := fn()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
result[key] = value
}
subTLS := false
if result["subKeyFile"] != "" || result["subCertFile"] != "" {
subTLS = true
}
result["subTLS"] = subTLS
delete(result, "subKeyFile")
delete(result, "subCertFile")
jsonObj(c, result, nil)
}
@ -169,3 +127,12 @@ func (a *SettingController) getUserSecret(c *gin.Context) {
jsonObj(c, user, nil)
}
}
func (a *SettingController) getDefaultXrayConfig(c *gin.Context) {
defaultJsonConfig, err := a.settingService.GetDefaultXrayConfig()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
jsonObj(c, defaultJsonConfig, nil)
}

View file

@ -0,0 +1,63 @@
package controller
import (
"x-ui/web/service"
"github.com/gin-gonic/gin"
)
type XraySettingController struct {
XraySettingService service.XraySettingService
SettingService service.SettingService
InboundService service.InboundService
XrayService service.XrayService
}
func NewXraySettingController(g *gin.RouterGroup) *XraySettingController {
a := &XraySettingController{}
a.initRouter(g)
return a
}
func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
g = g.Group("/xray")
g.POST("/", a.getXraySetting)
g.POST("/update", a.updateSetting)
g.GET("/getXrayResult", a.getXrayResult)
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
}
func (a *XraySettingController) getXraySetting(c *gin.Context) {
xraySetting, err := a.SettingService.GetXrayConfigTemplate()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
inboundTags, err := a.InboundService.GetInboundTags()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
xrayResponse := "{ \"xraySetting\": " + xraySetting + ", \"inboundTags\": " + inboundTags + " }"
jsonObj(c, xrayResponse, nil)
}
func (a *XraySettingController) updateSetting(c *gin.Context) {
xraySetting := c.PostForm("xraySetting")
err := a.XraySettingService.SaveXraySetting(xraySetting)
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
}
func (a *XraySettingController) getDefaultXrayConfig(c *gin.Context) {
defaultJsonConfig, err := a.SettingService.GetDefaultXrayConfig()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
jsonObj(c, defaultJsonConfig, nil)
}
func (a *XraySettingController) getXrayResult(c *gin.Context) {
jsonObj(c, a.XrayService.GetXrayResult(), nil)
}

View file

@ -7,8 +7,9 @@ import (
type XUIController struct {
BaseController
inboundController *InboundController
settingController *SettingController
inboundController *InboundController
settingController *SettingController
xraySettingController *XraySettingController
}
func NewXUIController(g *gin.RouterGroup) *XUIController {
@ -24,9 +25,11 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
g.GET("/", a.index)
g.GET("/inbounds", a.inbounds)
g.GET("/settings", a.settings)
g.GET("/xray", a.xraySettings)
a.inboundController = NewInboundController(g)
a.settingController = NewSettingController(g)
a.xraySettingController = NewXraySettingController(g)
}
func (a *XUIController) index(c *gin.Context) {
@ -40,3 +43,7 @@ func (a *XUIController) inbounds(c *gin.Context) {
func (a *XUIController) settings(c *gin.Context) {
html(c, "settings.html", "pages.settings.title", nil)
}
func (a *XUIController) xraySettings(c *gin.Context) {
html(c, "xray.html", "pages.xray.title", nil)
}

View file

@ -2,12 +2,10 @@ package entity
import (
"crypto/tls"
"encoding/json"
"net"
"strings"
"time"
"x-ui/util/common"
"x-ui/xray"
)
type Msg struct {
@ -16,47 +14,39 @@ type Msg struct {
Obj interface{} `json:"obj"`
}
type Pager struct {
Current int `json:"current"`
PageSize int `json:"page_size"`
Total int `json:"total"`
OrderBy string `json:"order_by"`
Desc bool `json:"desc"`
Key string `json:"key"`
List interface{} `json:"list"`
}
type AllSetting struct {
WebListen string `json:"webListen" form:"webListen"`
WebDomain string `json:"webDomain" form:"webDomain"`
WebPort int `json:"webPort" form:"webPort"`
WebCertFile string `json:"webCertFile" form:"webCertFile"`
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
WebBasePath string `json:"webBasePath" form:"webBasePath"`
SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge"`
ExpireDiff int `json:"expireDiff" form:"expireDiff"`
TrafficDiff int `json:"trafficDiff" form:"trafficDiff"`
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
TgBotToken string `json:"tgBotToken" form:"tgBotToken"`
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"`
TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"`
TgBotLoginNotify bool `json:"tgBotLoginNotify" form:"tgBotLoginNotify"`
TgCpu int `json:"tgCpu" form:"tgCpu"`
TgLang string `json:"tgLang" form:"tgLang"`
XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`
TimeLocation string `json:"timeLocation" form:"timeLocation"`
SecretEnable bool `json:"secretEnable" form:"secretEnable"`
SubEnable bool `json:"subEnable" form:"subEnable"`
SubListen string `json:"subListen" form:"subListen"`
SubPort int `json:"subPort" form:"subPort"`
SubPath string `json:"subPath" form:"subPath"`
SubDomain string `json:"subDomain" form:"subDomain"`
SubCertFile string `json:"subCertFile" form:"subCertFile"`
SubKeyFile string `json:"subKeyFile" form:"subKeyFile"`
SubUpdates int `json:"subUpdates" form:"subUpdates"`
SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"`
SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"`
WebListen string `json:"webListen" form:"webListen"`
WebDomain string `json:"webDomain" form:"webDomain"`
WebPort int `json:"webPort" form:"webPort"`
WebCertFile string `json:"webCertFile" form:"webCertFile"`
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
WebBasePath string `json:"webBasePath" form:"webBasePath"`
SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge"`
PageSize int `json:"pageSize" form:"pageSize"`
ExpireDiff int `json:"expireDiff" form:"expireDiff"`
TrafficDiff int `json:"trafficDiff" form:"trafficDiff"`
RemarkModel string `json:"remarkModel" form:"remarkModel"`
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
TgBotToken string `json:"tgBotToken" form:"tgBotToken"`
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"`
TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"`
TgBotLoginNotify bool `json:"tgBotLoginNotify" form:"tgBotLoginNotify"`
TgCpu int `json:"tgCpu" form:"tgCpu"`
TgLang string `json:"tgLang" form:"tgLang"`
TimeLocation string `json:"timeLocation" form:"timeLocation"`
SecretEnable bool `json:"secretEnable" form:"secretEnable"`
SubEnable bool `json:"subEnable" form:"subEnable"`
SubListen string `json:"subListen" form:"subListen"`
SubPort int `json:"subPort" form:"subPort"`
SubPath string `json:"subPath" form:"subPath"`
SubDomain string `json:"subDomain" form:"subDomain"`
SubCertFile string `json:"subCertFile" form:"subCertFile"`
SubKeyFile string `json:"subKeyFile" form:"subKeyFile"`
SubUpdates int `json:"subUpdates" form:"subUpdates"`
SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"`
SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"`
SubURI string `json:"subURI" form:"subURI"`
}
func (s *AllSetting) CheckValid() error {
@ -106,14 +96,14 @@ func (s *AllSetting) CheckValid() error {
if !strings.HasSuffix(s.WebBasePath, "/") {
s.WebBasePath += "/"
}
xrayConfig := &xray.Config{}
err := json.Unmarshal([]byte(s.XrayTemplateConfig), xrayConfig)
if err != nil {
return common.NewError("xray template config invalid:", err)
if !strings.HasPrefix(s.SubPath, "/") {
s.SubPath = "/" + s.SubPath
}
if !strings.HasSuffix(s.SubPath, "/") {
s.SubPath += "/"
}
_, err = time.LoadLocation(s.TimeLocation)
_, err := time.LoadLocation(s.TimeLocation)
if err != nil {
return common.NewError("time location not exist:", s.TimeLocation)
}

View file

@ -4,7 +4,7 @@
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue@1.7.2/antd.min.css">
<link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue@1.7.8/antd.min.css">
<link rel="stylesheet" href="{{ .base_path }}assets/element-ui@2.15.0/theme-chalk/display.css">
<link rel="stylesheet" href="{{ .base_path }}assets/css/custom.css?{{ .cur_ver }}">
<link rel=”icon” type=”image/x-icon” href="{{ .base_path }}assets/favicon.ico">
@ -13,6 +13,20 @@
[v-cloak] {
display: none;
}
/* vazirmatn-regular - arabic_latin_latin-ext */
@font-face {
font-display: swap;
font-family: 'Vazirmatn';
font-style: normal;
font-weight: 400;
src: url('{{ .base_path }}assets/Vazirmatn-UI-NL-Regular.woff2') format('woff2');
unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC, U+0030-0039;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Vazirmatn', 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol';
}
</style>
<title>{{ .host }}-{{ i18n .title}}</title>
</head>

View file

@ -1,19 +1,13 @@
{{define "js"}}
<script src="{{ .base_path }}assets/vue@2.6.12/vue.min.js"></script>
<script src="{{ .base_path }}assets/moment/moment.min.js"></script>
<script src="{{ .base_path }}assets/ant-design-vue@1.7.2/antd.min.js"></script>
<script src="{{ .base_path }}assets/base64/base64.min.js"></script>
<script src="{{ .base_path }}assets/ant-design-vue@1.7.8/antd.min.js"></script>
<script src="{{ .base_path }}assets/axios/axios.min.js"></script>
<script src="{{ .base_path }}assets/qs/qs.min.js"></script>
<script src="{{ .base_path }}assets/qrcode/qrious.min.js"></script>
<script src="{{ .base_path }}assets/clipboard/clipboard.min.js"></script>
<script src="{{ .base_path }}assets/uri/URI.min.js"></script>
<script src="{{ .base_path }}assets/js/axios-init.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/util/common.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/util/date-util.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/util/utils.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/model/xray.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/model/models.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/langs.js"></script>
<script>
const basePath = '{{ .base_path }}';

View file

@ -1,8 +1,7 @@
{{define "promptModal"}}
<a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title"
:closable="true" @ok="promptModal.ok" :mask-closable="false"
:class="themeSwitcher.darkCardClass"
:ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}'>
:ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}' :class="themeSwitcher.currentTheme">
<a-input id="prompt-modal-input" :type="promptModal.type"
v-model="promptModal.value"
:autosize="{minRows: 10, maxRows: 20}"

View file

@ -1,7 +1,7 @@
{{define "qrcodeModal"}}
<a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title"
:closable="true"
:class="themeSwitcher.darkCardClass"
:class="themeSwitcher.currentTheme"
:footer="null"
width="300px">
<a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;">
@ -13,7 +13,7 @@
</template>
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
<template v-for="(row, index) in qrModal.qrcodes">
<a-tag color="orange" style="margin-top: 10px;display: block;text-align: center;">[[ row.remark ]]</a-tag>
<a-tag color="green" style="margin: 10px 0; display: block; text-align: center;">[[ row.remark ]]</a-tag>
<canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" style="width: 100%; height: 100%;"></canvas>
</template>
</a-modal>
@ -22,39 +22,25 @@
const qrModal = {
title: '',
clientIndex: 0,
inbound: new Inbound(),
dbInbound: new DBInbound(),
client: null,
qrcodes: [],
clipboard: null,
visible: false,
subId: '',
show: function (title = '', dbInbound = new DBInbound(), clientIndex = 0) {
show: function (title = '', dbInbound, client) {
this.title = title;
this.clientIndex = clientIndex;
this.dbInbound = dbInbound;
this.inbound = dbInbound.toInbound();
settings = JSON.parse(this.inbound.settings);
this.client = settings.clients[clientIndex];
remark = [this.dbInbound.remark, ( this.client ? this.client.email : '')].filter(Boolean).join('-');
address = this.dbInbound.address;
this.client = client;
this.subId = '';
this.qrcodes = [];
if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) {
this.inbound.stream.tls.settings.domains.forEach((domain) => {
remarkText = [remark, domain.remark].filter(Boolean).join('-');
this.qrcodes.push({
remark: remarkText,
link: this.inbound.genLink(domain.domain, remarkText, clientIndex)
});
});
} else {
this.inbound.genAllLinks(this.dbInbound.remark, app.remarkModel, client).forEach(l => {
this.qrcodes.push({
remark: remark,
link: this.inbound.genLink(address, remark, clientIndex)
remark: l.remark,
link: l.link
});
}
});
this.visible = true;
},
close: function () {
@ -86,8 +72,7 @@
});
},
genSubLink(subID) {
const { domain: host, port, tls: isTLS, path: base } = app.subSettings;
return buildURL({ host, port, isTLS, base, path: subID+'?name='+remark });
return app.subSettings.subURI+subID+'?name='+subID;
}
},
updated() {

View file

@ -1,7 +1,7 @@
{{define "textModal"}}
<a-modal id="text-modal" v-model="txtModal.visible" :title="txtModal.title"
:closable="true" ok-text='{{ i18n "copy" }}' cancel-text='{{ i18n "close" }}'
:class="themeSwitcher.darkCardClass"
:class="themeSwitcher.currentTheme"
:ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}">
<a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;"
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)"
@ -31,7 +31,10 @@
this.clipboard = new ClipboardJS('#txt-modal-ok-btn', {
text: () => this.content,
});
this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
this.clipboard.on('success', () => {
app.$message.success('{{ i18n "copied" }}')
this.close();
});
}
});
},

View file

@ -2,65 +2,241 @@
<html lang="en">
{{template "head" .}}
<style>
#app {
padding-top: 100px;
h1 {
text-align: center;
margin: 20px 0 50px 0;
}
.ant-btn,
.ant-input {
height: 50px;
border-radius: 30px;
}
.ant-input-group-addon {
border-radius: 0 30px 30px 0;
width: 50px;
font-size: 18px;
}
.ant-input-affix-wrapper .ant-input-prefix {
left: 23px;
}
.ant-input-affix-wrapper .ant-input:not(:first-child) {
padding-left: 50px;
}
.centered {
display: flex;
text-align: center;
align-items: center;
justify-content: center;
}
.title {
font-size: 32px;
font-weight: bold;
}
#app {
overflow: hidden;
}
#login {
animation: charge 0.5s both;
background-color: #fff;
border-radius: 2rem;
padding: 3rem;
transition: all 0.3s;
}
#login:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
}
@keyframes charge {
from {
transform: translateY(5rem);
opacity: 0;
}
h1 {
text-align: center;
color: #fff;
margin: 20px 0 50px 0;
to {
transform: translateY(0);
opacity: 1;
}
.ant-btn, .ant-input {
height: 50px;
border-radius: 30px;
}
@keyframes wave {
from {
transform: rotate(0deg);
}
.ant-input-group-addon {
border-radius: 0 30px 30px 0;
width: 50px;
font-size: 18px;
to {
transform: rotate(360deg);
}
.ant-input-affix-wrapper .ant-input-prefix {
left: 23px;
}
.wave {
opacity: 0.6;
position: absolute;
bottom: 40%;
left: 50%;
width: 6000px;
height: 6000px;
background-color: rgba(0, 135, 113, 0.08);
margin-left: -3000px;
transform-origin: 50% 48%;
border-radius: 46%;
animation: wave 72s infinite linear;
pointer-events: none;
}
.wave2 {
animation: wave 88s infinite linear;
opacity: 0.3;
}
.wave3 {
animation: wave 80s infinite linear;
opacity: 0.1;
}
.under {
background-color: #dbf5ed;
}
.dark .wave {
background: rgb(10 117 87 / 20%);
}
.dark .under {
background-color: #101828;
}
.dark #login {
background-color: #151f31;
}
.dark h1 {
color: rgba(255, 255, 255, 0.85);
}
.ant-btn-primary-login {
color: #008771;
background-color: #eef9f7;
border-color: #89d9cc;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.12);
box-shadow: none;
width: 100%;
}
.ant-btn-primary-login:focus,
.ant-btn-primary-login:hover {
color: #fff;
background-color: #006655;
border-color: #006655;
background-image: linear-gradient(
270deg,
rgba(123, 199, 77, 0) 30%,
#009980,
rgba(123, 199, 77, 0) 100%
);
background-repeat: no-repeat;
animation: ma-bg-move ease-in-out 5s infinite;
background-position-x: -500px;
width: 95%;
animation-delay: -0.5s;
box-shadow: 0 2px 0 rgba(0, 0, 0, 0.045);
}
.ant-btn-primary-login.active,
.ant-btn-primary-login:active {
color: #fff;
background-color: #006655;
border-color: #006655;
}
@keyframes ma-bg-move {
0% {
background-position: -500px 0;
}
.ant-input-affix-wrapper .ant-input:not(:first-child) {
padding-left: 50px;
50% {
background-position: 1000px 0;
}
.centered {
display: flex;
text-align: center;
align-items: center;
justify-content: center;
100% {
background-position: 1000px 0;
}
.title {
font-size: 32px;
font-weight: bold;
}
.wave-btn-bg {
position: relative;
border-radius: 25px;
width: 100%;
}
.dark .wave-btn-bg {
color: #fff;
position: relative;
background-color: #0a7557;
border: 2px double transparent;
background-origin: border-box;
background-clip: padding-box, border-box;
background-size: 300%;
animation: wave-btn-tara 4s ease infinite;
transition: all 0.5s ease;
width: 100%;
}
.dark .wave-btn-bg-cl {
background-image: linear-gradient(rgba(13, 14, 33, 0), rgba(13, 14, 33, 0)),
radial-gradient(circle at left top, #006655, #009980, #006655) !important;
border-radius: 3em;
}
.dark .wave-btn-bg-cl:hover {
width: 95%;
}
.dark .wave-btn-bg-cl:before {
position: absolute;
content: "";
top: -5px;
left: -5px;
bottom: -5px;
right: -5px;
z-index: -1;
background: inherit;
background-size: inherit;
border-radius: 4em;
opacity: 0;
transition: 0.5s;
}
.dark .wave-btn-bg-cl:hover::before {
opacity: 1;
filter: blur(20px);
animation: wave-btn-tara 8s linear infinite;
}
@keyframes wave-btn-tara {
to {
background-position: 300%;
}
}
.dark .ant-btn-primary-login {
font-size: 14px;
color: #fff;
text-align: center;
background-image: linear-gradient(
rgba(13, 14, 33, 0.45),
rgba(13, 14, 33, 0.35)
);
border-radius: 2rem;
border: none;
outline: none;
background-color: transparent;
height: 46px;
position: relative;
white-space: nowrap;
cursor: pointer;
touch-action: manipulation;
padding: 0 15px;
width: 100%;
animation: none;
background-position-x: 0;
box-shadow: none;
}
</style>
<body>
<a-layout id="app" v-cloak class="login" :class="themeSwitcher.darkCardClass">
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
<transition name="list" appear>
<a-layout-content>
<a-layout-content class="under" style="min-height: 0;">
<div class='wave'></div>
<div class='wave wave2'></div>
<div class='wave wave3'></div>
<a-row type="flex" justify="center" align="middle" style="height: 100%; overflow: auto;">
<a-col :xs="22" :sm="20" :md="14" :lg="10" :xl="8" :xxl="6" id="login" style="margin: 3rem 0;">
<a-row type="flex" justify="center">
<a-col :xs="22" :sm="20" :md="16" :lg="12" :xl="8">
<a-col>
<h1 class="title">{{ i18n "pages.login.title" }}</h1>
</a-col>
</a-row>
<a-row type="flex" justify="center">
<a-col :xs="22" :sm="20" :md="16" :lg="12" :xl="8">
<a-col span="24">
<a-form>
<a-form-item>
<a-input v-model.trim="user.username" placeholder='{{ i18n "username" }}'
@keydown.enter.native="login" autofocus>
<a-icon slot="prefix" type="user" :style="'font-size: 16px;' + themeSwitcher.textStyle" />
<a-icon slot="prefix" type="user" style="font-size: 16px;"/>
</a-input>
</a-form-item>
<a-form-item>
@ -76,18 +252,20 @@
</a-form-item>
<a-form-item>
<a-row justify="center" class="centered">
<a-button type="primary" :loading="loading" @click="login" :icon="loading ? 'poweroff' : undefined"
:style="loading ? { width: '50px' } : { display: 'block', width: '100%' }">
[[ loading ? '' : '{{ i18n "login" }}' ]]
</a-button>
<div class="wave-btn-bg wave-btn-bg-cl">
<a-button class="ant-btn-primary-login" type="primary" :loading="loading" @click="login" :icon="loading ? 'poweroff' : undefined"
:style="loading ? { width: '50px' } : { display: 'inline-block' }">
[[ loading ? '' : '{{ i18n "login" }}' ]]
</a-button>
</div>
</a-row>
</a-form-item>
<a-form-item>
<a-row justify="center" class="centered">
<a-col :span="12">
<a-select ref="selectLang" v-model="lang" @change="setLang(lang)" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
<span role="img" :aria-label="l.name" v-text="l.icon"></span>
<a-col :span="24">
<a-select ref="selectLang" v-model="lang" @change="setLang(lang)" style="width: 150px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="l.value" label="English" v-for="l in supportLangs">
<span role="img" aria-label="l.name" v-text="l.icon"></span>
&nbsp;&nbsp;<span v-text="l.name"></span>
</a-select-option>
</a-select>
@ -96,12 +274,19 @@
</a-form-item>
<a-form-item>
<a-row justify="center" class="centered">
<theme-switch />
<a-col>
<a-icon type="bulb" :theme="themeSwitcher.isDarkTheme ? 'filled' : 'outlined'"></a-icon>&nbsp;
</a-col>
<a-col>
<theme-switch />
</a-col>
</a-row>
</a-form-item>
</a-form>
</a-col>
</a-row>
</a-col>
</a-row>
</a-layout-content>
</transition>
</a-layout>
@ -109,6 +294,12 @@
{{template "component/themeSwitcher" .}}
{{template "component/password" .}}
<script>
class User {
constructor() {
this.username = "";
this.password = "";
}
}
const app = new Vue({
delimiters: ['[[', ']]'],
@ -121,7 +312,6 @@
lang: ""
},
async created() {
this.updateBackground();
this.lang = getLang();
this.secretEnable = await this.getSecretStatus();
},
@ -143,21 +333,8 @@
return msg.obj;
}
},
updateBackground() {
const leftColor = RandomUtil.randomIntRange(0x222222, 0xFFFFFF / 2).toString(16);
const rightColor = RandomUtil.randomIntRange(0xFFFFFF / 2, 0xDDDDDD).toString(16);
const deg = RandomUtil.randomIntRange(0, 360);
const background = `linear-gradient(${deg}deg, #${leftColor} 10%, #${rightColor} 100%)`;
document.querySelector('#app').style.background = this.themeSwitcher.isDarkTheme ? colors.dark.bg : background;
},
},
watch: {
'themeSwitcher.isDarkTheme'(newVal, oldVal) {
this.updateBackground();
},
},
});
</script>
</body>
</html>

View file

@ -1,122 +1,207 @@
{{define "clientsBulkModal"}}
<a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible" :title="clientsBulkModal.title" @ok="clientsBulkModal.ok"
:confirm-loading="clientsBulkModal.confirmLoading" :closable="true" :mask-closable="false"
:class="themeSwitcher.darkCardClass"
:ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}'>
<a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible" :title="clientsBulkModal.title"
@ok="clientsBulkModal.ok" :confirm-loading="clientsBulkModal.confirmLoading" :closable="true" :mask-closable="false"
:ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
<a-form layout="inline">
<a-form-item label='{{ i18n "pages.client.method" }}'>
<a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 350px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option :value="0">Random</a-select-option>
<a-select-option :value="1">Random+Prefix</a-select-option>
<a-select-option :value="2">Random+Prefix+Num</a-select-option>
<a-select-option :value="3">Random+Prefix+Num+Postfix</a-select-option>
<a-select-option :value="4">Prefix+Num+Postfix [ BE CAREFUL! ]</a-select-option>
</a-select>
</a-form-item><br />
<a-form-item v-if="clientsBulkModal.emailMethod>1">
<span slot="label">{{ i18n "pages.client.first" }}</span>
<a-input-number v-model="clientsBulkModal.firstNum" :min="1"></a-input-number>
</a-form-item>
<a-form-item v-if="clientsBulkModal.emailMethod>1">
<span slot="label">{{ i18n "pages.client.last" }}</span>
<a-input-number v-model="clientsBulkModal.lastNum" :min="clientsBulkModal.firstNum"></a-input-number>
</a-form-item>
<a-form-item v-if="clientsBulkModal.emailMethod>0">
<span slot="label">{{ i18n "pages.client.prefix" }}</span>
<a-input v-model="clientsBulkModal.emailPrefix" style="width: 120px"></a-input>
</a-form-item>
<a-form-item v-if="clientsBulkModal.emailMethod>2">
<span slot="label">{{ i18n "pages.client.postfix" }}</span>
<a-input v-model="clientsBulkModal.emailPostfix" style="width: 120px"></a-input>
</a-form-item>
<a-form-item v-if="clientsBulkModal.emailMethod < 2">
<span slot="label">{{ i18n "pages.client.clientCount" }}</span>
<a-input-number v-model="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number>
</a-form-item>
<a-form-item v-if="app.subSettings.enable">
<span slot="label">
Subscription
<table width="100%" class="ant-table-tbody">
<tr>
<td>{{ i18n "pages.client.method" }}</td>
<td>
<a-form-item>
<a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 250px"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="0">Random</a-select-option>
<a-select-option :value="1">Random+Prefix</a-select-option>
<a-select-option :value="2">Random+Prefix+Num</a-select-option>
<a-select-option :value="3">Random+Prefix+Num+Postfix</a-select-option>
<a-select-option :value="4">Prefix+Num+Postfix</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr v-if="clientsBulkModal.emailMethod>1">
<td>{{ i18n "pages.client.first" }}</td>
<td>
<a-form-item>
<a-input-number v-model="clientsBulkModal.firstNum" :min="1"></a-input-number>
</a-form-item>
</td>
</tr>
<tr v-if="clientsBulkModal.emailMethod>1">
<td>{{ i18n "pages.client.last" }}</td>
<td>
<a-form-item>
<a-input-number v-model="clientsBulkModal.lastNum"
:min="clientsBulkModal.firstNum"></a-input-number>
</a-form-item>
</td>
</tr>
<tr v-if="clientsBulkModal.emailMethod>0">
<td>{{ i18n "pages.client.prefix" }}</td>
<td>
<a-form-item>
<a-input v-model="clientsBulkModal.emailPrefix" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr v-if="clientsBulkModal.emailMethod>2">
<td>{{ i18n "pages.client.postfix" }}</td>
<td>
<a-form-item>
<a-input v-model="clientsBulkModal.emailPostfix" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr v-if="clientsBulkModal.emailMethod < 2">
<td>{{ i18n "pages.client.clientCount" }}</td>
<td>
<a-form-item>
<a-input-number v-model="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number>
</a-form-item>
</td>
</tr>
<tr v-if="clientsBulkModal.inbound.canEnableTlsFlow()">
<td>Flow</td>
<td>
<a-form-item>
<a-select v-model="clientsBulkModal.flow" style="width: 250px"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr v-if="app.subSettings.enable">
<td>Subscription
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
</template>
<a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input v-model.trim="clientsBulkModal.subId" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr v-if="app.tgBotEnable">
<td>Telegram ID
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="clientsBulkModal.subId"></a-input>
</a-form-item>
<a-form-item v-if="app.tgBotEnable">
<span slot="label">
Telegram ID
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="clientsBulkModal.tgId"></a-input>
</a-form-item>
<br>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.IPLimit" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="clientsBulkModal.limitIp" min="0"></a-input-number>
</a-form-item>
<br>
<a-form-item v-if="clientsBulkModal.inbound.xtls" label="Flow">
<a-select v-model="clientsBulkModal.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="">{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-if="clientsBulkModal.inbound.canEnableTlsFlow()" label="Flow" layout="inline">
<a-select v-model="clientsBulkModal.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="clientsBulkModal.totalGB" :min="0"></a-input-number>
</a-form-item>
<br>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="clientsBulkModal.delayedStart" @click="clientsBulkModal.expiryTime=0"></a-switch>
</a-form-item>
<br>
<a-form-item label='{{ i18n "pages.client.expireDays" }}' v-if="clientsBulkModal.delayedStart">
<a-input-number v-model="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
<a-form-item v-else>
<span slot="label">
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.darkCardClass"
v-model="clientsBulkModal.expiryTime" style="width: 300px;"></a-date-picker>
</a-form-item>
</td>
<td>
<a-form-item>
<a-input v-model.trim="clientsBulkModal.tgId" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>
<label>
<span>{{ i18n "pages.inbounds.IPLimit" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</label>
</td>
<td>
<a-form-item>
<a-input-number v-model="clientsBulkModal.limitIp" min="0"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td v-if="clientsBulkModal.inbound.xtls">
<label>Flow</label>
</td>
<td v-if="clientsBulkModal.inbound.xtls">
<a-form-item>
<a-select v-model="clientsBulkModal.flow" style="width: 200px"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="">{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>
<span>{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input-number v-model="clientsBulkModal.totalGB" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.client.delayedStart" }}</td>
<td>
<a-form-item>
<a-switch v-model="clientsBulkModal.delayedStart"
@click="clientsBulkModal.expiryTime=0"></a-switch>
</a-form-item>
</td>
</tr>
<tr v-if="clientsBulkModal.delayedStart">
<td>{{ i18n "pages.client.expireDays" }}</td>
<td>
<a-form-item>
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
<tr v-else>
<td>
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.currentTheme" v-model="clientsBulkModal.expiryTime"
style="width: 250px;"></a-date-picker>
</a-form-item>
</td>
</tr>
<tr v-if="clientsBulkModal.expiryTime != 0">
<td>
<span>{{ i18n "pages.client.renew" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.client.renewDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input-number v-model.number="clientsBulkModal.reset" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
</table>
</a-form>
</a-modal>
<script>
@ -142,6 +227,7 @@
tgId: "",
flow: "",
delayedStart: false,
reset: 0,
ok() {
clients = [];
method = clientsBulkModal.emailMethod;
@ -170,6 +256,7 @@
if (clientsBulkModal.inbound.xtls) {
newClient.flow = clientsBulkModal.flow;
}
newClient.reset = clientsBulkModal.reset;
clients.push(newClient);
}
ObjectUtil.execute(clientsBulkModal.confirm, clients, clientsBulkModal.dbInbound.id);
@ -199,6 +286,7 @@
this.dbInbound = new DBInbound(dbInbound);
this.inbound = dbInbound.toInbound();
this.delayedStart = false;
this.reset = 0;
},
newClient(protocol) {
switch (protocol) {

View file

@ -1,8 +1,11 @@
{{define "clientsModal"}}
<a-modal id="client-modal" v-model="clientModal.visible" :title="clientModal.title" @ok="clientModal.ok"
:confirm-loading="clientModal.confirmLoading" :closable="true" :mask-closable="false"
:class="themeSwitcher.darkCardClass"
:class="themeSwitcher.currentTheme"
:ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'>
<template v-if="isEdit">
<a-tag v-if="isExpiry || isTrafficExhausted" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
</template>
{{template "form/client"}}
</a-modal>
<script>
@ -35,7 +38,7 @@
this.isEdit = isEdit;
this.dbInbound = new DBInbound(dbInbound);
this.inbound = dbInbound.toInbound();
this.clients = this.getClients(this.inbound.protocol, this.inbound.settings);
this.clients = this.inbound.clients;
this.index = index === null ? this.clients.length : index;
this.delayedStart = false;
if (isEdit) {
@ -49,15 +52,6 @@
this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
this.confirm = confirm;
},
getClients(protocol, clientSettings) {
switch (protocol) {
case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans;
case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
default: return null;
}
},
getClientId(protocol, client) {
switch (protocol) {
case Protocols.TROJAN: return client.password;
@ -109,8 +103,11 @@
get isExpiry() {
return this.clientModal.isEdit && this.client.expiryTime >0 ? (this.client.expiryTime < new Date().getTime()) : false;
},
get statsColor() {
return usageColor(clientStats.up + clientStats.down, app.trafficDiff, this.client.totalGB);
get delayedStart() {
return this.clientModal.delayedStart;
},
set delayedStart(value) {
this.clientModal.delayedStart = value;
},
get delayedExpireDays() {
return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
@ -151,7 +148,7 @@
this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
class: themeSwitcher.darkCardClass,
class: themeSwitcher.currentTheme,
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: async () => {

View file

@ -11,6 +11,10 @@
<a-icon type="setting"></a-icon>
<span>{{ i18n "menu.settings"}}</span>
</a-menu-item>
<a-menu-item key="{{ .base_path }}panel/xray">
<a-icon type="tool"></a-icon>
<span>{{ i18n "menu.xray"}}</span>
</a-menu-item>
<!--<a-menu-item key="{{ .base_path }}panel/clients">-->
<!-- <a-icon type="laptop"></a-icon>-->
<!-- <span>Client</span>-->
@ -26,7 +30,7 @@
<a-layout-sider :theme="themeSwitcher.currentTheme" id="sider" collapsible breakpoint="md" collapsed-width="0">
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" selected-keys="">
<a-menu-item mode="inline">
<a-icon type="bg-colors"></a-icon>
<a-icon type="bulb" :theme="themeSwitcher.isDarkTheme ? 'filled' : 'outlined'"></a-icon>
<theme-switch />
</a-menu-item>
</a-menu>
@ -38,14 +42,14 @@
<a-drawer id="sider-drawer" placement="left" :closable="false"
@close="siderDrawer.close()"
:visible="siderDrawer.visible"
:wrap-class-name="themeSwitcher.darkDrawerClass"
:wrap-class-name="themeSwitcher.currentTheme"
:wrap-style="{ padding: 0 }">
<div class="drawer-handle" @click="siderDrawer.change()" slot="handle">
<a-icon :type="siderDrawer.visible ? 'close' : 'menu-fold'"></a-icon>
</div>
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" selected-keys="">
<a-menu-item mode="inline">
<a-icon type="bg-colors"></a-icon>
<a-icon type="bulb" :theme="themeSwitcher.isDarkTheme ? 'filled' : 'outlined'"></a-icon>
<theme-switch />
</a-menu-item>
</a-menu>

View file

@ -4,12 +4,12 @@
:placeholder="placeholder"
@input="$emit('input', $event.target.value)">
<template v-if="icon" #prefix>
<a-icon :type="icon" :style="'font-size: 16px;' + themeSwitcher.textStyle" />
<a-icon :type="icon" style="font-size: 16px;" />
</template>
<template #addonAfter>
<a-icon :type="showPassword ? 'eye-invisible' : 'eye'"
@click="toggleShowPassword"
:style="'font-size: 16px;' + themeSwitcher.textStyle" />
style="font-size: 16px;" />
</template>
</a-input>
</template>

View file

@ -13,10 +13,10 @@
</a-col>
<a-col :lg="24" :xl="12">
<template v-if="type === 'text'">
<a-input :value="value" @input="$emit('input', $event.target.value)"></a-input>
<a-input :value="value" @input="$emit('input', $event.target.value)" :placeholder="placeholder"></a-input>
</template>
<template v-else-if="type === 'number'">
<a-input-number :value="value" @change="value => $emit('input', value)" :min="min" style="width: 100%;"></a-input-number>
<a-input-number :value="value" :step="step" @change="value => $emit('input', value)" :min="min" style="width: 100%;"></a-input-number>
</template>
<template v-else-if="type === 'switch'">
<a-switch :checked="value" @change="value => $emit('input', value)"></a-switch>
@ -29,7 +29,7 @@
{{define "component/setting"}}
<script>
Vue.component('setting-list-item', {
props: ["type", "title", "desc", "value", "min"],
props: ["type", "title", "desc", "value", "min", "step", "placeholder"],
template: `{{template "component/settingListItem"}}`,
});
</script>

View file

@ -1,8 +1,6 @@
{{define "component/themeSwitchTemplate"}}
<template>
<a-switch :default-checked="themeSwitcher.isDarkTheme"
checked-children="☀"
un-checked-children="🌙"
<a-switch size="small" :default-checked="themeSwitcher.isDarkTheme"
@change="themeSwitcher.toggleTheme()">
</a-switch>
</template>
@ -10,39 +8,17 @@
{{define "component/themeSwitcher"}}
<script>
const colors = {
dark: {
bg: "#242c3a",
text: "hsla(0,0%,100%,.65)"
},
light: {
bg: '#f0f2f5',
text: "rgba(0, 0, 0, 0.7)",
}
}
function createThemeSwitcher() {
const isDarkTheme = localStorage.getItem('dark-mode') === 'true';
const theme = isDarkTheme ? 'dark' : 'light';
return {
isDarkTheme,
bgStyle: `background: ${colors[theme].bg};`,
textStyle: `color: ${colors[theme].text};`,
darkClass: isDarkTheme ? 'ant-dark' : '',
darkCardClass: isDarkTheme ? 'ant-card-dark' : '',
darkDrawerClass: isDarkTheme ? 'ant-drawer-dark' : '',
get currentTheme() {
return this.isDarkTheme ? 'dark' : 'light';
},
toggleTheme() {
this.isDarkTheme = !this.isDarkTheme;
this.theme = this.isDarkTheme ? 'dark' : 'light';
localStorage.setItem('dark-mode', this.isDarkTheme);
this.bgStyle = `background: ${colors[this.theme].bg};`;
this.textStyle = `color: ${colors[this.theme].text};`;
this.darkClass = this.isDarkTheme ? 'ant-dark' : '';
this.darkCardClass = this.isDarkTheme ? 'ant-card-dark' : '';
this.darkDrawerClass = this.isDarkTheme ? 'ant-drawer-dark' : '';
},
};
}

View file

@ -1,161 +1,223 @@
{{define "form/client"}}
<a-form layout="inline" v-if="client">
<template v-if="isEdit">
<a-tag v-if="isExpiry || isTrafficExhausted" color="red" style="margin-bottom: 10px;display: block;text-align: center;">
Account is (Expired|Traffic Ended) And Disabled
</a-tag>
</template>
<a-form-item label='{{ i18n "pages.inbounds.enable" }}'>
<a-switch v-model="client.enable"></a-switch>
</a-form-item>
<br>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
<table width="100%" class="ant-table-tbody">
<tr>
<td>{{ i18n "pages.inbounds.enable" }}</td>
<td>
<a-form-item>
<a-switch v-model="client.enable"></a-switch>
</a-form-item>
</td>
</tr>
<tr>
<td>
<span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template>
<a-icon type="sync" @click="client.email = RandomUtil.randomLowerAndNum(9)"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input v-model.trim="client.email" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS">
<td>password
<a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS" @click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
<a-icon v-if="inbound.protocol === Protocols.TROJAN" @click="client.password = RandomUtil.randomSeq(10)" type="sync"> </a-icon>
</td>
<td>
<a-form-item>
<a-input v-model.trim="client.password" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS">
<td>ID <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"></a-icon></td>
<td>
<a-input v-model.trim="client.id" style="width: 300px;"></a-input>
</td>
</tr>
<tr v-if="client.email && app.subSettings.enable">
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"></a-icon></td>
<td>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
</template>
</a-tooltip>
<a-form-item>
<a-input v-model.trim="client.subId" style="width: 250px;"></a-input>
</a-form-item>
</td>
</tr>
<tr v-if="client.email && app.tgBotEnable">
<td>Telegram ID
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input v-model.trim="client.tgId" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>
<span>{{ i18n "pages.inbounds.IPLimit" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input-number v-model="client.limitIp" min="0"></a-input-number>
</a-form-item>
</td>
</tr>
<tr v-if="client.email && client.limitIp > 0 && isEdit">
<td>
<span>{{ i18n "pages.inbounds.IPLimitlog" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitlogDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span>
</template>
<span style="color: #FF4D4F">
<a-icon type="delete" @click="clearDBClientIps(client.email)"></a-icon>
</span>
</a-tooltip>
</td>
<td>
<a-form layout="block">
<a-textarea id="clientIPs" readonly @click="getDBClientIps(client.email)"
placeholder="Click To Get IPs" :auto-size="{ minRows: 5, maxRows: 10 }">
</a-textarea>
</a-form>
</td>
</tr>
<tr v-if="inbound.xtls">
<td>Flow</td>
<td>
<a-form-item>
<a-select v-model="client.flow" style="width: 200px"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="">{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr v-else-if="inbound.canEnableTlsFlow()">
<td>Flow</td>
<td>
<a-form-item>
<a-select v-model="client.flow" style="width: 200px"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>
<span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
</a-form-item>
<template v-if="isEdit && clientStats">
<br>
<span> {{ i18n "usage" }}:</span>
<a-tag :color="clientUsageColor(clientStats, app.trafficDiff)">
[[ sizeFormat(clientStats.up) ]] /
[[ sizeFormat(clientStats.down) ]]
([[ sizeFormat(clientStats.up + clientStats.down) ]])
</a-tag>
<a-tooltip>
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
<a-icon type="retweet"
@click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)"
v-if="client.email.length > 0"></a-icon>
</a-tooltip>
</template>
</a-tooltip>
</span>
<a-icon @click="client.email = RandomUtil.randomLowerAndNum(8)" type="sync"> </a-icon>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item>
<a-form-item label="Password" v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS">
<a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS" @click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
<a-icon v-if="inbound.protocol === Protocols.TROJAN" @click="client.password = RandomUtil.randomSeq(10)" type="sync"> </a-icon>
<a-input v-model.trim="client.password" style="width: 300px;"></a-input>
</a-form-item>
<br>
<a-form-item label="ID" v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS">
<a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon>
<a-input v-model.trim="client.id" style="width: 300px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email && app.subSettings.enable">
<span slot="label">
Subscription
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"> </a-icon>
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email && app.tgBotEnable" >
<span slot="label">
Telegram ID
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="client.tgId"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.IPLimit" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="client.limitIp" min="0"></a-input-number>
</a-form-item>
<a-form-item v-if="client.email && client.limitIp > 0 && isEdit">
<span slot="label">
<span>{{ i18n "pages.inbounds.IPLimitlog" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitlogDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span>
</template>
<span style="color: #FF4D4F">
<a-icon type="delete" @click="clearDBClientIps(client.email)"></a-icon>
</span>
</a-tooltip>
</span>
<a-form layout="block">
<a-textarea id="clientIPs" readonly
@click="getDBClientIps(client.email)"
placeholder="Click To Get IPs"
:auto-size="{ minRows: 5, maxRows: 10 }"
>
</a-textarea>
</a-form>
</a-form-item>
<br>
<a-form-item v-if="inbound.xtls" label="Flow">
<a-select v-model="client.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="">{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-else-if="inbound.canEnableTlsFlow()" label="Flow">
<a-select v-model="client.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
<template v-if="isEdit && clientStats">
<br>
<span> {{ i18n "usage" }}:</span>
<a-tag :color="statsColor">
[[ sizeFormat(clientStats.up) ]] /
[[ sizeFormat(clientStats.down) ]]
([[ sizeFormat(clientStats.up + clientStats.down) ]])
</a-tag>
<a-tooltip>
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
<a-icon type="retweet" @click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)"
v-if="client.email.length > 0"></a-icon>
</a-tooltip>
</template>
</a-form-item>
<br>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="clientModal.delayedStart" @click="client._expiryTime=0"></a-switch>
</a-form-item>
<br>
<a-form-item label='{{ i18n "pages.client.expireDays" }}' v-if="clientModal.delayedStart">
<a-input-number v-model="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
<a-form-item v-else>
<span slot="label">
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.darkCardClass"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
<a-tag color="red" v-if="isExpiry">Expired</a-tag>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.client.delayedStart" }}</td>
<td>
<a-form-item>
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
</a-form-item>
</td>
</tr>
<tr v-if="delayedStart">
<td>{{ i18n "pages.client.expireDays" }}</td>
<td>
<a-form-item>
<a-input-number v-model="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
<tr v-else>
<td>
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.currentTheme" v-model="client._expiryTime"
style="width: 170px;"></a-date-picker>
<a-tag color="red" v-if="isEdit && isExpiry">Expired</a-tag>
</a-form-item>
</td>
</tr>
<tr v-if="client.expiryTime != 0">
<td>
<span>{{ i18n "pages.client.renew" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.client.renewDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input-number v-model.number="client.reset" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
</table>
</a-form>
{{end}}

View file

@ -1,63 +1,94 @@
{{define "form/inbound"}}
<!-- base -->
<a-form layout="inline">
<a-form-item label='{{ i18n "enable" }}'>
<a-switch v-model="dbInbound.enable"></a-switch>
</a-form-item>
<br>
<a-form-item label='{{ i18n "remark" }}'>
<a-input v-model.trim="dbInbound.remark"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "protocol" }}'>
<a-select v-model="inbound.protocol" style="width: 160px;" :disabled="isEdit" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<span slot="label">
{{ i18n "monitor" }}
<a-tooltip>
<table width="100%" class="ant-table-tbody">
<tr>
<td>{{ i18n "enable" }}</td>
<td>
<a-form-item>
<a-switch v-model="dbInbound.enable"></a-switch>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "remark" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="dbInbound.remark" style="width: 250px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "protocol" }}</td>
<td>
<a-form-item>
<a-select v-model="inbound.protocol" style="width: 250px;" :disabled="isEdit" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "monitor" }}
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.monitorDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="inbound.listen"></a-input>
</a-form-item>
<br>
<a-form-item label='{{ i18n "pages.inbounds.port" }}'>
<a-input-number v-model="inbound.port"></a-input-number>
</a-form-item>
<br>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="dbInbound.totalGB" :min="0"></a-input-number>
</a-form-item>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.darkCardClass"
v-model="dbInbound._expiryTime" style="width: 250px;"></a-date-picker>
</a-form-item>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.listen" style="width: 250px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.port" }}</td>
<td>
<a-form-item>
<a-input-number v-model.number="inbound.port"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>
<span>{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input-number v-model="dbInbound.totalGB" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.currentTheme"
v-model="dbInbound._expiryTime" style="width: 250px;"></a-date-picker>
</a-form-item>
</td>
</tr>
</table>
</a-form>
<!-- vmess settings -->
<template v-if="inbound.protocol === Protocols.VMESS">
{{template "form/vmess"}}
@ -96,6 +127,7 @@
<!-- stream settings -->
<template v-if="inbound.canEnableStream()">
{{template "form/streamSettings"}}
{{template "form/externalProxy" }}
</template>
<!-- tls settings -->

View file

@ -0,0 +1,544 @@
{{define "form/outbound"}}
<!-- base -->
<a-tabs :active-key="outModal.activeKey" style="padding: 0; background-color: transparent;" @change="(activeKey) => {outModal.toggleJson(activeKey == '2'); }">
<a-tab-pane key="1" tab="Form">
<a-form layout="inline">
<table width="100%" class="ant-table-tbody">
<tr>
<td>{{ i18n "protocol" }}</td>
<td>
<a-form-item>
<a-select v-model="outbound.protocol" style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="x,y in Protocols" :value="x">[[ y ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.xray.outbound.tag" }}</td>
<td>
<a-form-item has-feedback :validate-status="outModal.duplicateTag? 'warning' : 'success'">
<a-input v-model.trim="outbound.tag" style="width: 250px" @change="outModal.check()" placeholder='{{ i18n "pages.xray.outbound.tagDesc" }}'></a-input>
</a-form-item>
</td>
</tr>
<!-- freedom settings-->
<template v-if="outbound.protocol === Protocols.Freedom">
<tr>
<td>Strategy</td>
<td>
<a-form-item>
<a-select
v-model="outbound.settings.domainStrategy"
style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in outboundDomainStrategies" :value="s">[[ s ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>Fragment</td>
<td>
<a-form-item>
<a-switch
:checked="Object.keys(outbound.settings.fragment).length >0"
@change="checked => outbound.settings.fragment = checked ? new Outbound.FreedomSettings.Fragment() : {}">
</a-switch>
</a-form-item>
</td>
</tr>
<template v-if="Object.keys(outbound.settings.fragment).length >0">
<tr>
<td>Packets</td>
<td>
<a-form-item>
<a-select
v-model="outbound.settings.fragment.packets"
style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['1-3','tlshello']" :value="s">[[ s ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>Length</td>
<td>
<a-form-item>
<a-input v-model.trim="outbound.settings.fragment.length" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>Interval</td>
<td>
<a-form-item>
<a-input v-model.trim="outbound.settings.fragment.interval" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
</template>
</template>
<!-- blackhole settings -->
<template v-if="outbound.protocol === Protocols.Blackhole">
<tr>
<td>Response Type</td>
<td>
<a-form-item>
<a-select
v-model="outbound.settings.type"
style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['', 'none','http']" :value="s">[[ s ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
</template>
<!-- dns settings -->
<template v-if="outbound.protocol === Protocols.DNS">
<tr>
<td>{{ i18n "pages.inbounds.network" }}</td>
<td>
<a-form-item>
<a-select
v-model="outbound.settings.network"
style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['udp','tcp']" :value="s">[[ s ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
</template>
<!-- Address + Port -->
<template v-if="outbound.hasAddressPort()">
<tr>
<td>{{ i18n "pages.inbounds.address" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="outbound.settings.address" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.port" }}</td>
<td>
<a-form-item>
<a-input-number v-model.number="outbound.settings.port" :min="1" :max="65532"></a-input-number>
</a-form-item>
</td>
</tr>
</template>
<!-- Vnext (vless/vmess) settings -->
<template v-if="[Protocols.VMess, Protocols.VLESS].includes(outbound.protocol)">
<tr>
<td>ID</td>
<td>
<a-form-item>
<a-input v-model.trim="outbound.settings.id" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<!-- vless settings -->
<template v-if="outbound.canEnableTlsFlow()">
<tr>
<td>Flow</td>
<td>
<a-form-item>
<a-select v-model="outbound.settings.flow" style="width: 250px" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
</template>
</template>
<!-- Servers (trojan/shadowsocks/socks/http) settings -->
<template v-if="outbound.hasServers()">
<tr v-if="outbound.hasUsername()">
<td>{{ i18n "username" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="outbound.settings.user" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "password" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="outbound.settings.password" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<!-- shadowsocks -->
<template v-if="outbound.protocol === Protocols.Shadowsocks">
<tr>
<td>{{ i18n "encryption" }}</td>
<td>
<a-form-item>
<a-select v-model="outbound.settings.method" style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>UDP over TCP</td>
<td>
<a-form-item>
<a-switch v-model="outbound.settings.uot"></a-switch>
</a-form-item>
</td>
</tr>
</template>
</template>
<!-- stream settings -->
<template v-if="outbound.canEnableStream()">
<tr>
<td>{{ i18n "transmission" }}</td>
<td>
<a-form-item>
<a-select v-model="outbound.stream.network" @change="streamNetworkChange"
style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="tcp">TCP</a-select-option>
<a-select-option value="kcp">KCP</a-select-option>
<a-select-option value="ws">WebSocket</a-select-option>
<a-select-option value="http">HTTP2</a-select-option>
<a-select-option value="quic">QUIC</a-select-option>
<a-select-option value="grpc">gRPC</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<template v-if="outbound.stream.network === 'tcp'">
<tr>
<td>http {{ i18n "camouflage" }}</td>
<td>
<a-form-item>
<a-switch
:checked="outbound.stream.tcp.type === 'http'"
@change="checked => outbound.stream.tcp.type = checked ? 'http' : 'none'">
</a-switch>
</a-form-item>
</td>
</tr>
<template v-if="outbound.stream.tcp.type == 'http'">
<tr>
<td>{{ i18n "host" }}</td>
<td>
<a-form-item>
<a-input style="width: 250px;" v-model.trim="outbound.stream.tcp.host"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "path" }}</td>
<td>
<a-form-item>
<a-input style="width: 250px;" v-model.trim="outbound.stream.tcp.path"></a-input>
</a-form-item>
</td>
</tr>
</template>
</template>
<!-- kcp -->
<template v-if="outbound.stream.network === 'kcp'">
<tr>
<td>{{ i18n "camouflage" }}</td>
<td>
<a-form-item>
<a-select v-model="outbound.stream.kcp.type" style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="none">none (not camouflage)</a-select-option>
<a-select-option value="srtp">srtp (video call)</a-select-option>
<a-select-option value="utp">utp (BT download)</a-select-option>
<a-select-option value="wechat-video">wechat-video (WeChat video)</a-select-option>
<a-select-option value="dtls">dtls (DTLS 1.2 packages)</a-select-option>
<a-select-option value="wireguard">wireguard (wireguard packages)</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "password" }}</td>
<td>
<a-form-item>
<a-input v-model="outbound.stream.kcp.seed" style="width: 250px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>mtu</td>
<td>
<a-form-item>
<a-input-number v-model.number="outbound.stream.kcp.mtu"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>tti (ms)</td>
<td>
<a-form-item>
<a-input-number v-model.number="outbound.stream.kcp.tti"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>uplink capacity (MB/S)</td>
<td>
<a-form-item>
<a-input-number v-model.number="outbound.stream.kcp.upCap"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>downlink capacity (MB/S)</td>
<td>
<a-form-item>
<a-input-number v-model.number="outbound.stream.kcp.downCap"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>congestion</td>
<td>
<a-form-item>
<a-switch v-model="outbound.stream.kcp.congestion"></a-switch>
</a-form-item>
</td>
</tr>
<tr>
<td>read buffer size (MB)</td>
<td>
<a-form-item>
<a-input-number v-model.number="outbound.stream.kcp.readBuffer"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>write buffer size (MB)</td>
<td>
<a-form-item>
<a-input-number v-model.number="outbound.stream.kcp.writeBuffer"></a-input-number>
</a-form-item>
</td>
</tr>
</template>
<!-- ws -->
<template v-if="outbound.stream.network === 'ws'">
<tr>
<td>{{ i18n "host" }}</td>
<td><a-form-item><a-input style="width: 250px" v-model="outbound.stream.ws.host"></a-input></a-form-item></td>
</tr>
<tr>
<td>{{ i18n "path" }}</td>
<td><a-form-item><a-input style="width: 250px;" v-model.trim="outbound.stream.ws.path"></a-input></a-form-item></td>
</tr>
</template>
<!-- http -->
<template v-if="outbound.stream.network === 'http'">
<tr>
<td>{{ i18n "host" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="outbound.stream.http.host" style="width: 250px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "path" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="outbound.stream.http.path" style="width: 250px;"></a-input>
</a-form-item>
</td>
</tr>
</template>
<!-- quic -->
<template v-if="outbound.stream.network === 'quic'">
<tr>
<td>{{ i18n "pages.inbounds.stream.quic.encryption" }}</td>
<td>
<a-form-item>
<a-select v-model="outbound.stream.quic.security" style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="none">none</a-select-option>
<a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option>
<a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "password" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="outbound.stream.quic.key" style="width: 250px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "camouflage" }}</td>
<td>
<a-form-item>
<a-select v-model="outbound.stream.quic.type" style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="none">none (not camouflage)</a-select-option>
<a-select-option value="srtp">srtp (video call)</a-select-option>
<a-select-option value="utp">utp (BT download)</a-select-option>
<a-select-option value="wechat-video">wechat-video (WeChat video)</a-select-option>
<a-select-option value="dtls">dtls (DTLS 1.2 packages)</a-select-option>
<a-select-option value="wireguard">wireguard (wireguard packages)</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
</template>
<!-- grpc -->
<template v-if="outbound.stream.network === 'grpc'">
<tr>
<td>serviceName</td>
<td>
<a-form-item>
<a-input v-model.trim="outbound.stream.grpc.serviceName" style="width: 250px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>MultiMode</td>
<td>
<a-form-item>
<a-switch v-model="outbound.stream.grpc.multiMode"></a-switch>
</a-form-item>
</td>
</tr>
</template>
</template>
<!-- tls settings -->
<template v-if="outbound.canEnableTls()">
<tr>
<td>{{ i18n "security" }}</td>
<td>
<a-form-item>
<a-radio-group v-model="outbound.stream.security" button-style="solid">
<a-radio-button value="none">{{ i18n "none" }}</a-radio-button>
<a-radio-button value="tls">TLS</a-radio-button>
<a-radio-button v-if="outbound.canEnableReality()" value="reality">Reality</a-radio-button>
</a-radio-group>
</a-form-item>
</td>
</tr>
<template v-if="outbound.stream.isTls">
<tr>
<td>SNI</td>
<td>
<a-form-item placeholder="Server Name Indication">
<a-input v-model.trim="outbound.stream.tls.serverName" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>uTLS</td>
<td>
<a-form-item>
<a-select v-model="outbound.stream.tls.fingerprint"
style="width: 250px" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value=''>None</a-select-option>
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>ALPN</td>
<td>
<a-form-item>
<a-select
mode="multiple"
style="width: 250px"
:dropdown-class-name="themeSwitcher.currentTheme"
v-model="outbound.stream.tls.alpn">
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>Allow insecure</td>
<td>
<a-form-item>
<a-switch v-model="outbound.stream.tls.allowInsecure"></a-switch>
</a-form-item>
</td>
</tr>
</template>
<!-- reality settings -->
<template v-if="outbound.stream.isReality">
<tr>
<td>{{ i18n "domainName" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="outbound.stream.reality.serverName" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>uTLS</td>
<td>
<a-form-item>
<a-select v-model="outbound.stream.reality.fingerprint"
style="width: 250px" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>Short Id</td>
<td>
<a-form-item>
<a-input v-model.trim="outbound.stream.reality.shortId" style="width:250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>SpiderX</td>
<td>
<a-form-item>
<a-input v-model.trim="outbound.stream.reality.spiderX" style="width:250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>Public Key</td>
<td>
<a-form-item>
<a-input v-model.trim="outbound.stream.reality.publicKey" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
</template>
</template>
</table>
</a-form>
</a-tab-pane>
<a-tab-pane key="2" tab="JSON" force-render="true">
<a-form-item style="margin: 10px 0">
Link: <a-input v-model.trim="outModal.link" style="width: 300px; margin-right: 5px;" placeholder="vmess:// vless:// trojan:// ss://"></a-input>
<a-button @click="convertLink" type="primary"><a-icon type="form"></a-icon></a-button>
</a-form-item>
<textarea style="position:absolute; left: -800px;" id="outboundJson"></textarea>
</a-tab-pane>
</a-tabs>
{{end}}

View file

@ -1,22 +1,42 @@
{{define "form/dokodemo"}}
<a-form layout="inline">
<a-form-item label='{{ i18n "pages.inbounds.targetAddress"}}'>
<a-input v-model.trim="inbound.settings.address"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.destinationPort"}}'>
<a-input-number v-model="inbound.settings.port"></a-input-number>
</a-form-item>
<br>
<a-form-item label='{{ i18n "pages.inbounds.network"}}'>
<a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="tcp,udp">TCP+UDP</a-select-option>
<a-select-option value="tcp">TCP</a-select-option>
<a-select-option value="udp">UDP</a-select-option>
</a-select>
</a-form-item>
<br>
<a-form-item label="FollowRedirect">
<a-switch v-model="inbound.settings.followRedirect"></a-switch>
</a-form-item>
<table width="100%" class="ant-table-tbody">
<tr>
<td>{{ i18n "pages.inbounds.targetAddress"}}</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.settings.address"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.destinationPort"}}</td>
<td>
<a-form-item>
<a-input-number v-model.number="inbound.settings.port"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.network"}}</td>
<td>
<a-form-item>
<a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="tcp,udp">tcp+udp</a-select-option>
<a-select-option value="tcp">tcp</a-select-option>
<a-select-option value="udp">udp</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>FollowRedirect</td>
<td>
<a-form-item>
<a-switch v-model="inbound.settings.followRedirect"></a-switch>
</a-form-item>
</td>
</tr>
</table>
</a-form>
{{end}}

View file

@ -1,19 +1,21 @@
{{define "form/http"}}
<a-form layout="inline">
<a-form-item>
<a-row>
<a-button type="primary" size="small" @click="inbound.settings.addAccount(new Inbound.SocksSettings.SocksAccount())">+</a-button>
</a-row>
<a-input-group v-for="(account, index) in inbound.settings.accounts">
<a-input style="width: 45%" v-model.trim="account.user"
addon-before='{{ i18n "username" }}'></a-input>
<a-input style="width: 55%" v-model.trim="account.pass"
addon-before='{{ i18n "password" }}'>
<table style="width: 100%; text-align: center; margin-bottom: 10px;">
<tr>
<td width="45%">{{ i18n "username" }}</td>
<td width="45%">{{ i18n "password" }}</td>
<td><a-button size="small" @click="inbound.settings.addAccount(new Inbound.HttpSettings.HttpAccount())">+</a-button></td>
</tr>
</table>
<a-input-group compact v-for="(account, index) in inbound.settings.accounts" style="margin-bottom: 10px;">
<a-input style="width: 50%" v-model.trim="account.user" placeholder='{{ i18n "username" }}'>
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
</a-input>
<a-input style="width: 50%" v-model.trim="account.pass" placeholder='{{ i18n "password" }}'>
<template slot="addonAfter">
<a-button type="primary" size="small" @click="inbound.settings.delAccount(index)">-</a-button>
<a-button size="small" @click="inbound.settings.delAccount(index)">-</a-button>
</template>
</a-input>
</a-input-group>
</a-form-item>
</a-form>
{{end}}

View file

@ -1,96 +1,9 @@
{{define "form/shadowsocks"}}
<a-form layout="inline" style="padding: 10px 0px;">
<a-form layout="inline">
<template v-if="inbound.isSSMultiUser">
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.shadowsockses.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template>
</a-tooltip>
</span>
<a-icon @click="client.email = RandomUtil.randomLowerAndNum(8)" type="sync"> </a-icon>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item>
<a-form-item label="Password">
<a-icon @click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
<a-input v-model.trim="client.password" style="width: 250px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email && app.subSettings.enable">
<span slot="label">
Subscription
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"> </a-icon>
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email && app.tgBotEnable">
<span slot="label">
Telegram ID
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="client.tgId"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.IPLimit" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="client.limitIp" min="0"></a-input-number>
</a-form-item>
<br>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
</a-form-item>
<br>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
</a-form-item>
<br>
<a-form-item v-if="delayedStart" label='{{ i18n "pages.client.expireDays" }}'>
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
<a-form-item v-else>
<span slot="label">
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.darkCardClass"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
</a-form-item>
{{template "form/client"}}
</a-collapse-panel>
</a-collapse>
<a-collapse v-else>
@ -107,24 +20,40 @@
</table>
</a-collapse-panel>
</a-collapse>
</template>
</a-form>
<a-form layout="inline">
<a-form-item label='{{ i18n "encryption" }}'>
<a-select v-model="inbound.settings.method" style="width: 250px;" :dropdown-class-name="themeSwitcher.darkCardClass" @change="SSMethodChange">
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-if="inbound.isSS2022" label='{{ i18n "password" }}'>
<a-icon @click="inbound.settings.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
<a-input v-model.trim="inbound.settings.password" style="width: 250px;"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.network" }}'>
<a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="tcp,udp">TCP+UDP</a-select-option>
<a-select-option value="tcp">TCP</a-select-option>
<a-select-option value="udp">UDP</a-select-option>
</a-select>
</a-form-item>
</template>
<table width="100%" class="ant-table-tbody">
<tr>
<td>{{ i18n "encryption" }}</td>
<td>
<a-form-item>
<a-select v-model="inbound.settings.method" style="width: 250px;" @change="SSMethodChange" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr v-if="inbound.isSS2022">
<td>{{ i18n "password" }}
<a-icon @click="inbound.settings.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.settings.password" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.network" }}</td>
<td>
<a-form-item>
<a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="tcp,udp">tcp+udp</a-select-option>
<a-select-option value="tcp">tcp</a-select-option>
<a-select-option value="udp">udp</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
</table>
</a-form>
{{end}}

View file

@ -1,33 +1,52 @@
{{define "form/socks"}}
<a-form layout="inline">
<a-form-item label='{{ i18n "password" }}'>
<a-switch :checked="inbound.settings.auth === 'password'"
@change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
</a-form-item>
<br>
<template v-if="inbound.settings.auth === 'password'">
<a-form-item>
<a-row>
<a-button type="primary" size="small" @click="inbound.settings.addAccount(new Inbound.SocksSettings.SocksAccount())">+</a-button>
</a-row>
<a-input-group v-for="(account, index) in inbound.settings.accounts">
<a-input style="width: 45%" v-model.trim="account.user"
addon-before='{{ i18n "username" }}'></a-input>
<a-input style="width: 55%" v-model.trim="account.pass"
addon-before='{{ i18n "password" }}'>
<template slot="addonAfter">
<a-button type="primary" size="small" @click="inbound.settings.delAccount(index)">-</a-button>
</template>
</a-input>
</a-input-group>
</a-form-item>
</template>
<br>
<a-form-item label='{{ i18n "pages.inbounds.enable" }} udp'>
<a-switch v-model="inbound.settings.udp"></a-switch>
</a-form-item>
<a-form-item v-if="inbound.settings.udp" label="IP">
<a-input v-model.trim="inbound.settings.ip"></a-input>
</a-form-item>
<table width="100%" class="ant-table-tbody">
<tr>
<td style="width: 30%;">{{ i18n "password" }}</td>
<td>
<a-form-item>
<a-switch :checked="inbound.settings.auth === 'password'"
@change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
</a-form-item>
</td>
</tr>
<tr v-if="inbound.settings.auth === 'password'">
<td colspan="2">
<table style="width: 100%; text-align: center; margin-bottom: 10px;">
<tr>
<td width="45%">{{ i18n "username" }}</td>
<td width="45%">{{ i18n "password" }}</td>
<td><a-button size="small" @click="inbound.settings.addAccount(new Inbound.SocksSettings.SocksAccount())">+</a-button></td>
</tr>
</table>
<a-input-group compact v-for="(account, index) in inbound.settings.accounts" style="margin-bottom: 10px;">
<a-input style="width: 50%" v-model.trim="account.user" placeholder='{{ i18n "username" }}'>
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
</a-input>
<a-input style="width: 50%" v-model.trim="account.pass" placeholder='{{ i18n "password" }}'>
<template slot="addonAfter">
<a-button size="small" @click="inbound.settings.delAccount(index)">-</a-button>
</template>
</a-input>
</a-input-group>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.enable" }} udp</td>
<td>
<a-form-item>
<a-switch v-model="inbound.settings.udp"></a-switch>
</a-form-item>
</td>
</tr>
<tr v-if="inbound.settings.udp">
<td>IP</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.settings.ip"></a-input>
</a-form-item>
</td>
</tr>
</table>
</a-form>
{{end}}

View file

@ -2,100 +2,7 @@
<a-form layout="inline" style="padding: 10px 0px;">
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.trojans.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template>
</a-tooltip>
</span>
<a-icon @click="client.email = RandomUtil.randomLowerAndNum(8)" type="sync"> </a-icon>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item>
<a-form-item label="Password">
<a-icon @click="client.password = RandomUtil.randomSeq(10)" type="sync"> </a-icon>
<a-input v-model.trim="client.password" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email && app.subSettings.enable">
<span slot="label">
Subscription
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"> </a-icon>
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email && app.tgBotEnable">
<span slot="label">
Telegram ID
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="client.tgId"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.IPLimit" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="client.limitIp" min="0"></a-input-number>
</a-form-item>
<br>
<a-form-item v-if="inbound.xtls" label="Flow">
<a-select v-model="client.flow" style="width: 150px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="">{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
</a-form-item>
<br>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
</a-form-item>
<br>
<a-form-item v-if="delayedStart" label='{{ i18n "pages.client.expireDays" }}'>
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
<a-form-item v-else>
<span slot="label">
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.darkCardClass"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
</a-form-item>
{{template "form/client"}}
</a-collapse-panel>
</a-collapse>
<a-collapse v-else>
@ -117,7 +24,7 @@
<a-form layout="inline">
<a-form-item label="Fallbacks">
<a-row>
<a-button type="primary" size="small" @click="inbound.settings.addTrojanFallback()">
<a-button type="primary" size="small" @click="inbound.settings.addFallback()">
+
</a-button>
</a-row>
@ -126,27 +33,54 @@
<!-- trojan fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
<a-divider>
<a-divider style="margin:0;">
fallback[[ index + 1 ]]
<a-icon type="delete" @click="() => inbound.settings.delTrojanFallback(index)"
<a-icon type="delete" @click="() => inbound.settings.delFallback(index)"
style="color: rgb(255, 77, 79);cursor: pointer;"/>
</a-divider>
<a-form-item label="Name">
<a-input v-model="fallback.name"></a-input>
</a-form-item>
<a-form-item label="Alpn">
<a-input v-model="fallback.alpn"></a-input>
</a-form-item>
<a-form-item label="Path">
<a-input v-model="fallback.path"></a-input>
</a-form-item>
<a-form-item label="Dest">
<a-input v-model="fallback.dest"></a-input>
</a-form-item>
<a-form-item label="xVer">
<a-input-number v-model="fallback.xver"></a-input-number>
</a-form-item>
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
<table width="100%">
<tr>
<td style="width: 20%;">Name</td>
<td>
<a-form-item>
<a-input v-model="fallback.name" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>Alpn</td>
<td>
<a-form-item>
<a-input v-model="fallback.alpn" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>Path</td>
<td>
<a-form-item>
<a-input v-model="fallback.path" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>Dest</td>
<td>
<a-form-item>
<a-input v-model="fallback.dest" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>xVer</td>
<td>
<a-form-item>
<a-input-number v-model="fallback.xver" :min="0" :max="2"></a-input-number>
</a-form-item>
</td>
</tr>
</table>
</a-form>
<a-divider style="margin:0;"></a-divider>
</template>
{{end}}

View file

@ -2,106 +2,7 @@
<a-form layout="inline" style="padding: 10px 0px;">
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template>
</a-tooltip>
</span>
<a-icon @click="client.email = RandomUtil.randomLowerAndNum(8)" type="sync"> </a-icon>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item>
<a-form-item label="ID">
<a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon>
<a-input v-model.trim="client.id" style="width: 300px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email && app.subSettings.enable">
<span slot="label">
Subscription
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"> </a-icon>
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email && app.tgBotEnable">
<span slot="label">
Telegram ID
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="client.tgId"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.IPLimit" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="client.limitIp" min="0"></a-input-number>
</a-form-item>
<br>
<a-form-item v-if="inbound.xtls" label="Flow">
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-else-if="inbound.canEnableTlsFlow()" label="Flow">
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
</a-form-item>
<br>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
</a-form-item>
<br>
<a-form-item v-if="delayedStart" label='{{ i18n "pages.client.expireDays" }}'>
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
<a-form-item v-else>
<span slot="label">
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.darkCardClass"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
</a-form-item>
{{template "form/client"}}
</a-collapse-panel>
</a-collapse>
<a-collapse v-else>
@ -134,27 +35,54 @@
<!-- vless fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
<a-divider>
<a-divider style="margin:0;">
fallback[[ index + 1 ]]
<a-icon type="delete" @click="() => inbound.settings.delFallback(index)"
style="color: rgb(255, 77, 79);cursor: pointer;"/>
</a-divider>
<a-form-item label="Name">
<a-input v-model="fallback.name"></a-input>
</a-form-item>
<a-form-item label="Alpn">
<a-input v-model="fallback.alpn"></a-input>
</a-form-item>
<a-form-item label="Path">
<a-input v-model="fallback.path"></a-input>
</a-form-item>
<a-form-item label="Dest">
<a-input v-model="fallback.dest"></a-input>
</a-form-item>
<a-form-item label="xVer">
<a-input-number v-model="fallback.xver"></a-input-number>
</a-form-item>
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
<table width="100%">
<tr>
<td style="width: 20%;">Name</td>
<td>
<a-form-item>
<a-input v-model="fallback.name" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>Alpn</td>
<td>
<a-form-item>
<a-input v-model="fallback.alpn" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>Path</td>
<td>
<a-form-item>
<a-input v-model="fallback.path" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>Dest</td>
<td>
<a-form-item>
<a-input v-model="fallback.dest" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>xVer</td>
<td>
<a-form-item>
<a-input-number v-model="fallback.xver" :min="0" :max="2"></a-input-number>
</a-form-item>
</td>
</tr>
</table>
</a-form>
<a-divider style="margin:0;"></a-divider>
</template>
{{end}}

View file

@ -2,95 +2,7 @@
<a-form layout="inline" style="padding: 10px 0px;">
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vmesses.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template>
</a-tooltip>
</span>
<a-icon @click="client.email = RandomUtil.randomLowerAndNum(8)" type="sync"> </a-icon>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item>
<br>
<a-form-item label="ID">
<a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon>
<a-input v-model.trim="client.id" style="width: 300px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email && app.subSettings.enable">
<span slot="label">
Subscription
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"> </a-icon>
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email && app.tgBotEnable">
<span slot="label">
Telegram ID
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="client.tgId"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.IPLimit" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="client.limitIp" min="0"></a-input-number>
</a-form-item>
<br>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
</a-form-item>
<br>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
</a-form-item>
<br>
<a-form-item v-if="delayedStart" label='{{ i18n "pages.client.expireDays" }}'>
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
<a-form-item v-else>
<span slot="label">
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.darkCardClass"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
</a-form-item>
{{template "form/client"}}
</a-collapse-panel>
</a-collapse>
<a-collapse v-else>

View file

@ -0,0 +1,32 @@
{{define "form/externalProxy"}}
<a-form layout="inline">
<a-divider style="margin:0;"></a-divider>
<a-form-item label="External Proxy">
<a-switch v-model="externalProxy"></a-switch>
<a-button v-if="externalProxy" type="primary" style="margin-left: 10px" size="small" @click="inbound.stream.externalProxy.push({forceTls: 'same', dest: '', port: 443, remark: ''})">+</a-button>
</a-form-item>
<table width="100%" class="ant-table-tbody" v-if="externalProxy" style="margin-bottom:5px">
<tr style="line-height: 40px;">
<td width="100%">
<a-input-group style="margin: 0 5px;" compact v-for="(row, index) in inbound.stream.externalProxy">
<template>
<a-tooltip title="Force TLS">
<a-select v-model="row.forceTls" style="width:20%; margin: 0px" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="same">{{ i18n "pages.inbounds.same" }}</a-select-option>
<a-select-option value="none">{{ i18n "none" }}</a-select-option>
<a-select-option value="tls">TLS</a-select-option>
</a-select>
</a-tooltip>
</template>
<a-input style="width: 35%" v-model.trim="row.dest" placeholder='{{ i18n "host" }}'></a-input>
<a-tooltip title='{{ i18n "pages.inbounds.port" }}'>
<a-input-number style="width: 15%;" v-model.number="row.port" min="1" max="65531"></a-input-number>
</a-tooltip>
<a-input style="width: 20%" v-model.trim="row.remark" placeholder='{{ i18n "remark" }}'></a-input>
<a-button style="width: 10%; margin: 0px" @click="inbound.stream.externalProxy.splice(index, 1)">-</a-button>
</a-input-group>
</td>
</tr>
</table>
</a-form>
{{end}}

View file

@ -1,10 +1,22 @@
{{define "form/streamGRPC"}}
<a-form layout="inline">
<a-form-item label="ServiceName">
<a-input v-model.trim="inbound.stream.grpc.serviceName"></a-input>
</a-form-item>
<a-form-item label="Multi Mode">
<a-switch v-model="inbound.stream.grpc.multiMode"></a-switch>
</a-form-item>
<table width="100%" class="ant-table-tbody">
<tr>
<td>serviceName</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.grpc.serviceName" style="width: 250px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>MultiMode</td>
<td>
<a-form-item>
<a-switch v-model="inbound.stream.grpc.multiMode"></a-switch>
</a-form-item>
</td>
</tr>
</table>
</a-form>
{{end}}

View file

@ -1,12 +1,24 @@
{{define "form/streamHTTP"}}
<a-form layout="inline">
<a-form-item label='{{ i18n "path" }}'>
<a-input v-model.trim="inbound.stream.http.path"></a-input>
</a-form-item>
<a-form-item label="Host">
<a-row v-for="(host, index) in inbound.stream.http.host">
<a-input v-model.trim="inbound.stream.http.host[index]"></a-input>
</a-row>
</a-form-item>
<table width="100%" class="ant-table-tbody">
<tr>
<td>{{ i18n "path" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.http.path" style="width: 250px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>host</td>
<td>
<a-form-item>
<a-row v-for="(host, index) in inbound.stream.http.host">
<a-input v-model.trim="inbound.stream.http.host[index]" style="width: 250px;"></a-input>
</a-row>
</a-form-item>
</td>
</tr>
</table>
</a-form>
{{end}}

View file

@ -1,47 +1,86 @@
{{define "form/streamKCP"}}
<a-form layout="inline">
<a-form-item label='{{ i18n "camouflage" }}'>
<a-select v-model="inbound.stream.kcp.type" style="width: 280px;" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="none">None (Not Camouflage)</a-select-option>
<a-select-option value="srtp">SRTP (Camouflage Video Call)</a-select-option>
<a-select-option value="utp">UTP (Camouflage BT Download)</a-select-option>
<a-select-option value="wechat-video">Wechat-Video (Camouflage WeChat Video)</a-select-option>
<a-select-option value="dtls">DTLS (Camouflage DTLS 1.2 Packages)</a-select-option>
<a-select-option value="wireguard">Wireguard (Camouflage Wireguard Packages)</a-select-option>
</a-select>
</a-form-item>
<br>
<a-form-item label='{{ i18n "password" }}'>
<a-icon @click="inbound.stream.kcp.seed = RandomUtil.randomSeq(10)" type="sync"> </a-icon>
<a-input v-model="inbound.stream.kcp.seed" style="width: 150px;" ></a-input>
</a-form-item>
<br>
<a-form-item label="MTU">
<a-input-number v-model="inbound.stream.kcp.mtu"></a-input-number>
</a-form-item>
<br>
<a-form-item label="TTI (ms)">
<a-input-number v-model="inbound.stream.kcp.tti"></a-input-number>
</a-form-item>
<br>
<a-form-item label="Uplink Capacity (MB/S)">
<a-input-number v-model="inbound.stream.kcp.upCap"></a-input-number>
</a-form-item>
<br>
<a-form-item label="Downlink Capacity (MB/S)">
<a-input-number v-model="inbound.stream.kcp.downCap"></a-input-number>
</a-form-item>
<br>
<a-form-item label="Congestion">
<a-switch v-model="inbound.stream.kcp.congestion"></a-switch>
</a-form-item>
<br>
<a-form-item label="Read Buffer Size (MB)">
<a-input-number v-model="inbound.stream.kcp.readBuffer"></a-input-number>
</a-form-item>
<br>
<a-form-item label="Write Buffer Size (MB)">
<a-input-number v-model="inbound.stream.kcp.writeBuffer"></a-input-number>
</a-form-item>
<table width="100%" class="ant-table-tbody">
<tr>
<td>{{ i18n "camouflage" }}</td>
<td>
<a-form-item>
<a-select v-model="inbound.stream.kcp.type" style="width: 250px;"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="none">None (Not Camouflage)</a-select-option>
<a-select-option value="srtp">SRTP (Camouflage Video Call)</a-select-option>
<a-select-option value="utp">UTP (Camouflage BT Download)</a-select-option>
<a-select-option value="wechat-video">Wechat-Video (Camouflage WeChat Video)</a-select-option>
<a-select-option value="dtls">DTLS (Camouflage DTLS 1.2 Packages)</a-select-option>
<a-select-option value="wireguard">Wireguard (Camouflage Wireguard Packages)</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "password" }}</td>
<td>
<a-form-item>
<a-input v-model="inbound.stream.kcp.seed" style="width: 250px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>MTU</td>
<td>
<a-form-item>
<a-input-number v-model.number="inbound.stream.kcp.mtu"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>TTI (ms)</td>
<td>
<a-form-item>
<a-input-number v-model.number="inbound.stream.kcp.tti"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>Uplink Capacity (MB/S)</td>
<td>
<a-form-item>
<a-input-number v-model.number="inbound.stream.kcp.upCap"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>Downlink Capacity (MB/S)</td>
<td>
<a-form-item>
<a-input-number v-model.number="inbound.stream.kcp.downCap"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>Congestion</td>
<td>
<a-form-item>
<a-switch v-model="inbound.stream.kcp.congestion"></a-switch>
</a-form-item>
</td>
</tr>
<tr>
<td>Read Buffer Size (MB)</td>
<td>
<a-form-item>
<a-input-number v-model.number="inbound.stream.kcp.readBuffer"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>Write Buffer Size (MB)</td>
<td>
<a-form-item>
<a-input-number v-model.number="inbound.stream.kcp.writeBuffer"></a-input-number>
</a-form-item>
</td>
</tr>
</table>
</a-form>
{{end}}

View file

@ -1,25 +1,43 @@
{{define "form/streamQUIC"}}
<a-form layout="inline">
<a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'>
<a-select v-model="inbound.stream.quic.security" style="width: 165px;" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="none">none</a-select-option>
<a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option>
<a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='{{ i18n "password" }}'>
<a-icon @click="inbound.stream.quic.key = RandomUtil.randomSeq(10)" type="sync"> </a-icon>
<a-input v-model.trim="inbound.stream.quic.key" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "camouflage" }}'>
<a-select v-model="inbound.stream.quic.type" style="width: 280px;" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="none">none (not camouflage)</a-select-option>
<a-select-option value="srtp">srtp (camouflage video call)</a-select-option>
<a-select-option value="utp">utp (camouflage BT download)</a-select-option>
<a-select-option value="wechat-video">wechat-video (camouflage WeChat video)</a-select-option>
<a-select-option value="dtls">dtls (camouflage DTLS 1.2 packages)</a-select-option>
<a-select-option value="wireguard">wireguard (camouflage wireguard packages)</a-select-option>
</a-select>
</a-form-item>
<table width="100%" class="ant-table-tbody">
<tr>
<td>{{ i18n "pages.inbounds.stream.quic.encryption" }}</td>
<td>
<a-form-item>
<a-select v-model="inbound.stream.quic.security" style="width: 250px;"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="none">none</a-select-option>
<a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option>
<a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "password" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.quic.key" style="width: 250px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "camouflage" }}</td>
<td>
<a-form-item>
<a-select v-model="inbound.stream.quic.type" style="width: 280px;"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="none">none (not camouflage)</a-select-option>
<a-select-option value="srtp">srtp (camouflage video call)</a-select-option>
<a-select-option value="utp">utp (camouflage BT download)</a-select-option>
<a-select-option value="wechat-video">wechat-video (camouflage WeChat video)</a-select-option>
<a-select-option value="dtls">dtls (camouflage DTLS 1.2 packages)</a-select-option>
<a-select-option value="wireguard">wireguard (camouflage wireguard packages)</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
</table>
</a-form>
{{end}}

View file

@ -2,7 +2,8 @@
<!-- select stream network -->
<a-form layout="inline">
<a-form-item label='{{ i18n "transmission" }}'>
<a-select v-model="inbound.stream.network" @change="streamNetworkChange" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select v-model="inbound.stream.network" @change="streamNetworkChange"
:dropdown-class-name="themeSwitcher.currentTheme" style="width: 100px;">
<a-select-option value="tcp">TCP</a-select-option>
<a-select-option value="kcp">KCP</a-select-option>
<a-select-option value="ws">WS</a-select-option>

View file

@ -33,7 +33,7 @@
<td>
<a-form-item>
<a-select v-model="inbound.stream.sockopt.tproxy" style="width: 250px;"
:dropdown-class-name="themeSwitcher.darkCardClass">
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="off">OFF</a-select-option>
<a-select-option value="redirect">Redirect</a-select-option>
<a-select-option value="tproxy">T-Proxy</a-select-option>

View file

@ -1,7 +1,7 @@
{{define "form/streamTCP"}}
<!-- tcp type -->
<a-form layout="inline">
<a-form-item label="AcceptProxyProtocol">
<a-form-item label="Accept Proxy Protocol" v-if="inbound.canEnableTls()">
<a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch>
</a-form-item>
<a-form-item label='HTTP {{ i18n "camouflage" }}'>
@ -14,63 +14,100 @@
<!-- tcp request -->
<a-form v-if="inbound.stream.tcp.type === 'http'" layout="inline">
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestVersion" }}'>
<a-input v-model.trim="inbound.stream.tcp.request.version"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestMethod" }}'>
<a-input v-model.trim="inbound.stream.tcp.request.method"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestPath" }}'>
<a-row v-for="(path, index) in inbound.stream.tcp.request.path">
<a-input v-model.trim="inbound.stream.tcp.request.path[index]"></a-input>
</a-row>
</a-form-item>
<br>
<a-form-item>
<a-row>
<span>{{ i18n "pages.inbounds.stream.general.requestHeader" }}:</span>
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.tcp.request.addHeader('Host', 'xxx.com')">+</a-button>
</a-row>
<a-input-group v-for="(header, index) in inbound.stream.tcp.request.headers">
<a-input style="width: 50%" v-model.trim="header.name"
addon-before='{{ i18n "pages.inbounds.stream.general.name" }}'></a-input>
<a-input style="width: 50%" v-model.trim="header.value"
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter">
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.tcp.request.removeHeader(index)">-</a-button>
</template>
</a-input>
</a-input-group>
</a-form-item>
</a-form>
<!-- tcp response -->
<a-form v-if="inbound.stream.tcp.type === 'http'" layout="inline">
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseVersion" }}'>
<a-input v-model.trim="inbound.stream.tcp.response.version"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseStatus" }}'>
<a-input v-model.trim="inbound.stream.tcp.response.status"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseStatusDescription" }}'>
<a-input v-model.trim="inbound.stream.tcp.response.reason"></a-input>
</a-form-item>
<a-form-item>
<a-row>
<span>{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}:</span>
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">+</a-button>
</a-row>
<a-input-group v-for="(header, index) in inbound.stream.tcp.response.headers">
<a-input style="width: 50%" v-model.trim="header.name"
addon-before='{{ i18n "pages.inbounds.stream.general.name" }}'></a-input>
<a-input style="width: 50%" v-model.trim="header.value"
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter">
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.tcp.response.removeHeader(index)">-</a-button>
</template>
</a-input>
</a-input-group>
</a-form-item>
<table width="100%" class="ant-table-tbody">
<tr>
<td>{{ i18n "pages.inbounds.stream.tcp.requestVersion" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.tcp.request.version" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.stream.tcp.requestMethod" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.tcp.request.method" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td style="vertical-align: top; padding-top: 10px;">{{ i18n "pages.inbounds.stream.tcp.requestPath" }}
<a-button size="small" @click="inbound.stream.tcp.request.addPath('/')">+</a-button>
</td>
<td>
<a-form-item>
<a-row v-for="(path, index) in inbound.stream.tcp.request.path">
<a-input v-model.trim="inbound.stream.tcp.request.path[index]" style="width: 200px;">
<a-button size="small" slot="addonAfter"
@click="inbound.stream.tcp.request.removePath(index)"
v-if="inbound.stream.tcp.request.path.length>1">-</a-button>
</a-input>
</a-row>
</a-form-item>
</td>
</tr>
<tr>
<td colspan="2" width="100%">
<a-form-item>
<span>{{ i18n "pages.inbounds.stream.general.requestHeader" }}:</span>
<a-button size="small" style="margin-left: 10px" @click="inbound.stream.tcp.request.addHeader('', '')">+</a-button>
<a-input-group compact v-for="(header, index) in inbound.stream.tcp.request.headers">
<a-input style="width: 50%" v-model.trim="header.name" placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
</a-input>
<a-input style="width: 50%" v-model.trim="header.value" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<a-button slot="addonAfter" size="small" @click="inbound.stream.tcp.request.removeHeader(index)">-</a-button>
</a-input>
</a-input-group>
</a-form-item>
</td>
</tr>
<!-- tcp response -->
<tr>
<td>{{ i18n "pages.inbounds.stream.tcp.responseVersion" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.tcp.response.version" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.stream.tcp.responseStatus" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.tcp.response.status" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.stream.tcp.responseStatusDescription" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.tcp.response.reason" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td colspan="2" width="100%">
<a-form-item>
<span>{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}:</span>
<a-button size="small" style="margin-left: 10px"
@click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">+</a-button>
<a-input-group compact v-for="(header, index) in inbound.stream.tcp.response.headers">
<a-input style="width: 50%" v-model.trim="header.name" placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
</a-input>
<a-input style="width: 50%" v-model.trim="header.value"
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter">
<a-button size="small" @click="inbound.stream.tcp.response.removeHeader(index)">-</a-button>
</template>
</a-input>
</a-input-group>
</a-form-item>
</td>
</tr>
</table>
</a-form>
{{end}}

View file

@ -11,15 +11,17 @@
<a-form-item>
<a-row>
<span>{{ i18n "pages.inbounds.stream.general.requestHeader" }}:</span>
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.ws.addHeader('Host', '')">+</a-button>
<a-button type="primary" size="small" style="margin-left: 10px"
@click="inbound.stream.ws.addHeader('Host', '')">+</a-button>
</a-row>
<a-input-group v-for="(header, index) in inbound.stream.ws.headers">
<a-input style="width: 50%" v-model.trim="header.name"
addon-before='{{ i18n "pages.inbounds.stream.general.name"}}'></a-input>
addon-before='{{ i18n "pages.inbounds.stream.general.name"}}'></a-input>
<a-input style="width: 50%" v-model.trim="header.value"
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter">
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.ws.removeHeader(index)">-</a-button>
<a-button type="primary" size="small" style="margin-left: 10px"
@click="inbound.stream.ws.removeHeader(index)">-</a-button>
</template>
</a-input>
</a-input-group>

View file

@ -1,219 +1,395 @@
{{define "form/tlsSettings"}}
<!-- tls enable -->
<a-form layout="inline" v-if="inbound.canSetTls()">
<a-form-item v-if="inbound.canEnableTls()" label="TLS">
<a-switch v-model="inbound.tls">
</a-switch>
</a-form-item>
<a-form-item v-if="inbound.canEnableReality()">
<span slot="label">
Reality
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.realityDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-switch v-model="inbound.reality"></a-switch>
</a-form-item>
<a-form-item v-if="inbound.canEnableXtls()">
<span slot="label">
XTLS
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.xtlsDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-switch v-model="inbound.xtls"></a-switch>
</a-form-item>
</a-form>
<!-- tls settings -->
<a-form v-if="inbound.tls" layout="inline">
<a-form-item label='Multi Domain'>
<a-switch v-model="multiDomain"></a-switch>
</a-form-item>
<a-form-item v-if="multiDomain">
<a-row>
<span>Domains:</span>
<a-button v-if="multiDomain" type="primary" size="small" @click="inbound.stream.tls.settings.domains.push({remark: '', domain: ''})" style="margin-left: 10px">+</a-button>
</a-row>
<a-input-group v-for="(row, index) in inbound.stream.tls.settings.domains">
<a-input style="width: 40%" v-model.trim="row.remark" addon-before='{{ i18n "remark" }}'></a-input>
<a-input style="width: 60%" v-model.trim="row.domain" addon-before='{{ i18n "host" }}'>
<template slot="addonAfter">
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.tls.settings.domains.splice(index, 1)">-</a-button>
<a-form v-if="inbound.canEnableTls()" layout="inline">
<a-divider style="margin:0;"></a-divider>
<table width="100%" class="ant-table-tbody">
<tr>
<td colspan="2">
<a-form-item label='{{ i18n "security" }}'>
<a-radio-group v-model="inbound.stream.security" button-style="solid">
<a-radio-button value="none">{{ i18n "none" }}</a-radio-button>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.xtlsDesc" }}</span>
</template>
<a-radio-button v-if="inbound.canEnableXtls()" value="xtls">XTLS</a-radio-button>
</a-tooltip>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.realityDesc" }}</span>
</template>
<a-radio-button v-if="inbound.canEnableReality()" value="reality">Reality</a-radio-button>
</a-tooltip>
<a-radio-button value="tls">TLS</a-radio-button>
</a-radio-group>
</a-form-item>
</td>
</tr>
<!-- tls settings -->
<template v-if="inbound.stream.isTls">
<tr>
<td>SNI</td>
<td>
<a-form-item placeholder="Server Name Indication">
<a-input v-model.trim="inbound.stream.tls.sni" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>CipherSuites</td>
<td>
<a-form-item>
<a-select v-model="inbound.stream.tls.cipherSuites" style="width: 250px"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="">auto</a-select-option>
<a-select-option v-for="key,value in TLS_CIPHER_OPTION"
:value="key">[[ value]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>Min/Max Version</td>
<td>
<a-form-item>
<a-input-group compact>
<a-select style="width: 125px" v-model="inbound.stream.tls.minVersion"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in TLS_VERSION_OPTION"
:value="key">[[ key ]]</a-select-option>
</a-select>
<a-select style="width: 125px" v-model="inbound.stream.tls.maxVersion"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in TLS_VERSION_OPTION"
:value="key">[[ key ]]</a-select-option>
</a-select>
</a-input-group>
</a-form-item>
</td>
</tr>
<tr>
<td>uTLS</td>
<td>
<a-form-item>
<a-select v-model="inbound.stream.tls.settings.fingerprint" style="width: 250px"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value=''>None</a-select-option>
<a-select-option v-for="key in UTLS_FINGERPRINT"
:value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>ALPN</td>
<td>
<a-form-item>
<a-select mode="multiple" style="width: 250px"
:dropdown-class-name="themeSwitcher.currentTheme"
v-model="inbound.stream.tls.alpn">
<a-select-option v-for="alpn in ALPN_OPTION"
:value="alpn">[[ alpn ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>Allow insecure</td>
<td>
<a-form-item>
<a-switch v-model="inbound.stream.tls.settings.allowInsecure"></a-switch>
</a-form-item>
</td>
</tr>
<tr>
<td>Reject Unknown SNI</td>
<td>
<a-form-item>
<a-switch v-model="inbound.stream.tls.rejectUnknownSni"></a-switch>
</a-form-item>
</td>
</tr>
<template v-for="cert,index in inbound.stream.tls.certs">
<tr>
<td>{{ i18n "certificate" }}</td>
<td>
<a-form-item>
<a-radio-group v-model="cert.useFile" button-style="solid">
<a-radio-button
:value="true">{{ i18n "pages.inbounds.certificatePath"}}
</a-radio-button>
<a-radio-button
:value="false">{{ i18n "pages.inbounds.certificateContent"}}
</a-radio-button>
</a-radio-group>
<a-button v-if="index === 0" type="primary" size="small"
@click="inbound.stream.tls.addCert()" style="margin-left: 10px">+</a-button>
<a-button v-if="inbound.stream.tls.certs.length>1" type="primary" size="small"
@click="inbound.stream.tls.removeCert(index)" style="margin-left: 10px">-</a-button>
</a-form-item>
</td>
</tr>
<template v-if="cert.useFile">
<tr>
<td>{{ i18n "pages.inbounds.publicKeyPath" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="cert.certFile" style="width:250px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.keyPath" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="cert.keyFile" style="width:250px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td></td>
<td>
<a-button type="primary" icon="import" @click="setDefaultCertData(index)">
{{ i18n "pages.inbounds.setDefaultCert" }}
</a-button>
</td>
</tr>
</template>
</a-input>
</a-input-group>
</a-form-item>
<a-form-item v-else label='{{ i18n "domainName" }}'>
<a-input v-model.trim="inbound.stream.tls.server" style="width: 250px"></a-input>
</a-form-item>
<a-form-item label="CipherSuites">
<a-select v-model="inbound.stream.tls.cipherSuites" style="width: 300px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="">auto</a-select-option>
<a-select-option v-for="key in TLS_CIPHER_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="MinVersion">
<a-select v-model="inbound.stream.tls.minVersion" style="width: 60px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="MaxVersion">
<a-select v-model="inbound.stream.tls.maxVersion" style="width: 60px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="SNI" placeholder="Server Name Indication">
<a-input v-model.trim="inbound.stream.tls.settings.serverName" style="width: 250px"></a-input>
</a-form-item>
<a-form-item label="uTLS">
<a-select v-model="inbound.stream.tls.settings.fingerprint"
style="width: 170px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value=''>None</a-select-option>
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Alpn">
<a-select
mode="multiple"
style="width: 250px"
:dropdown-class-name="themeSwitcher.darkCardClass"
v-model="inbound.stream.tls.alpn">
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
</a-select>
</a-form-item>
<br>
<a-form-item label="Allow insecure">
<a-switch v-model="inbound.stream.tls.settings.allowInsecure"></a-switch>
</a-form-item>
<br>
<a-form-item label="Reject Unknown SNI">
<a-switch v-model="inbound.stream.tls.rejectUnknownSni"></a-switch>
</a-form-item>
<template v-for="cert,index in inbound.stream.tls.certs">
<a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="cert.useFile" button-style="solid">
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
</a-radio-group>
<a-button v-if="index === 0" type="primary" size="small" @click="inbound.stream.tls.addCert()" style="margin-left: 10px">+</a-button>
<a-button v-if="inbound.stream.tls.certs.length>1" type="primary" size="small" @click="inbound.stream.tls.removeCert(index)" style="margin-left: 10px">-</a-button>
</a-form-item>
<template v-if="cert.useFile">
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
<a-input v-model.trim="cert.certFile" style="width:300px;"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
<a-input v-model.trim="cert.keyFile" style="width:300px;"></a-input>
</a-form-item>
<a-button type="primary" icon="import" @click="setDefaultCertData(index)">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
<template v-else>
<tr>
<td>{{ i18n "pages.inbounds.publicKeyContent" }}</td>
<td>
<a-form-item>
<a-input type="textarea" :rows="3" style="width:250px;" v-model="cert.cert"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.keyContent" }}</td>
<td>
<a-form-item>
<a-input type="textarea" :rows="3" style="width:250px;" v-model="cert.key"></a-input>
</a-form-item>
</td>
</tr>
</template>
<tr>
<td>ocspStapling</td>
<td>
<a-form-item>
<a-input-number v-model.number="cert.ocspStapling" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
</template>
</template>
<template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="cert.cert"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="cert.key"></a-input>
</a-form-item>
</template>
<br>
<a-form-item label="ocspStapling">
<a-input-number v-model.number="cert.ocspStapling" :min="0"></a-input-number>
</a-form-item>
</template>
</a-form>
<!-- xtls settings -->
<a-form v-else-if="inbound.xtls" layout="inline">
<a-form-item label='{{ i18n "domainName" }}'>
<a-input v-model.trim="inbound.stream.xtls.server"></a-input>
</a-form-item>
<a-form-item label="SNI" placeholder="Server Name Indication">
<a-input v-model.trim="inbound.stream.xtls.settings.serverName" style="width: 250px"></a-input>
</a-form-item>
<a-form-item label="Alpn">
<a-checkbox-group v-model="inbound.stream.xtls.alpn" style="width:200px">
<a-checkbox v-for="key in ALPN_OPTION" :value="key">[[ key ]]</a-checkbox>
</a-checkbox-group>
</a-form-item>
<a-form-item label="Allow insecure">
<a-switch v-model="inbound.stream.xtls.settings.allowInsecure"></a-switch>
</a-form-item>
<template v-for="cert,index in inbound.stream.xtls.certs">
<a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="cert.useFile" button-style="solid">
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
</a-radio-group>
<a-button v-if="index === 0" type="primary" size="small" @click="inbound.stream.xtls.addCert()" style="margin-left: 10px">+</a-button>
<a-button v-if="inbound.stream.xtls.certs.length>1" type="primary" size="small" @click="inbound.stream.xtls.removeCert(index)" style="margin-left: 10px">-</a-button>
</a-form-item>
<template v-if="cert.useFile">
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
<a-input v-model.trim="cert.certFile" style="width:300px;"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
<a-input v-model.trim="cert.keyFile" style="width:300px;"></a-input>
</a-form-item>
<a-button type="primary" icon="import" @click="setDefaultCertXtls(index)">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
<!-- xtls settings -->
<template v-else-if="inbound.xtls">
<tr>
<td>
<span>SNI</span>
</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.xtls.server" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>
<span>Alpn</span>
</td>
<td>
<a-form-item>
<a-select mode="multiple" style="width: 250px" :dropdown-class-name="themeSwitcher.currentTheme"
v-model="inbound.stream.xtls.alpn">
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>
<span>Allow insecure</span>
</td>
<td>
<a-form-item>
<a-switch v-model="inbound.stream.xtls.settings.allowInsecure"></a-switch>
</a-form-item>
</td>
</tr>
<template v-for="cert,index in inbound.stream.xtls.certs">
<tr>
<td>{{ i18n "certificate" }}</td>
<td>
<a-form-item>
<a-radio-group v-model="cert.useFile" button-style="solid">
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath"
}}</a-radio-button>
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent"
}}</a-radio-button>
</a-radio-group>
<a-button v-if="index === 0" type="primary" size="small"
@click="inbound.stream.xtls.addCert()" style="margin-left: 10px">+</a-button>
<a-button v-if="inbound.stream.xtls.certs.length>1" type="primary" size="small"
@click="inbound.stream.xtls.removeCert(index)" style="margin-left: 10px">-</a-button>
</a-form-item>
</td>
</tr>
<template v-if="cert.useFile">
<tr>
<td>{{ i18n "pages.inbounds.publicKeyPath" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="cert.certFile" style="width:250px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.keyPath" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="cert.keyFile" style="width:250px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td></td>
<td>
<a-button type="primary" icon="import" @click="setDefaultCertData(index)">{{ i18n
"pages.inbounds.setDefaultCert" }}</a-button>
</td>
</tr>
</template>
<template v-else>
<tr>
<td>{{ i18n "pages.inbounds.publicKeyContent" }}</td>
<td>
<a-form-item>
<a-input type="textarea" :rows="3" style="width:250px;" v-model="cert.cert"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.keyContent" }}</td>
<td>
<a-form-item>
<a-input type="textarea" :rows="3" style="width:250px;" v-model="cert.key"></a-input>
</a-form-item>
</td>
</tr>
</template>
</template>
</template>
<template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="cert.cert"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="cert.key"></a-input>
</a-form-item>
</template>
</template>
</a-form>
<!-- reality settings -->
<a-form v-else-if="inbound.reality" layout="inline">
<a-form-item label="Show">
<a-switch v-model="inbound.stream.reality.show">
</a-switch>
</a-form-item>
<a-form-item label="xVer">
<a-input-number v-model="inbound.stream.reality.xver" :min="0" style="width: 60px"></a-input-number>
</a-form-item>
<a-form-item label="uTLS">
<a-select v-model="inbound.stream.reality.settings.fingerprint"
style="width: 135px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='{{ i18n "domainName" }}'>
<a-input v-model.trim="inbound.stream.reality.settings.serverName" style="width: 250px"></a-input>
</a-form-item>
<a-form-item label="Dest">
<a-input v-model.trim="inbound.stream.reality.dest" style="width: 300px"></a-input>
</a-form-item>
<a-form-item label="Server Names">
<a-input v-model.trim="inbound.stream.reality.serverNames" style="width: 300px"></a-input>
</a-form-item>
<a-form-item label="ShortIds">
<a-icon @click="inbound.stream.reality.shortIds = RandomUtil.randomShortId()" type="sync"> </a-icon>
<a-input v-model.trim="inbound.stream.reality.shortIds" style="width: 150px;"></a-input>
</a-form-item>
<br>
<a-form-item label="SpiderX">
<a-input v-model.trim="inbound.stream.reality.settings.spiderX" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item label="Private Key">
<a-input v-model.trim="inbound.stream.reality.privateKey" style="width: 300px"></a-input>
</a-form-item>
<a-form-item label="Public Key">
<a-input v-model.trim="inbound.stream.reality.settings.publicKey" style="width: 300px"></a-input>
</a-form-item>
<a-form-item>
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get New Key</a-button>
</a-form-item>
<!-- reality settings -->
<template v-else-if="inbound.reality">
<tr>
<td>
<span>Show</span>
</td>
<td>
<a-form-item>
<a-switch v-model="inbound.stream.reality.show"></a-switch>
</a-form-item>
</td>
</tr>
<tr>
<td>
<span>xVer</span>
</td>
<td>
<a-form-item>
<a-input-number v-model="inbound.stream.reality.xver" :min="0"
style="width: 60px"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>
<span>uTLS</span>
</td>
<td>
<a-form-item>
<a-select v-model="inbound.stream.reality.settings.fingerprint" style="width: 135px"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>
<span>Dest</span>
</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.reality.dest" style="width: 300px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>
<span>Server Names</span>
</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.reality.serverNames" style="width: 300px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>
<span>ShortIds</span> <a-icon @click="inbound.stream.reality.shortIds = RandomUtil.randomShortId()"
type="sync"> </a-icon>
</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.reality.shortIds" style="width: 150px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>
<span>SpiderX</span>
</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.reality.settings.spiderX" style="width: 150px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>
<span>Private Key</span>
</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.reality.privateKey" style="width: 300px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>
<span>Public Key</span>
</td>
<td>
<a-form-item>
<a-input v-model.trim="inbound.stream.reality.settings.publicKey"
style="width: 300px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td colspan="2">
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get New Key</a-button>
</td>
</tr>
</template>
</table>
</a-form>
{{end}}

View file

@ -2,34 +2,66 @@
<template slot="actions" slot-scope="text, client, index">
<a-tooltip>
<template slot="title">{{ i18n "qrCode" }}</template>
<a-icon style="font-size: 24px;" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record,index);"></a-icon>
<a-icon style="font-size: 24px;" class="normal-icon" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record.id,client);"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title">{{ i18n "pages.client.edit" }}</template>
<a-icon style="font-size: 24px;" type="edit" @click="openEditClient(record.id,client);"></a-icon>
<a-icon style="font-size: 24px;" class="normal-icon" type="edit" @click="openEditClient(record.id,client);"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title">{{ i18n "info" }}</template>
<a-icon style="font-size: 24px;" type="info-circle" @click="showInfo(record,index);"></a-icon>
<a-icon style="font-size: 24px;" class="normal-icon" type="info-circle" @click="showInfo(record.id,client);"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
<a-icon style="font-size: 24px;" type="retweet" @click="resetClientTraffic(client,record.id)" v-if="client.email.length > 0"></a-icon>
<a-popconfirm @confirm="resetClientTraffic(client,record.id,false)"
title='{{ i18n "pages.inbounds.resetTrafficContent"}}'
:overlay-class-name="themeSwitcher.currentTheme"
ok-text='{{ i18n "reset"}}'
cancel-text='{{ i18n "cancel"}}'>
<a-icon slot="icon" type="question-circle-o" :style="themeSwitcher.isDarkTheme ? 'color: #3c89e8' : 'color: blue'"></a-icon>
<a-icon style="font-size: 24px;" class="normal-icon" type="retweet" v-if="client.email.length > 0"></a-icon>
</a-popconfirm>
</a-tooltip>
<a-tooltip>
<template slot="title"><span style="color: #FF4D4F"> {{ i18n "delete"}}</span></template>
<a-icon style="font-size: 24px;" type="delete" v-if="isRemovable(record.id)" @click="delClient(record.id,client)"></a-icon>
<a-popconfirm @confirm="delClient(record.id,client,false)"
title='{{ i18n "pages.inbounds.deleteClientContent"}}'
:overlay-class-name="themeSwitcher.currentTheme"
ok-text='{{ i18n "delete"}}'
ok-type="danger"
cancel-text='{{ i18n "cancel"}}'>
<a-icon slot="icon" type="question-circle-o" style="color: #e04141"></a-icon>
<a-icon style="font-size: 24px" class="delete-icon" type="delete" v-if="isRemovable(record.id)"></a-icon>
</a-popconfirm>
</a-tooltip>
</template>
<template slot="enable" slot-scope="text, client, index">
<a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch>
</template>
<template slot="online" slot-scope="text, client, index">
<template v-if="isClientOnline(client.email)">
<a-tag color="green">{{ i18n "online" }}</a-tag>
</template>
<template v-else>
<a-tag>{{ i18n "offline" }}</a-tag>
</template>
</template>
<template slot="client" slot-scope="text, client">
<a-tooltip>
<template slot="title">
<template v-if="!isClientEnabled(record, client.email)">{{ i18n "depleted" }}</template>
<template v-else-if="!client.enable">{{ i18n "disabled" }}</template>
<template v-else-if="isClientOnline(client.email)">{{ i18n "online" }}</template>
</template>
<a-badge :class="isClientOnline(client.email)? 'online-animation' : ''" :color="client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'">
</a-badge>
</a-tooltip>
[[ client.email ]]
<a-tag v-if="!isClientEnabled(record, client.email)" color="red">{{ i18n "depleted" }}</a-tag>
</template>
<template slot="traffic" slot-scope="text, client">
<a-popover :overlay-class-name="themeSwitcher.darkClass">
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content" v-if="client.email">
<table cellpadding="2" width="100%">
<tr>
@ -38,28 +70,200 @@
</tr>
<tr v-if="client.totalGB > 0">
<td>{{ i18n "remained" }}</td>
<td>[[ sizeFormat(client.totalGB - getUpStats(record, client.email) - getDownStats(record, client.email)) ]]</td>
<td>[[ sizeFormat(getRemStats(record, client.email)) ]]</td>
</tr>
</table>
</template>
<a-tag :color="statsColor(record, client.email)">
[[ sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]] /
<template v-if="client.totalGB > 0">[[client._totalGB]]GB</template>
<template v-else>
<svg style="fill: currentColor; height: 10px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"/></svg>
</template>
</a-tag>
<table>
<tr>
<td width="80px" style="margin:0; text-align: right;font-size: 1em;">
[[ sizeFormat(getSumStats(record, client.email)) ]]
</td>
<td width="120px" v-if="!client.enable">
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'"
:show-info="false"
:percent="statsProgress(record, client.email)"/>
</td>
<td width="120px" v-else-if="client.totalGB > 0">
<a-progress :stroke-color="clientStatsColor(record, client.email)"
:show-info="false"
:status="isClientOnline(client.email)? 'active' : isClientEnabled(record, client.email)? 'exception' : ''"
:percent="statsProgress(record, client.email)"/>
</td>
<td width="120px" v-else class="infinite-bar">
<a-progress
:show-info="false"
:status="isClientOnline(client.email)? 'active' : ''"
:percent="100"></a-progress>
</td>
<td width="60px">
<template v-if="client.totalGB > 0">[[ client._totalGB + "GB" ]]</template>
<span v-else style="font-weight: 100;font-size: 14pt;">&infin;</span>
</td>
</tr>
</table>
</a-popover>
</template>
<template slot="expiryTime" slot-scope="text, client, index">
<template v-if="client.expiryTime > 0">
<a-tag :color="usageColor(new Date().getTime(), app.expireDiff, client.expiryTime)">
[[ DateUtil.formatMillis(client._expiryTime) ]]
</a-tag>
<template v-if="client.expiryTime !=0 && client.reset >0">
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
<span v-else>[[ DateUtil.formatMillis(client._expiryTime) ]]</span>
</template>
<table>
<tr>
<td width="80px" style="margin:0; text-align: right;font-size: 1em;">
[[ remainedDays(client.expiryTime) ]]
</td>
<td width="120px" class="infinite-bar">
<a-progress :show-info="false"
:status="isClientOnline(client.email)? 'active' : isClientEnabled(record, client.email)? 'exception' : ''"
:percent="expireProgress(client.expiryTime, client.reset)"/>
</td>
<td width="60px">[[ client.reset + "d" ]]</td>
</tr>
</table>
</a-popover>
</template>
<a-tag v-else-if="client.expiryTime < 0" color="cyan">
[[ client._expiryTime ]] {{ i18n "pages.client.days" }}
</a-tag>
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
<template v-else>
<a-popover v-if="client.expiryTime != 0" :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
<span v-else>[[ DateUtil.formatMillis(client._expiryTime) ]]</span>
</template>
<a-tag style="min-width: 50px; border: none;" :color="userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)">
[[ remainedDays(client.expiryTime) ]]
</a-tag>
</a-popover>
<a-tag v-else :color="userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)" style="border: none;" class="infinite-tag">&infin;</a-tag>
</template>
</template>
<template slot="actionMenu" slot-scope="text, client, index">
<a-dropdown :trigger="['click']">
<a-icon @click="e => e.preventDefault()" type="ellipsis" style="font-size: 20px;"></a-icon>
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
<a-menu-item v-if="record.hasLink()" @click="showQrcode(record.id,client);">
<a-icon style="font-size: 14px;" type="qrcode"></a-icon>
{{ i18n "qrCode" }}
</a-menu-item>
<a-menu-item @click="openEditClient(record.id,client);">
<a-icon style="font-size: 14px;" type="edit"></a-icon>
{{ i18n "pages.client.edit" }}
</a-menu-item>
<a-menu-item @click="showInfo(record.id,client);">
<a-icon style="font-size: 14px;" type="info-circle"></a-icon>
{{ i18n "info" }}
</a-menu-item>
<a-menu-item @click="resetClientTraffic(client,record.id)" v-if="client.email.length > 0">
<a-icon style="font-size: 14px;" type="retweet"></a-icon>
{{ i18n "pages.inbounds.resetTraffic" }}
</a-menu-item>
<a-menu-item v-if="isRemovable(record.id)" @click="delClient(record.id,client)">
<a-icon style="font-size: 14px;" type="delete"></a-icon>
<span style="color: #FF4D4F"> {{ i18n "delete"}}</span>
</a-menu-item>
<a-menu-item>
<a-switch v-model="client.enable" size="small" @change="switchEnableClient(record.id,client)">
</a-switch>
{{ i18n "enable"}}
</a-menu-item>
</a-menu>
</a-dropdown>
</template>
<template slot="info" slot-scope="text, client, index">
<a-popover placement="bottomRight" :overlay-class-name="themeSwitcher.currentTheme" trigger="click">
<template slot="content">
<table>
<tr>
<td colspan="3" style="text-align: center;">{{ i18n "pages.inbounds.traffic" }}</td>
</tr>
<tr>
<td width="80px" style="margin:0; text-align: right;font-size: 1em;">
[[ sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]]
</td>
<td width="120px" v-if="!client.enable">
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'"
:show-info="false"
:percent="statsProgress(record, client.email)"/>
</td>
<td width="120px" v-else-if="client.totalGB > 0">
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content" v-if="client.email">
<table cellpadding="2" width="100%">
<tr>
<td>↑[[ sizeFormat(getUpStats(record, client.email)) ]]</td>
<td>↓[[ sizeFormat(getDownStats(record, client.email)) ]]</td>
</tr>
<tr>
<td>{{ i18n "remained" }}</td>
<td>[[ sizeFormat(getRemStats(record, client.email)) ]]</td>
</tr>
</table>
</template>
<a-progress :stroke-color="clientStatsColor(record, client.email)"
:show-info="false"
:status="isClientOnline(client.email)? 'active' : isClientEnabled(record, client.email)? 'exception' : ''"
:percent="statsProgress(record, client.email)"/>
</a-popover>
</td>
<td width="120px" v-else class="infinite-bar">
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? '#2c1e32':'#F2EAF1'"
:show-info="false"
:status="isClientOnline(client.email)? 'active' : ''"
:percent="100"></a-progress>
</td>
<td width="80px">
<template v-if="client.totalGB > 0">[[ client._totalGB + "GB" ]]</template>
<span v-else style="font-weight: 100;font-size: 14pt;">&infin;</span>
</td>
</tr>
<tr>
<td colspan="3" style="text-align: center;">
<a-divider style="margin: 0; border-collapse: separate;"></a-divider>
{{ i18n "pages.inbounds.expireDate" }}
</td>
</tr>
<tr>
<template v-if="client.expiryTime !=0 && client.reset >0">
<td width="80px" style="margin:0; text-align: right;font-size: 1em;">
[[ remainedDays(client.expiryTime) ]]
</td>
<td width="120px" class="infinite-bar">
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
<span v-else>[[ DateUtil.formatMillis(client._expiryTime) ]]</span>
</template>
<a-progress :show-info="false"
:status="isClientOnline(client.email)? 'active' : isClientEnabled(record, client.email)? 'exception' : ''"
:percent="expireProgress(client.expiryTime, client.reset)"/>
</a-popover>
</td>
<td width="60px">[[ client.reset + "d" ]]</td>
</template>
<template v-else>
<td colspan="3" style="text-align: center;">
<a-popover v-if="client.expiryTime != 0" :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
<span v-else>[[ DateUtil.formatMillis(client._expiryTime) ]]</span>
</template>
<a-tag style="min-width: 50px; border: none;"
:color="userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)">
[[ remainedDays(client.expiryTime) ]]
</a-tag>
</a-popover>
<a-tag v-else :color="client.enable ? 'purple' : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'" class="infinite-tag">&infin;</a-tag>
</template>
</td>
</tr>
</table>
</template>
<a-badge>
<a-icon v-if="!client.enable" slot="count" type="pause-circle" :style="'color: ' + themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-icon>
<a-button shape="round" size="small" style="font-size: 14px; padding: 0 10px;"><a-icon type="solution"></a-icon></a-button>
</a-badge>
</a-popover>
</template>
{{end}}

View file

@ -3,16 +3,16 @@
v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}'
:closable="true"
:mask-closable="true"
:class="themeSwitcher.darkCardClass"
:class="themeSwitcher.currentTheme"
:footer="null"
width="600px"
>
<table style="margin-bottom: 10px; width: 100%;">
<tr><td>
<table>
<tr><td>{{ i18n "protocol" }}</td><td><a-tag color="green">[[ dbInbound.protocol ]]</a-tag></td></tr>
<tr><td>{{ i18n "pages.inbounds.address" }}</td><td><a-tag color="blue">[[ dbInbound.address ]]</a-tag></td></tr>
<tr><td>{{ i18n "pages.inbounds.port" }}</td><td><a-tag color="green">[[ dbInbound.port ]]</a-tag></td></tr>
<tr><td>{{ i18n "protocol" }}</td><td><a-tag color="purple">[[ dbInbound.protocol ]]</a-tag></td></tr>
<tr><td>{{ i18n "pages.inbounds.address" }}</td><td><a-tag>[[ dbInbound.address ]]</a-tag></td></tr>
<tr><td>{{ i18n "pages.inbounds.port" }}</td><td><a-tag>[[ dbInbound.port ]]</a-tag></td></tr>
</table>
</td>
<td v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
@ -21,44 +21,45 @@
<td>{{ i18n "transmission" }}</td><td><a-tag color="green">[[ inbound.network ]]</a-tag></td>
</tr>
<template v-if="inbound.isTcp || inbound.isWs || inbound.isH2">
<tr v-if="inbound.host"><td>{{ i18n "host" }}</td><td><a-tag color="green">[[ inbound.host ]]</a-tag></td></tr>
<tr v-else><td>{{ i18n "host" }}</td><td><a-tag color="orange">{{ i18n "none" }}</a-tag></td></tr>
<tr v-if="inbound.path"><td>{{ i18n "path" }}</td><td><a-tag color="green">[[ inbound.path ]]</a-tag></td></tr>
<tr v-else><td>{{ i18n "path" }}</td><td><a-tag color="orange">{{ i18n "none" }}</a-tag></td></tr>
<tr>
<td>{{ i18n "host" }}</td>
<td v-if="inbound.host"><a-tag>[[ inbound.host ]]</a-tag></td>
<td v-else><a-tag color="orange">{{ i18n "none" }}</a-tag></td></tr>
</tr>
<tr>
<td>{{ i18n "path" }}</td>
<td v-if="inbound.path"><a-tag>[[ inbound.path ]]</a-tag></td>
<td v-else><a-tag color="orange">{{ i18n "none" }}</a-tag></td>
</tr>
</template>
<template v-if="inbound.isQuic">
<tr><td>quic {{ i18n "encryption" }}</td><td><a-tag color="green">[[ inbound.quicSecurity ]]</a-tag></td></tr>
<tr><td>quic {{ i18n "password" }}</td><td><a-tag color="green">[[ inbound.quicKey ]]</a-tag></td></tr>
<tr><td>quic {{ i18n "camouflage" }}</td><td><a-tag color="green">[[ inbound.quicType ]]</a-tag></td></tr>
<tr><td>quic {{ i18n "encryption" }}</td><td><a-tag>[[ inbound.quicSecurity ]]</a-tag></td></tr>
<tr><td>quic {{ i18n "password" }}</td><td><a-tag>[[ inbound.quicKey ]]</a-tag></td></tr>
<tr><td>quic {{ i18n "camouflage" }}</td><td><a-tag>[[ inbound.quicType ]]</a-tag></td></tr>
</template>
<template v-if="inbound.isKcp">
<tr><td>kcp {{ i18n "encryption" }}</td><td><a-tag color="green">[[ inbound.kcpType ]]</a-tag></td></tr>
<tr><td>kcp {{ i18n "password" }}</td><td><a-tag color="green">[[ inbound.kcpSeed ]]</a-tag></td></tr>
<tr><td>kcp {{ i18n "encryption" }}</td><td><a-tag>[[ inbound.kcpType ]]</a-tag></td></tr>
<tr><td>kcp {{ i18n "password" }}</td><td><a-tag>[[ inbound.kcpSeed ]]</a-tag></td></tr>
</template>
<template v-if="inbound.isGrpc">
<tr><td>grpc serviceName</td><td><a-tag color="green">[[ inbound.serviceName ]]</a-tag></td></tr>
<tr><td>grpc multiMode</td><td><a-tag color="green">[[ inbound.stream.grpc.multiMode ]]</a-tag></td></tr>
<tr><td>grpc serviceName</td><td><a-tag>[[ inbound.serviceName ]]</a-tag></td></tr>
<tr><td>grpc multiMode</td><td><a-tag>[[ inbound.stream.grpc.multiMode ]]</a-tag></td></tr>
</template>
</table>
</td></tr>
<tr colspan="2" v-if="dbInbound.hasLink()">
<td v-if="inbound.tls">
tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
<td>
{{ i18n "security" }}
<a-tag :color="inbound.stream.security == 'none' ? 'red' : 'blue'">[[ inbound.stream.security ]]</a-tag>
<br />
<template v-if="inbound.stream.security != 'none'">
{{ i18n "domainName" }}
<a-tag :color="inbound.serverName ? 'blue' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
</template>
</td>
<td v-else-if="inbound.xtls">
xtls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
xtls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
</td>
<td v-else-if="inbound.reality">
reality: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
reality Destination: <a-tag :color="inbound.stream.reality.dest ? 'green' : 'orange'">[[ inbound.stream.reality.dest ]]</a-tag>
</td>
<td v-else>tls: <a-tag color="red">{{ i18n "disabled" }}</a-tag></td>
</tr>
</table>
<table v-if="dbInbound.isSS" style="margin-bottom: 10px; width: 100%;">
@ -67,7 +68,7 @@
<td><a-tag color="green">[[ inbound.settings.method ]]</a-tag></td>
</tr><tr v-if="inbound.isSS2022">
<td>{{ i18n "password" }}</td>
<td><a-tag color="blue">[[ inbound.settings.password ]]</a-tag></td>
<td><a-tag>[[ inbound.settings.password ]]</a-tag></td>
</tr><tr>
<td>{{ i18n "pages.inbounds.network" }}</td>
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
@ -82,21 +83,21 @@
</tr>
<tr v-if="infoModal.clientSettings.id">
<td>ID</td>
<td><a-tag color="green">[[ infoModal.clientSettings.id ]]</a-tag></td>
<td><a-tag>[[ infoModal.clientSettings.id ]]</a-tag></td>
</tr>
<tr v-if="infoModal.inbound.canEnableTlsFlow()">
<td>Flow</td>
<td><a-tag color="green">[[ infoModal.clientSettings.flow ]]</a-tag></td>
<td><a-tag>[[ infoModal.clientSettings.flow ]]</a-tag></td>
</tr>
<tr v-if="infoModal.clientSettings.password">
<td>Password</td>
<td><a-tag color="green">[[ infoModal.clientSettings.password ]]</a-tag></td>
<td><a-tag>[[ infoModal.clientSettings.password ]]</a-tag></td>
</tr>
<tr>
<td>{{ i18n "status" }}</td>
<td>
<a-tag v-if="isEnable" color="blue">{{ i18n "enabled" }}</a-tag>
<a-tag v-else color="red">{{ i18n "disabled" }}</a-tag>
<a-tag v-if="isEnable" color="green">{{ i18n "enabled" }}</a-tag>
<a-tag v-else>{{ i18n "disabled" }}</a-tag>
<a-tag v-if="!isActive" color="red">{{ i18n "depleted" }}</a-tag>
</td>
</tr>
@ -104,11 +105,11 @@
<td>{{ i18n "usage" }}</td>
<td>
<a-tag color="green">[[ sizeFormat(infoModal.clientStats.up + infoModal.clientStats.down) ]]</a-tag>
<a-tag color="blue">↑ [[ sizeFormat(infoModal.clientStats.up) ]] / [[ sizeFormat(infoModal.clientStats.down) ]] ↓</a-tag>
<a-tag>↑ [[ sizeFormat(infoModal.clientStats.up) ]] / [[ sizeFormat(infoModal.clientStats.down) ]] ↓</a-tag>
</td>
</tr>
</table>
<table style="margin-bottom: 10px; width: 100%;">
<table style="margin-bottom: 10px; width: 100%; text-align: center;">
<tr>
<th>{{ i18n "remained" }}</th>
<th>{{ i18n "pages.inbounds.totalFlow" }}</th>
@ -124,7 +125,7 @@
<a-tag v-if="infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)">
[[ sizeFormat(infoModal.clientSettings.totalGB) ]]
</a-tag>
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
<a-tag v-else color="purple" class="infinite-tag">&infin;</a-tag>
</td>
<td>
<template v-if="infoModal.clientSettings.expiryTime > 0">
@ -132,8 +133,8 @@
[[ DateUtil.formatMillis(infoModal.clientSettings.expiryTime) ]]
</a-tag>
</template>
<a-tag v-else-if="infoModal.clientSettings.expiryTime < 0" color="cyan">[[ infoModal.clientSettings.expiryTime / -86400000 ]] {{ i18n "pages.client.days" }}</a-tag>
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
<a-tag v-else-if="infoModal.clientSettings.expiryTime < 0" color="green">[[ infoModal.clientSettings.expiryTime / -86400000 ]] {{ i18n "pages.client.days" }}</a-tag>
<a-tag v-else color="purple" class="infinite-tag">&infin;</a-tag>
</td>
</tr>
</table>
@ -166,7 +167,7 @@
<template v-if="dbInbound.hasLink()">
<a-divider>URL</a-divider>
<a-row v-for="(link,index) in infoModal.links">
<a-col :span="22"><a-tag color="cyan">[[ link.remark ]]</a-tag><br />[[ link.link ]]</a-col>
<a-col :span="22"><a-tag color="green">[[ link.remark ]]</a-tag><br />[[ link.link ]]</a-col>
<a-col :span="2" style="text-align: right;">
<a-tooltip title='{{ i18n "copy" }}'>
<button class="ant-btn ant-btn-primary" :id="'copy-url-link-'+index" @click="copyToClipboard('copy-url-link-'+index, link.link)">
@ -181,7 +182,7 @@
<template v-if="dbInbound.isSS && !inbound.isSSMultiUser">
<a-divider>URL</a-divider>
<a-row v-for="(link,index) in infoModal.links">
<a-col :span="22"><a-tag color="cyan">[[ link.remark ]]</a-tag><br />[[ link.link ]]</a-col>
<a-col :span="22"><a-tag color="green">[[ link.remark ]]</a-tag><br />[[ link.link ]]</a-col>
<a-col :span="2" style="text-align: right;">
<a-tooltip title='{{ i18n "copy" }}'>
<button class="ant-btn ant-btn-primary" :id="'copy-url-link-'+index" @click="copyToClipboard('copy-url-link-'+index, link.link)">
@ -199,9 +200,9 @@
<th>FollowRedirect</th>
</tr><tr>
<td><a-tag color="green">[[ inbound.settings.address ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.port ]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.port ]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.followRedirect ]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.followRedirect ]]</a-tag></td>
</tr>
</table>
<table v-if="dbInbound.isSocks" style="margin-bottom: 10px; width: 100%;">
@ -212,7 +213,7 @@
</tr>
<tr>
<td><a-tag color="green">[[ inbound.settings.auth ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.udp]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.udp]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.ip ]]</a-tag></td>
</tr>
<template v-if="inbound.settings.auth == 'password'">
@ -221,8 +222,8 @@
<td>{{ i18n "username" }}</td>
<td>{{ i18n "password" }}</td>
</tr><tr v-for="account,index in inbound.settings.accounts">
<td><a-tag color="green">[[ index ]]</a-tag></td>
<td><a-tag color="blue">[[ account.user ]]</a-tag></td>
<td>[[ index ]]</td>
<td><a-tag color="green">[[ account.user ]]</a-tag></td>
<td><a-tag color="green">[[ account.pass ]]</a-tag></td>
</tr>
</template>
@ -233,8 +234,8 @@
<th>{{ i18n "username" }}</th>
<th>{{ i18n "password" }}</th>
</tr><tr v-for="account,index in inbound.settings.accounts">
<td><a-tag color="green">[[ index ]]</a-tag></td>
<td><a-tag color="blue">[[ account.user ]]</a-tag></td>
<td>[[ index ]]</td>
<td><a-tag color="green">[[ account.user ]]</a-tag></td>
<td><a-tag color="green">[[ account.pass ]]</a-tag></td>
</tr>
</table>
@ -260,27 +261,10 @@
this.index = index;
this.inbound = dbInbound.toInbound();
this.dbInbound = new DBInbound(dbInbound);
this.settings = JSON.parse(this.inbound.settings);
this.clientSettings = this.settings.clients ? Object.values(this.settings.clients)[index] : null;
this.isExpired = this.inbound.isExpiry(index);
this.clientStats = this.settings.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
remark = [this.dbInbound.remark, ( this.clientSettings ? this.clientSettings.email : '')].filter(Boolean).join('-');
address = this.dbInbound.address;
this.links = [];
if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) {
this.inbound.stream.tls.settings.domains.forEach((domain) => {
remarkText = [remark, domain.remark].filter(Boolean).join('-');
this.links.push({
remark: remarkText,
link: this.inbound.genLink(domain.domain, remarkText, index)
});
});
} else {
this.links.push({
remark: remark,
link: this.inbound.genLink(address, remark, index)
});
}
this.clientSettings = this.inbound.clients ? this.inbound.clients[index] : null;
this.isExpired = this.inbound.clients ? this.inbound.isExpiry(index): this.dbInbound.isExpiry;
this.clientStats = this.inbound.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
this.links = this.inbound.genAllLinks(this.dbInbound.remark, app.remarkModel, this.clientSettings);
if (this.clientSettings) {
if (this.clientSettings.subId) {
this.subLink = this.genSubLink(this.clientSettings.subId);
@ -295,8 +279,7 @@
infoModal.visible = false;
},
genSubLink(subID) {
const { domain: host, port, tls: isTLS, path: base } = app.subSettings;
return buildURL({ host, port, isTLS, base, path: subID+'?name='+remark });
return app.subSettings.subURI+subID+'?name='+subID;
}
};
@ -315,7 +298,7 @@
if (infoModal.clientStats) {
return infoModal.clientStats.enable;
}
return infoModal.dbInbound.isEnable;
return true;
},
get isEnable() {
if (infoModal.clientSettings) {

View file

@ -1,7 +1,7 @@
{{define "inboundModal"}}
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" @ok="inModal.ok"
:confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
:class="themeSwitcher.darkCardClass"
:class="themeSwitcher.currentTheme"
:ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
{{template "form/inbound"}}
</a-modal>
@ -43,15 +43,6 @@
loading(loading) {
inModal.confirmLoading = loading;
},
getClients(protocol, clientSettings) {
switch (protocol) {
case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans;
case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
default: return null;
}
},
};
new Vue({
@ -70,7 +61,7 @@
return inModal.isEdit;
},
get client() {
return inModal.getClients(this.inbound.protocol, this.inbound.settings)[0];
return inModal.inbound.clients[0];
},
get delayedExpireDays() {
return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
@ -78,22 +69,25 @@
set delayedExpireDays(days) {
this.client.expiryTime = -86400000 * days;
},
get multiDomain() {
return this.inbound.stream.tls.settings.domains.length > 0;
get externalProxy() {
return this.inbound.stream.externalProxy.length > 0;
},
set multiDomain(value) {
set externalProxy(value) {
if (value) {
inModal.inbound.stream.tls.server = "";
inModal.inbound.stream.tls.settings.domains = [{ remark: "", domain: window.location.hostname }];
inModal.inbound.stream.externalProxy = [{
forceTls: "same",
dest: window.location.hostname,
port: inModal.inbound.port,
remark: ""
}];
} else {
inModal.inbound.stream.tls.server = "";
inModal.inbound.stream.tls.settings.domains = [];
inModal.inbound.stream.externalProxy = [];
}
}
},
methods: {
streamNetworkChange() {
if (!inModal.inbound.canSetTls()) {
if (!inModal.inbound.canEnableTls()) {
this.inModal.inbound.stream.security = 'none';
}
if (!inModal.inbound.canEnableReality()) {

View file

@ -8,27 +8,56 @@
}
}
@media (max-width: 768px) {
.ant-card-body {
padding: .5rem;
}
}
.ant-col-sm-24 {
margin-top: 10px;
margin: 0.5rem -2rem 0.5rem 2rem;
}
tr.hideExpandIcon .ant-table-row-expand-icon {
display: none;
}
.infinite-tag {
padding: 0 5px;
border-radius: 2rem;
min-width: 50px;
}
.infinite-bar .ant-progress-inner .ant-progress-bg {
background-color: #F2EAF1;
border: #D5BED2 solid 1px;
}
.dark .infinite-bar .ant-progress-inner .ant-progress-bg {
background-color: #3c1536;
border: #7a316f solid 1px;
}
.ant-collapse {
margin: 5px 0;
}
.online-animation .ant-badge-status-dot {
animation: 1.2s ease infinite normal none running onlineAnimation;
}
@keyframes onlineAnimation {
0%, 50%, 100% { transform: scale(1); opacity: 1; }
10% { transform: scale(1.5); opacity: .2; }
}
</style>
<body>
<a-layout id="app" v-cloak>
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
{{ template "commonSider" . }}
<a-layout id="content-layout" :style="themeSwitcher.bgStyle">
<a-layout id="content-layout">
<a-layout-content>
<a-spin :spinning="spinning" :delay="500" tip="loading">
<a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'>
<transition name="list" appear>
<a-tag v-if="false" color="red" style="margin-bottom: 10px">
Please go to the panel settings as soon as possible to modify the username and password, otherwise there may be a risk of leaking account information
</a-tag>
</transition>
<transition name="list" appear>
<a-card hoverable style="margin-bottom: 20px;" :class="themeSwitcher.darkCardClass">
<a-card hoverable>
<a-row>
<a-col :xs="24" :sm="24" :lg="12">
{{ i18n "pages.inbounds.totalDownUp" }}:
@ -43,39 +72,59 @@
<a-tag color="green">[[ dbInbounds.length ]]</a-tag>
</a-col>
<a-col :xs="24" :sm="24" :lg="12">
<template>
<div>
<a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200">
</a-back-top>
{{ i18n "clients" }}:
<a-tag color="green">[[ total.clients ]]</a-tag>
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.darkClass">
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in total.deactive">[[ clientEmail ]]</p>
</template>
<a-tag v-if="total.deactive.length">[[ total.deactive.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.darkClass">
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in total.depleted">[[ clientEmail ]]</p>
</template>
<a-tag color="red" v-if="total.depleted.length">[[ total.depleted.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.darkClass">
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in total.expiring">[[ clientEmail ]]</p>
</template>
<a-tag color="orange" v-if="total.expiring.length">[[ total.expiring.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in onlineClients">[[ clientEmail ]]</p>
</template>
<a-tag color="blue" v-if="onlineClients.length">[[ onlineClients.length ]]</a-tag>
</a-popover>
</div>
</template>
</a-col>
</a-row>
</a-card>
</transition>
<transition name="list" appear>
<a-card hoverable :class="themeSwitcher.darkCardClass">
<a-card hoverable>
<div slot="title">
<a-row>
<a-col :xs="24" :sm="24" :lg="12">
<a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button>
<a-col :xs="12" :sm="12" :lg="12">
<a-button type="primary" icon="plus" @click="openAddInbound">
<template v-if="!isMobile">{{ i18n "pages.inbounds.addInbound" }}</template>
</a-button>
<a-dropdown :trigger="['click']">
<a-button type="primary" icon="menu">{{ i18n "pages.inbounds.generalActions" }}</a-button>
<a-button type="primary" icon="menu">
<template v-if="!isMobile">{{ i18n "pages.inbounds.generalActions" }}</template>
</a-button>
<a-menu slot="overlay" @click="a => generalActions(a)" :theme="themeSwitcher.currentTheme">
<a-menu-item key="import">
<a-icon type="import"></a-icon>
{{ i18n "pages.inbounds.importInbound" }}
</a-menu-item>
<a-menu-item key="export">
<a-icon type="export"></a-icon>
{{ i18n "pages.inbounds.export" }}
@ -95,12 +144,12 @@
</a-menu>
</a-dropdown>
</a-col>
<a-col :xs="24" :sm="24" :lg="12" style="text-align: right;">
<a-col :xs="12" :sm="12" :lg="12" style="text-align: right;">
<a-select v-model="refreshInterval"
style="width: 65px;"
v-if="isRefreshEnabled"
@change="changeRefreshInterval"
:dropdown-class-name="themeSwitcher.darkCardClass">
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in [5,10,30,60]" :value="key*1000">[[ key ]]s</a-select-option>
</a-select>
<a-icon type="sync" :spin="refreshing" @click="manualRefresh" style="margin: 0 5px;"></a-icon>
@ -108,31 +157,36 @@
</a-col>
</a-row>
</div>
<a-switch v-model="enableFilter"
checked-children='{{ i18n "search" }}' un-checked-children='{{ i18n "filter" }}'
@change="toggleFilter" style="margin-right: 10px;">
</a-switch>
<a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px"></a-input>
<a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid">
<a-radio-button value="">{{ i18n "none" }}</a-radio-button>
<a-radio-button value="deactive">{{ i18n "disabled" }}</a-radio-button>
<a-radio-button value="depleted">{{ i18n "depleted" }}</a-radio-button>
<a-radio-button value="expiring">{{ i18n "depletingSoon" }}</a-radio-button>
</a-radio-group>
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
<div style="display: flex; align-items: center; justify-content: flex-start;">
<a-switch v-model="enableFilter"
style="margin-right: .5rem;"
@change="toggleFilter">
<a-icon slot="checkedChildren" type="search"></a-icon>
<a-icon slot="unCheckedChildren" type="filter"></a-icon>
</a-switch>
<a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px" :size="isMobile ? 'small' : ''"></a-input>
<a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid" :size="isMobile ? 'small' : ''">
<a-radio-button value="">{{ i18n "none" }}</a-radio-button>
<a-radio-button value="deactive">{{ i18n "disabled" }}</a-radio-button>
<a-radio-button value="depleted">{{ i18n "depleted" }}</a-radio-button>
<a-radio-button value="expiring">{{ i18n "depletingSoon" }}</a-radio-button>
<a-radio-button value="online">{{ i18n "online" }}</a-radio-button>
</a-radio-group>
</div>
<a-back-top></a-back-top>
<a-table :columns="isMobile ? mobileColums : columns" :row-key="dbInbound => dbInbound.id"
:data-source="searchedInbounds"
:loading="spinning" :scroll="{ x: 1200 }"
:pagination="false"
:scroll="isMobile ? {} : { x: 1000 }"
:pagination=pagination(searchedInbounds)
:expand-icon-as-cell="false"
:expand-row-by-click="false"
:expand-icon-column-index="0"
:row-class-name="dbInbound => (dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess || (dbInbound.isSS && dbInbound.toInbound().isSSMultiUser) ? '' : 'hideExpandIcon')"
style="margin-top: 20px"
@change="() => getDBInbounds()">
:indent-size="0"
:row-class-name="dbInbound => (dbInbound.isMultiUser() ? '' : 'hideExpandIcon')"
style="margin-top: 10px">
<template slot="action" slot-scope="text, dbInbound">
<a-icon type="edit" style="font-size: 22px" @click="openEditInbound(dbInbound.id);"></a-icon>
<a-dropdown :trigger="['click']">
<a @click="e => e.preventDefault()">{{ i18n "pages.inbounds.operate" }}</a>
<a-icon @click="e => e.preventDefault()" type="more" style="font-size: 20px; text-decoration: solid;"></a-icon>
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="themeSwitcher.currentTheme">
<a-menu-item key="edit">
<a-icon type="edit"></a-icon>
@ -142,7 +196,7 @@
<a-icon type="qrcode"></a-icon>
{{ i18n "qrCode" }}
</a-menu-item>
<template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess || (dbInbound.isSS && dbInbound.toInbound().isSSMultiUser)">
<template v-if="dbInbound.isMultiUser()">
<a-menu-item key="addClient">
<a-icon type="user-add"></a-icon>
{{ i18n "pages.client.add"}}
@ -170,6 +224,10 @@
{{ i18n "info"}}
</a-menu-item>
</template>
<a-menu-item key="clipboard">
<a-icon type="copy"></a-icon>
{{ i18n "pages.inbounds.copyToClipboard" }}
</a-menu-item>
<a-menu-item key="resetTraffic">
<a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }}
</a-menu-item>
@ -181,43 +239,53 @@
<a-icon type="delete"></a-icon> {{ i18n "delete"}}
</span>
</a-menu-item>
<a-menu-item v-if="isMobile">
<a-switch size="small" v-model="dbInbound.enable" @change="switchEnable(dbInbound.id)"></a-switch>
{{ i18n "pages.inbounds.enable" }}
</a-menu-item>
</a-menu>
</a-dropdown>
</template>
<template slot="protocol" slot-scope="text, dbInbound">
<a-tag style="margin:0;" color="blue">[[ dbInbound.protocol ]]</a-tag>
<a-tag style="margin:0;" color="purple">[[ dbInbound.protocol ]]</a-tag>
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
<a-tag style="margin:0;" color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="cyan">TLS</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isXtls" color="cyan">XTLS</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="cyan">Reality</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="blue">TLS</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isXtls" color="blue">XTLS</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="blue">Reality</a-tag>
</template>
</template>
<template slot="clients" slot-scope="text, dbInbound">
<template v-if="clientCount[dbInbound.id]">
<a-tag style="margin:0;" color="green">[[ clientCount[dbInbound.id].clients ]]</a-tag>
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.darkClass">
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p>
</template>
<a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.darkClass">
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p>
</template>
<a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.darkClass">
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p>
</template>
<a-tag style="margin:0; padding: 0 2px;" color="orange" v-if="clientCount[dbInbound.id].expiring.length">[[ clientCount[dbInbound.id].expiring.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].online">[[ clientEmail ]]</p>
</template>
<a-tag style="margin:0; padding: 0 2px;" color="blue" v-if="clientCount[dbInbound.id].online.length">[[ clientCount[dbInbound.id].online.length ]]</a-tag>
</a-popover>
</template>
</template>
<template slot="traffic" slot-scope="text, dbInbound">
<a-popover :overlay-class-name="themeSwitcher.darkClass">
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<table cellpadding="2" width="100%">
<tr>
@ -230,7 +298,7 @@
</tr>
</table>
</template>
<a-tag :color="dbInbound.total == 0 ? 'green' : dbInbound.up + dbInbound.down < dbInbound.total ? 'cyan' : 'red'">
<a-tag :color="usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)">
[[ sizeFormat(dbInbound.up + dbInbound.down) ]] /
<template v-if="dbInbound.total > 0">
[[ sizeFormat(dbInbound.total) ]]
@ -245,35 +313,117 @@
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id)"></a-switch>
</template>
<template slot="expiryTime" slot-scope="text, dbInbound">
<template v-if="dbInbound.expiryTime > 0">
<a-tag v-if="dbInbound.isExpiry" color="red">
<a-popover v-if="dbInbound.expiryTime > 0" :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
[[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
</template>
<a-tag style="min-width: 50px;" :color="usageColor(new Date().getTime(), app.expireDiff, dbInbound._expiryTime)">
[[ remainedDays(dbInbound._expiryTime) ]]
</a-tag>
<a-tag v-else color="blue">
[[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
</a-tag>
</template>
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
</a-popover>
<a-tag v-else color="purple" class="infinite-tag">&infin;</a-tag>
</template>
<template slot="info" slot-scope="text, dbInbound">
<a-popover placement="bottomRight" :overlay-class-name="themeSwitcher.currentTheme" trigger="click">
<template slot="content">
<table cellpadding="2">
<tr>
<td>{{ i18n "pages.inbounds.protocol" }}</td>
<td>
<a-tag style="margin:0;" color="purple">[[ dbInbound.protocol ]]</a-tag>
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
<a-tag style="margin:0;" color="blue">[[ dbInbound.toInbound().stream.network ]]</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="green">tls</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="green">reality</a-tag>
</template>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.port" }}</td>
<td><a-tag>[[ dbInbound.port ]]</a-tag></td>
</tr>
<tr v-if="clientCount[dbInbound.id]">
<td>{{ i18n "clients" }}</td>
<td>
<a-tag style="margin:0;" color="blue">[[ clientCount[dbInbound.id].clients ]]</a-tag>
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p>
</template>
<a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p>
</template>
<a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p>
</template>
<a-tag style="margin:0; padding: 0 2px;" color="orange" v-if="clientCount[dbInbound.id].expiring.length">[[ clientCount[dbInbound.id].expiring.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].online">[[ clientEmail ]]</p>
</template>
<a-tag style="margin:0; padding: 0 2px;" color="green" v-if="clientCount[dbInbound.id].online.length">[[ clientCount[dbInbound.id].online.length ]]</a-tag>
</a-popover>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.traffic" }}</td>
<td>
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<table cellpadding="2" width="100%">
<tr>
<td>↑[[ sizeFormat(dbInbound.up) ]]</td>
<td>↓[[ sizeFormat(dbInbound.down) ]]</td>
</tr>
<tr v-if="dbInbound.total > 0 && dbInbound.up + dbInbound.down < dbInbound.total">
<td>{{ i18n "remained" }}</td>
<td>[[ sizeFormat(dbInbound.total - dbInbound.up - dbInbound.down) ]]</td>
</tr>
</table>
</template>
<a-tag :color="usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)">
[[ sizeFormat(dbInbound.up + dbInbound.down) ]] /
<template v-if="dbInbound.total > 0">
[[ sizeFormat(dbInbound.total) ]]
</template>
<template v-else>&infin;</template>
</a-tag>
</a-popover>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.expireDate" }}</td>
<td>
<a-tag style="min-width: 50px; text-align: center;" v-if="dbInbound.expiryTime > 0" :color="dbInbound.isExpiry? 'red': 'blue'">
[[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
</a-tag>
<a-tag v-else style="text-align: center;" color="purple" class="infinite-tag">&infin;</a-tag>
</td>
</tr>
</table>
</template>
<a-badge>
<a-icon v-if="!dbInbound.enable" slot="count" type="pause-circle" :style="'color: ' + themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-icon>
<a-button shape="round" size="small" style="font-size: 14px; padding: 0 10px;">
<a-icon type="info"></a-icon>
</a-button>
</a-badge>
</a-popover>
</template>
<template slot="expandedRowRender" slot-scope="record">
<a-table
v-if="(record.protocol === Protocols.VLESS) || (record.protocol === Protocols.VMESS)"
:row-key="client => client.id"
:columns="innerColumns"
:columns="isMobile ? innerMobileColumns : innerColumns"
:data-source="getInboundClients(record)"
:pagination="false"
style="margin-left: 20px;"
>
{{template "client_table"}}
</a-table>
<a-table
v-else-if="record.protocol === Protocols.TROJAN || record.toInbound().isSSMultiUser"
:row-key="client => client.id"
:columns="innerTrojanColumns"
:data-source="getInboundClients(record)"
:pagination="false"
style="margin-left: 20px;"
>
:pagination=pagination(getInboundClients(record))
:style="isMobile ? 'margin: -16px -5px -17px;' : 'margin-left: 10px;'">
{{template "client_table"}}
</a-table>
</template>
@ -285,6 +435,12 @@
</a-layout>
</a-layout>
{{template "js" .}}
<script src="{{ .base_path }}assets/base64/base64.min.js"></script>
<script src="{{ .base_path }}assets/qrcode/qrious.min.js"></script>
<script src="{{ .base_path }}assets/clipboard/clipboard.min.js"></script>
<script src="{{ .base_path }}assets/uri/URI.min.js"></script>
<script src="{{ .base_path }}assets/js/model/xray.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/model/dbinbound.js?{{ .cur_ver }}"></script>
{{template "component/themeSwitcher" .}}
<script>
const columns = [{
@ -292,6 +448,7 @@
align: 'right',
dataIndex: "id",
width: 30,
responsive: ["xs"],
}, {
title: '{{ i18n "pages.inbounds.operate" }}',
align: 'center',
@ -320,7 +477,7 @@
}, {
title: '{{ i18n "clients" }}',
align: 'left',
width: 40,
width: 50,
scopedSlots: { customRender: 'clients' },
}, {
title: '{{ i18n "pages.inbounds.traffic" }}',
@ -330,26 +487,46 @@
}, {
title: '{{ i18n "pages.inbounds.expireDate" }}',
align: 'center',
width: 80,
width: 40,
scopedSlots: { customRender: 'expiryTime' },
}];
const mobileColums = [{
title: "ID",
align: 'right',
dataIndex: "id",
width: 10,
responsive: ["s"],
}, {
title: '{{ i18n "pages.inbounds.operate" }}',
align: 'center',
width: 25,
scopedSlots: { customRender: 'action' },
}, {
title: '{{ i18n "pages.inbounds.remark" }}',
align: 'left',
width: 70,
dataIndex: "remark",
}, {
title: '{{ i18n "pages.inbounds.info" }}',
align: 'center',
width: 10,
scopedSlots: { customRender: 'info' },
}];
const innerColumns = [
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 40, scopedSlots: { customRender: 'enable' } },
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 50, scopedSlots: { customRender: 'actions' } },
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 20, scopedSlots: { customRender: 'enable' } },
{ title: '{{ i18n "online" }}', width: 20, scopedSlots: { customRender: 'online' } },
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
{ title: '{{ i18n "pages.inbounds.traffic" }}', width: 50, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 50, scopedSlots: { customRender: 'expiryTime' } },
{ title: 'UUID', width: 120, dataIndex: "id" },
{ title: '{{ i18n "pages.inbounds.traffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, align: 'center', scopedSlots: { customRender: 'expiryTime' } },
];
const innerTrojanColumns = [
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 40, scopedSlots: { customRender: 'enable' } },
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
{ title: '{{ i18n "pages.inbounds.traffic" }}', width: 50, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 50, scopedSlots: { customRender: 'expiryTime' } },
{ title: '{{ i18n "password" }}', width: 170, dataIndex: "password" },
const innerMobileColumns = [
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 10, align: 'center', scopedSlots: { customRender: 'actionMenu' } },
{ title: '{{ i18n "pages.inbounds.client" }}', width: 90, align: 'left', scopedSlots: { customRender: 'client' } },
{ title: '{{ i18n "pages.inbounds.info" }}', width: 10, align: 'center', scopedSlots: { customRender: 'info' } },
];
const app = new Vue({
@ -370,17 +547,18 @@
defaultCert: '',
defaultKey: '',
clientCount: [],
onlineClients: [],
isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
refreshing: false,
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
subSettings: {
enable : false,
port: 0,
path: '',
domain: '',
tls: false
subURI : ''
},
tgBotEnable: false
remarkModel: '-ieo',
tgBotEnable: false,
pageSize: 0,
isMobile: window.innerWidth <= 768,
},
methods: {
loading(spinning = true) {
@ -393,11 +571,19 @@
this.refreshing = false;
return;
}
await this.getOnlineUsers();
this.setInbounds(msg.obj);
setTimeout(() => {
this.refreshing = false;
}, 500);
},
async getOnlineUsers() {
const msg = await HttpUtil.post('/panel/inbound/onlines');
if (!msg.success) {
return;
}
this.onlineClients = msg.obj != null ? msg.obj : [];
},
async getDefaultSettings() {
const msg = await HttpUtil.post('/panel/setting/defaultSettings');
if (!msg.success) {
@ -411,11 +597,10 @@
this.tgBotEnable = tgBotEnable;
this.subSettings = {
enable : subEnable,
port: subPort,
path: subPath,
domain: subDomain,
tls: subTLS
subURI: subURI
};
this.pageSize = pageSize;
this.remarkModel = remarkModel;
}
},
setInbounds(dbInbounds) {
@ -441,8 +626,8 @@
}
},
getClientCounts(dbInbound, inbound) {
let clientCount = 0, active = [], deactive = [], depleted = [], expiring = [];
clients = this.getClients(dbInbound.protocol, inbound.settings);
let clientCount = 0, active = [], deactive = [], depleted = [], expiring = [], online = [];
clients = inbound.clients;
clientStats = dbInbound.clientStats
now = new Date().getTime()
if (clients) {
@ -450,6 +635,7 @@
if (dbInbound.enable) {
clients.forEach(client => {
client.enable ? active.push(client.email) : deactive.push(client.email);
if(this.isClientOnline(client.email)) online.push(client.email);
});
clientStats.forEach(client => {
if (!client.enable) {
@ -471,6 +657,7 @@
deactive: deactive,
depleted: depleted,
expiring: expiring,
online: online,
};
},
searchInbounds(key) {
@ -530,6 +717,9 @@
},
generalActions(action) {
switch (action.key) {
case "import":
this.importInbound();
break;
case "export":
this.exportAllLinks();
break;
@ -547,10 +737,10 @@
clickAction(action, dbInbound) {
switch (action.key) {
case "qrcode":
this.showQrcode(dbInbound);
this.showQrcode(dbInbound.id);
break;
case "showInfo":
this.showInfo(dbInbound);
this.showInfo(dbInbound.id);
break;
case "edit":
this.openEditInbound(dbInbound.id);
@ -564,6 +754,9 @@
case "export":
this.inboundLinks(dbInbound.id);
break;
case "clipboard":
this.copyToClipboard(dbInbound.id);
break;
case "resetTraffic":
this.resetTraffic(dbInbound.id);
break;
@ -586,6 +779,7 @@
title: '{{ i18n "pages.inbounds.cloneInbound"}} \"' + dbInbound.remark + '\"',
content: '{{ i18n "pages.inbounds.cloneInboundContent"}}',
okText: '{{ i18n "pages.inbounds.cloneInboundOk"}}',
class: themeSwitcher.currentTheme,
cancelText: '{{ i18n "cancel" }}',
onOk: () => {
const baseInbound = dbInbound.toInbound();
@ -752,7 +946,7 @@
this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
class: themeSwitcher.darkCardClass,
class: themeSwitcher.currentTheme,
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => {
@ -767,31 +961,26 @@
this.$confirm({
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
class: themeSwitcher.darkCardClass,
class: themeSwitcher.currentTheme,
okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/panel/inbound/del/' + dbInboundId),
});
},
delClient(dbInboundId, client) {
delClient(dbInboundId, client,confirmation = true) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
clientId = this.getClientId(dbInbound.protocol, client);
this.$confirm({
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
class: themeSwitcher.darkCardClass,
okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`),
});
},
getClients(protocol, clientSettings) {
switch (protocol) {
case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans;
case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
default: return null;
if (confirmation){
this.$confirm({
title: '{{ i18n "pages.inbounds.deleteClient"}}',
content: '{{ i18n "pages.inbounds.deleteClientContent"}}',
class: themeSwitcher.currentTheme,
okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`),
});
} else {
this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`);
}
},
getClientId(protocol, client) {
@ -805,7 +994,7 @@
newDbInbound = new DBInbound(dbInbound);
if (dbInbound.listen.startsWith("@")){
rootInbound = this.inbounds.find((i) =>
i.tls &&
i.isTcp &&
['trojan','vless'].includes(i.protocol) &&
i.settings.fallbacks.find(f => f.dest === dbInbound.listen)
);
@ -813,18 +1002,27 @@
newDbInbound.listen = rootInbound.listen;
newDbInbound.port = rootInbound.port;
newInbound = newDbInbound.toInbound();
newInbound.stream.security = 'tls';
newInbound.stream.security = rootInbound.stream.security;
newInbound.stream.tls = rootInbound.stream.tls;
newInbound.stream.externalProxy = rootInbound.stream.externalProxy;
newDbInbound.streamSettings = newInbound.stream.toString();
}
}
return newDbInbound;
},
showQrcode(dbInbound, clientIndex) {
showQrcode(dbInboundId, client) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
newDbInbound = this.checkFallback(dbInbound);
qrModal.show('{{ i18n "qrCode"}}', newDbInbound, clientIndex);
qrModal.show('{{ i18n "qrCode"}}', newDbInbound, client);
},
showInfo(dbInbound, index) {
showInfo(dbInboundId, client) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
index=0;
if (dbInbound.isMultiUser()){
inbound = dbInbound.toInbound();
clients = inbound.clients;
index = this.findIndexOfClient(dbInbound.protocol, clients, client);
}
newDbInbound = this.checkFallback(dbInbound);
infoModal.show(newDbInbound, index);
},
@ -836,7 +1034,7 @@
this.loading()
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
inbound = dbInbound.toInbound();
clients = this.getClients(dbInbound.protocol, inbound.settings);
clients = inbound.clients;
index = this.findIndexOfClient(dbInbound.protocol, clients, client);
clients[index].enable = !clients[index].enable;
clientId = this.getClientId(dbInbound.protocol, clients[index]);
@ -850,31 +1048,27 @@
}
},
getInboundClients(dbInbound) {
if (dbInbound.protocol == Protocols.VLESS) {
return dbInbound.toInbound().settings.vlesses;
} else if (dbInbound.protocol == Protocols.VMESS) {
return dbInbound.toInbound().settings.vmesses;
} else if (dbInbound.protocol == Protocols.TROJAN) {
return dbInbound.toInbound().settings.trojans;
} else if (dbInbound.protocol == Protocols.SHADOWSOCKS) {
return dbInbound.toInbound().settings.shadowsockses;
}
return dbInbound.toInbound().clients;
},
resetClientTraffic(client, dbInboundId) {
this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
class: themeSwitcher.darkCardClass,
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email),
})
resetClientTraffic(client, dbInboundId, confirmation = true) {
if (confirmation){
this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
class: themeSwitcher.currentTheme,
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email),
})
} else {
this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email);
}
},
resetAllTraffic() {
this.$confirm({
title: '{{ i18n "pages.inbounds.resetAllTrafficTitle"}}',
content: '{{ i18n "pages.inbounds.resetAllTrafficContent"}}',
class: themeSwitcher.darkCardClass,
class: themeSwitcher.currentTheme,
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/panel/inbound/resetAllTraffics'),
@ -884,7 +1078,7 @@
this.$confirm({
title: dbInboundId > 0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficTitle"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficTitle"}}',
content: dbInboundId > 0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficContent"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficContent"}}',
class: themeSwitcher.darkCardClass,
class: themeSwitcher.currentTheme,
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/panel/inbound/resetAllClientTraffics/' + dbInboundId),
@ -894,48 +1088,117 @@
this.$confirm({
title: '{{ i18n "pages.inbounds.delDepletedClientsTitle"}}',
content: '{{ i18n "pages.inbounds.delDepletedClientsContent"}}',
class: themeSwitcher.darkCardClass,
class: themeSwitcher.currentTheme,
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/panel/inbound/delDepletedClients/' + dbInboundId),
})
},
isExpiry(dbInbound, index) {
return dbInbound.toInbound().isExpiry(index)
return dbInbound.toInbound().isExpiry(index);
},
getUpStats(dbInbound, email) {
if (email.length == 0) return 0
clientStats = dbInbound.clientStats.find(stats => stats.email === email)
return clientStats ? clientStats.up : 0
if (email.length == 0) return 0;
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
return clientStats ? clientStats.up : 0;
},
getDownStats(dbInbound, email) {
if (email.length == 0) return 0
clientStats = dbInbound.clientStats.find(stats => stats.email === email)
return clientStats ? clientStats.down : 0
},
statsColor(dbInbound, email) {
if(email.length == 0) return 'blue';
if (email.length == 0) return 0;
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
return usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total);
return clientStats ? clientStats.down : 0;
},
getSumStats(dbInbound, email) {
if (email.length == 0) return 0;
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
return clientStats ? clientStats.up + clientStats.down : 0;
},
getRemStats(dbInbound, email) {
if (email.length == 0) return 0;
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
if (!clientStats) return 0;
remained = clientStats.total - (clientStats.up + clientStats.down);
return remained>0 ? remained : 0;
},
clientStatsColor(dbInbound, email) {
if (email.length == 0) return clientUsageColor();
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
return clientUsageColor(clientStats, app.trafficDiff)
},
statsProgress(dbInbound, email) {
if (email.length == 0) return 100;
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
if (!clientStats) return 0;
if (clientStats.total == 0) return 100;
return 100*(clientStats.down + clientStats.up)/clientStats.total;
},
expireProgress(expTime, reset) {
now = new Date().getTime();
remainedSeconds = expTime < 0 ? -expTime/1000 : (expTime-now)/1000;
resetSeconds = reset * 86400;
if (remainedSeconds >= resetSeconds) return 0;
return 100*(1-(remainedSeconds/resetSeconds));
},
remainedDays(expTime){
if (expTime == 0) return null;
if (expTime < 0) return formatSecond(expTime/-1000);
now = new Date().getTime();
if (expTime < now) return '{{ i18n "depleted" }}';
return formatSecond((expTime-now)/1000);
},
statsExpColor(dbInbound, email){
if (email.length == 0) return '#7a316f';
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
if (!clientStats) return '#7a316f';
statsColor = usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total);
expColor = usageColor(new Date().getTime(), this.expireDiff, clientStats.expiryTime);
switch (true) {
case statsColor == "red" || expColor == "red":
return "#cf3c3c"; // Red
case statsColor == "orange" || expColor == "orange":
return "#f37b24"; // Orange
case statsColor == "green" || expColor == "green":
return "#008771"; // Green
default:
return "#7a316f"; // purple
}
},
isClientEnabled(dbInbound, email) {
clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null
return clientStats ? clientStats['enable'] : true
clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null;
return clientStats ? clientStats['enable'] : true;
},
isRemovable(dbInbound_id) {
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInbound_id)).length > 1
isClientOnline(email) {
return this.onlineClients.includes(email);
},
isRemovable(dbInboundId) {
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInboundId)).length > 1
},
inboundLinks(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
newDbInbound = this.checkFallback(dbInbound);
txtModal.show('{{ i18n "pages.inbounds.export"}}', newDbInbound.genInboundLinks, newDbInbound.remark);
},
importInbound() {
promptModal.open({
title: '{{ i18n "pages.inbounds.importInbound" }}',
type: 'textarea',
value: '',
okText: '{{ i18n "pages.inbounds.import" }}',
confirm: async (dbInboundText) => {
await this.submit('/panel/inbound/import', {data: dbInboundText}, promptModal);
promptModal.close();
},
});
},
exportAllLinks() {
let copyText = '';
let copyText = [];
for (const dbInbound of this.dbInbounds) {
copyText += dbInbound.genInboundLinks
copyText.push(dbInbound.genInboundLinks(this.remarkModel));
}
txtModal.show('{{ i18n "pages.inbounds.export"}}', copyText, 'All-Inbounds');
txtModal.show('{{ i18n "pages.inbounds.export"}}', copyText.join('\r\n'), 'All-Inbounds');
},
copyToClipboard(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
txtModal.show('{{ i18n "pages.inbounds.inboundData" }}', JSON.stringify(dbInbound, null, 2));
},
async startDataRefreshLoop() {
while (this.isRefreshEnabled) {
@ -963,6 +1226,30 @@
this.spinning = false;
}
},
pagination(obj){
if (this.pageSize > 0 && obj.length>this.pageSize) {
// Set page options based on object size
sizeOptions = []
for (i=this.pageSize;i<=obj.length;i=i+this.pageSize) {
sizeOptions.push(i.toString());
}
// Add option to see all in one page
sizeOptions.push(i.toString());
p = {
showSizeChanger: true,
size: 'small',
position: 'bottom',
pageSize: this.pageSize,
pageSizeOptions: sizeOptions
}
return p
}
return false
},
onResize() {
this.isMobile = window.innerWidth <= 768;
}
},
watch: {
searchKey: debounce(function (newVal) {
@ -970,6 +1257,8 @@
}, 500)
},
mounted() {
window.addEventListener('resize', this.onResize);
this.onResize();
this.loading();
this.getDefaultSettings();
if (this.isRefreshEnabled) {

View file

@ -6,6 +6,9 @@
.ant-layout-content {
margin: 24px 16px;
}
.ant-card-hoverable {
margin-inline: 0.3rem;
}
}
.ant-col-sm-24 {
@ -18,21 +21,20 @@
</style>
<body>
<a-layout id="app" v-cloak>
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
{{ template "commonSider" . }}
<a-layout id="content-layout" :style="themeSwitcher.bgStyle">
<a-layout id="content-layout">
<a-layout-content>
<a-spin :spinning="spinning" :delay="200" :tip="loadingTip"/>
<transition name="list" appear>
<a-row>
<a-card hoverable :class="themeSwitcher.darkCardClass">
<a-card hoverable>
<a-row>
<a-col :sm="24" :md="12">
<a-row>
<a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal"
:stroke-color="status.cpu.color"
:class="themeSwitcher.darkCardClass"
:percent="status.cpu.percent"></a-progress>
<div>CPU: [[ cpuCoreFormat(status.cpuCores) ]]</div>
<div>Speed: [[ cpuSpeedFormat(status.cpuSpeedMhz) ]]</div>
@ -40,7 +42,6 @@
<a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal"
:stroke-color="status.mem.color"
:class="themeSwitcher.darkCardClass"
:percent="status.mem.percent"></a-progress>
<div>
{{ i18n "pages.index.memory"}}: [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]]
@ -53,7 +54,6 @@
<a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal"
:stroke-color="status.swap.color"
:class="themeSwitcher.darkCardClass"
:percent="status.swap.percent"></a-progress>
<div>
Swap: [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]]
@ -62,7 +62,6 @@
<a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal"
:stroke-color="status.disk.color"
:class="themeSwitcher.darkCardClass"
:percent="status.disk.percent"></a-progress>
<div>
{{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]]
@ -77,37 +76,41 @@
<transition name="list" appear>
<a-row>
<a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass">
<a-card hoverable>
3X: <a href="https://github.com/MHSanaei/3x-ui/releases" target="_blank"><a-tag color="green">v{{ .cur_ver }}</a-tag></a>
Xray: <a-tag color="green" style="cursor: pointer;" @click="openSelectV2rayVersion">v[[ status.xray.version ]]</a-tag>
<a href="https://t.me/panel3xui" target="_blank"><a-tag color="green">@panel3xui</a-tag></a>
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass">
<a-card hoverable>
{{ i18n "menu.link" }}:
<a-tag color="blue" style="cursor: pointer;" @click="openLogs()">{{ i18n "pages.index.logs" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="openBackup">{{ i18n "pages.index.backup" }}</a-tag>
<a-tag color="purple" style="cursor: pointer;" @click="openLogs()">{{ i18n "pages.index.logs" }}</a-tag>
<a-tag color="purple" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag>
<a-tag color="purple" style="cursor: pointer;" @click="openBackup">{{ i18n "pages.index.backup" }}</a-tag>
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass">
<a-card hoverable>
{{ i18n "pages.index.xrayStatus" }}:
<a-tag :color="status.xray.color">[[ status.xray.state ]]</a-tag>
<a-tooltip v-if="status.xray.state === State.Error">
<template slot="title">
<p v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</p>
<a-popover v-if="status.xray.state === State.Error"
:overlay-class-name="themeSwitcher.currentTheme">
<span slot="title" style="font-size: 12pt">Error in running xray-core
<a-tag color="purple" style="cursor: pointer; float: right;" @click="openLogs()">{{ i18n "pages.index.logs" }}</a-tag>
</span>
<template slot="content">
<p style="max-width: 400px" v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</p>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
<a-tag color="blue" style="cursor: pointer;" @click="stopXrayService">{{ i18n "pages.index.stopXray" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="restartXrayService">{{ i18n "pages.index.restartXray" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch" }}</a-tag>
</a-popover>
<a-tag color="purple" style="cursor: pointer;" @click="stopXrayService">{{ i18n "pages.index.stopXray" }}</a-tag>
<a-tag color="purple" style="cursor: pointer;" @click="restartXrayService">{{ i18n "pages.index.restartXray" }}</a-tag>
<a-tag color="purple" style="cursor: pointer;" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch" }}</a-tag>
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass">
<a-card hoverable>
{{ i18n "pages.index.operationHours" }}:
Xray:
<a-tag color="green">[[ formatSecond(status.appStats.uptime) ]]</a-tag>
@ -116,7 +119,7 @@
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass">
<a-card hoverable>
{{ i18n "pages.index.systemLoad" }}: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
<a-tooltip>
<template slot="title">
@ -127,7 +130,7 @@
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass">
<a-card hoverable>
{{ i18n "usage"}}:
Memory [[ sizeFormat(status.appStats.mem) ]] -
Threads [[ status.appStats.threads ]]
@ -135,7 +138,7 @@
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass">
<a-card hoverable>
<a-row>
<a-col :span="12">
IPv4:
@ -159,7 +162,7 @@
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass">
<a-card hoverable>
<a-row>
<a-col :span="12">
TCP: [[ status.tcpCount ]]
@ -183,7 +186,7 @@
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass">
<a-card hoverable>
<a-row>
<a-col :span="12">
<a-icon type="arrow-up"></a-icon>
@ -209,7 +212,7 @@
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass">
<a-card hoverable>
<a-row>
<a-col :span="12">
<a-icon type="cloud-upload"></a-icon>
@ -241,21 +244,21 @@
<a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}'
:closable="true" @ok="() => versionModal.visible = false"
:class="themeSwitcher.darkCardClass"
:class="themeSwitcher.currentTheme"
footer="">
<h2>{{ i18n "pages.index.xraySwitchClick"}}</h2>
<h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2>
<template v-for="version, index in versionModal.versions">
<a-tag :color="index % 2 == 0 ? 'blue' : 'green'"
<a-tag :color="index % 2 == 0 ? 'purple' : 'green'"
style="margin: 10px" @click="switchV2rayVersion(version)">
[[ version ]]
</a-tag>
</template>
</a-modal>
<a-modal id="log-modal" v-model="logModal.visible" title="X-UI logs"
<a-modal id="log-modal" v-model="logModal.visible" title="Logs"
:closable="true" @ok="() => logModal.visible = false" @cancel="() => logModal.visible = false"
:class="themeSwitcher.darkCardClass"
:class="themeSwitcher.currentTheme"
width="800px"
footer="">
<a-form layout="inline">
@ -263,7 +266,7 @@
<a-select v-model="logModal.rows"
style="width: 80px"
@change="openLogs()"
:dropdown-class-name="themeSwitcher.darkCardClass">
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="10">10</a-select-option>
<a-select-option value="20">20</a-select-option>
<a-select-option value="50">50</a-select-option>
@ -274,7 +277,7 @@
<a-select v-model="logModal.level"
style="width: 120px"
@change="openLogs()"
:dropdown-class-name="themeSwitcher.darkCardClass">
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="debug">Debug</a-select-option>
<a-select-option value="info">Info</a-select-option>
<a-select-option value="notice">Notice</a-select-option>
@ -286,7 +289,7 @@
<a-checkbox v-model="logModal.syslog" @change="openLogs()"></a-checkbox>
</a-form-item>
<a-form-item>
<button class="ant-btn ant-btn-primary" @click="openLogs()"><a-icon type="sync"></a-icon> Reload</button>
<a-button class="ant-btn ant-btn-primary" :loading="logModal.loading" @click="openLogs()"><a-icon :spin="logModal.loading" type="sync"></a-icon> Reload</a-button>
</a-form-item>
<a-form-item>
<a-button type="primary" style="margin-bottom: 10px;"
@ -295,17 +298,16 @@
</a-button>
</a-form-item>
</a-form>
<a-input type="textarea" v-model="logModal.logs" disabled="true"
:autosize="{ minRows: 10, maxRows: 22}"></a-input>
<div class="ant-input" style="height: auto; max-height: 500px; overflow: auto;" v-html="logModal.logs"></div>
</a-modal>
<a-modal id="backup-modal" v-model="backupModal.visible" :title="backupModal.title"
:closable="true" :class="themeSwitcher.darkCardClass"
:closable="true" :class="themeSwitcher.currentTheme"
@ok="() => backupModal.hide()" @cancel="() => backupModal.hide()">
<p style="color: inherit; font-size: 16px; padding: 4px 2px;">
<a-icon type="warning" style="color: inherit; font-size: 20px;"></a-icon>
[[ backupModal.description ]]
</p>
<a-alert type="warning" style="margin-bottom: 10px; width: fit-content"
:message="backupModal.description"
show-icon
></a-alert>
<a-space direction="horizontal" style="text-align: center" style="margin-bottom: 10px;">
<a-button type="primary" @click="exportDatabase()">
[[ backupModal.exportText ]]
@ -318,6 +320,7 @@
</a-layout>
{{template "js" .}}
<script src="{{ .base_path }}assets/clipboard/clipboard.min.js"></script>
{{template "component/themeSwitcher" .}}
{{template "textModal"}}
<script>
@ -346,11 +349,11 @@
get color() {
const percent = this.percent;
if (percent < 80) {
return '#67C23A';
return '#008771'; // Green
} else if (percent < 90) {
return '#E6A23C';
return "#f37b24"; // Orange
} else {
return '#F56C6C';
return "#cf3c3c"; // Red
}
}
}
@ -427,9 +430,46 @@
rows: 20,
level: 'info',
syslog: false,
loading: false,
show(logs) {
this.visible = true;
this.logs = logs? logs.join("\n"): "No Record...";
this.logs = logs? this.formatLogs(logs) : "No Record...";
},
formatLogs(logs) {
let formattedLogs = '';
const levels = ["DEBUG","INFO","NOTICE","WARNING","ERROR"];
const levelColors = ["#3c89e8","#008771","#008771","#f37b24","#e04141","#bcbcbc"];
logs.forEach((log, index) => {
let [data, message] = log.split(" - ",2);
const parts = data.split(" ")
if(index>0) formattedLogs += '<br>';
if (parts.length === 3) {
const d = parts[0];
const t = parts[1];
const level = parts[2];
const levelIndex = levels.indexOf(level,levels) || 5;
//formattedLogs += `<span style="color: gray;">${index + 1}.</span>`;
formattedLogs += `<span style="color: ${levelColors[0]};">${d} ${t}</span> `;
formattedLogs += `<span style="color: ${levelColors[levelIndex]}">${level}</span>`;
} else {
const levelIndex = levels.indexOf(data,levels) || 5;
formattedLogs += `<span style="color: ${levelColors[levelIndex]}">${data}</span>`;
}
if(message){
if(message.startsWith("XRAY:"))
message = "<b>XRAY: </b>" + message.substring(5);
else
message = "<b>X-UI: </b>" + message;
}
formattedLogs += message ? ' - ' + message : '';
});
return formattedLogs;
},
hide() {
this.visible = false;
@ -504,7 +544,7 @@
title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}',
content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}' + ` ${version}?`,
okText: '{{ i18n "confirm"}}',
class: themeSwitcher.darkCardClass,
class: themeSwitcher.currentTheme,
cancelText: '{{ i18n "cancel"}}',
onOk: async () => {
versionModal.hide();
@ -531,13 +571,14 @@
}
},
async openLogs(){
this.loading(true);
logModal.loading = true;
const msg = await HttpUtil.post('server/logs/'+logModal.rows,{level: logModal.level, syslog: logModal.syslog});
this.loading(false);
if (!msg.success) {
return;
}
logModal.show(msg.obj);
await PromiseUtil.sleep(500);
logModal.loading = false;
},
async openConfig() {
this.loading(true);

File diff suppressed because it is too large Load diff

1401
web/html/xui/xray.html Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,127 @@
{{define "outModal"}}
<a-modal id="out-modal" v-model="outModal.visible" :title="outModal.title" @ok="outModal.ok"
:confirm-loading="outModal.confirmLoading" :closable="true" :mask-closable="false"
:ok-button-props="{ props: { disabled: !outModal.isValid } }" style="overflow: hidden;"
:ok-text="outModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
{{template "form/outbound"}}
</a-modal>
<script>
const outModal = {
title: '',
visible: false,
confirmLoading: false,
okText: '{{ i18n "sure" }}',
isEdit: false,
confirm: null,
outbound: new Outbound(),
jsonMode: false,
link: '',
cm: null,
duplicateTag: false,
isValid: true,
activeKey: '1',
tags: [],
ok() {
ObjectUtil.execute(outModal.confirm, outModal.outbound.toJson());
},
show({ title='', okText='{{ i18n "sure" }}', outbound, confirm=(outbound)=>{}, isEdit=false, tags=[] }) {
this.title = title;
this.okText = okText;
this.confirm = confirm;
this.jsonMode = false;
this.link = '';
this.activeKey = '1';
this.visible = true;
this.outbound = isEdit ? Outbound.fromJson(outbound) : new Outbound();
this.isEdit = isEdit;
this.tags = tags;
this.check()
},
close() {
outModal.visible = false;
outModal.loading(false);
},
loading(loading) {
outModal.confirmLoading = loading;
},
check(){
if(outModal.outbound.tag == '' || outModal.tags.includes(outModal.outbound.tag)){
this.duplicateTag = true;
this.isValid = false;
} else {
this.duplicateTag = false;
this.isValid = true;
}
},
toggleJson(jsonTab) {
textAreaObj = document.getElementById('outboundJson');
if(jsonTab){
if(this.cm != null) {
this.cm.toTextArea();
this.cm=null;
}
textAreaObj.value = JSON.stringify(this.outbound.toJson(), null, 2);
this.cm = CodeMirror.fromTextArea(textAreaObj, app.cmOptions);
this.cm.on('change',editor => {
value = editor.getValue();
if(this.isJsonString(value)){
this.outbound = Outbound.fromJson(JSON.parse(value));
this.check();
}
});
this.activeKey = '2';
} else {
if(this.cm != null) {
this.cm.toTextArea();
this.cm=null;
}
this.activeKey = '1';
}
},
isJsonString(str) {
try {
JSON.parse(str);
} catch (e) {
return false;
}
return true;
},
};
new Vue({
delimiters: ['[[', ']]'],
el: '#out-modal',
data: {
outModal: outModal,
get outbound() {
return outModal.outbound;
},
},
methods: {
streamNetworkChange() {
if (this.outModal.outbound.protocol == Protocols.VLESS && !outModal.outbound.canEnableTlsFlow()) {
delete this.outModal.outbound.settings.flow;
}
},
canEnableTls() {
return this.outModal.outbound.canEnableTls();
},
convertLink(){
newOutbound = Outbound.fromLink(outModal.link);
if(newOutbound){
this.outModal.outbound = newOutbound;
this.outModal.toggleJson(true);
this.outModal.check();
this.$message.success('Link imported successfully...');
outModal.link = '';
} else {
this.$message.error('Wrong Link!');
outModal.link = '';
}
},
},
});
</script>
{{end}}

View file

@ -0,0 +1,176 @@
{{define "reverseModal"}}
<a-modal id="reverse-modal" v-model="reverseModal.visible" :title="reverseModal.title" @ok="reverseModal.ok"
:confirm-loading="reverseModal.confirmLoading" :closable="true" :mask-closable="false"
:ok-text="reverseModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
<a-form layout="inline">
<table width="100%" class="ant-table-tbody">
<tr>
<td>{{ i18n "pages.xray.outbound.type" }}</td>
<td>
<a-form-item>
<a-select v-model="reverseModal.reverse.type" style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="x,y in reverseTypes" :value="y">[[ x ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.xray.outbound.tag" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="reverseModal.reverse.tag" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.xray.outbound.domain" }}</td>
<td>
<a-form-item>
<a-input v-model.trim="reverseModal.reverse.domain" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<template v-if="reverseModal.reverse.type=='bridge'">
<tr>
<td>{{ i18n "pages.xray.outbound.intercon" }}</td>
<td>
<a-form-item>
<a-select v-model="reverseModal.rules[0].outboundTag" style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="x in reverseModal.outboundTags" :value="x">[[ x ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.xray.rules.outbound" }}</td>
<td>
<a-form-item>
<a-select v-model="reverseModal.rules[1].outboundTag" style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="x in reverseModal.outboundTags" :value="x">[[ x ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
</template>
<template v-else>
<tr>
<td>{{ i18n "pages.xray.outbound.intercon" }}</td>
<td>
<a-form-item>
<a-checkbox-group
v-model="reverseModal.rules[0].inboundTag"
:options="reverseModal.inboundTags"></a-checkbox-group>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.xray.rules.inbound" }}</td>
<td>
<a-form-item>
<a-checkbox-group
v-model="reverseModal.rules[1].inboundTag"
:options="reverseModal.inboundTags"></a-checkbox-group>
</a-form-item>
</td>
</tr>
</template>
</table>
</a-form>
</a-modal>
<script>
const reverseModal = {
title: '',
visible: false,
confirmLoading: false,
okText: '{{ i18n "sure" }}',
isEdit: false,
confirm: null,
reverse: {
tag: "",
type: "",
domain: ""
},
rules: [
{ outboundTag: '', inboundTag: []},
{ outboundTag: '', inboundTag: []}
],
inboundTags: [],
outboundTags: [],
ok() {
reverseModal.rules[0].domain = ["full:" + reverseModal.reverse.domain];
reverseModal.rules[0].type = 'field';
reverseModal.rules[1].type = 'field';
if(reverseModal.reverse.type == 'bridge'){
reverseModal.rules[0].inboundTag = [reverseModal.reverse.tag];
reverseModal.rules[1].inboundTag = [reverseModal.reverse.tag];
} else {
reverseModal.rules[0].outboundTag = reverseModal.reverse.tag;
reverseModal.rules[1].outboundTag = reverseModal.reverse.tag;
}
ObjectUtil.execute(reverseModal.confirm, reverseModal.reverse, reverseModal.rules);
},
show({ title='', okText='{{ i18n "sure" }}', reverse, rules, confirm=(reverse, rules)=>{}, isEdit=false }) {
this.title = title;
this.okText = okText;
this.confirm = confirm;
this.visible = true;
if(isEdit) {
this.reverse = {
tag: reverse.tag,
type: reverse.type,
domain: reverse.domain,
};
reverse;
rules0 = rules.filter(r => r.domain != null);
if(rules0.length == 0) rules0 = [{ outboundTag: '', domain: ["full:" + this.reverse.domain], inboundTag: []}];
rules1 = rules.filter(r => r.domain == null);
if(rules1.length == 0) rules1 = [{ outboundTag: '', inboundTag: []}];
this.rules = [];
this.rules.push({
domain: rules0[0].domain,
outboundTag: rules0[0].outboundTag,
inboundTag: rules0.map(r => r.inboundTag).flat()
});
this.rules.push({
outboundTag: rules1[0].outboundTag,
inboundTag: rules1.map(r => r.inboundTag).flat()
});
} else {
this.reverse = {
tag: "reverse-" + app.reverseData.length,
type: "bridge",
domain: "reverse.xui"
}
this.rules = [
{ outboundTag: '', inboundTag: []},
{ outboundTag: '', inboundTag: []}
]
}
this.isEdit = isEdit;
this.inboundTags = app.templateSettings.inbounds.filter((i) => !ObjectUtil.isEmpty(i.tag)).map(obj => obj.tag);
this.inboundTags.push(...app.inboundTags);
this.outboundTags = app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag);
},
close() {
reverseModal.visible = false;
reverseModal.loading(false);
},
loading(loading) {
reverseModal.confirmLoading = loading;
},
};
new Vue({
delimiters: ['[[', ']]'],
el: '#reverse-modal',
data: {
reverseModal: reverseModal,
reverseTypes: { bridge: '{{ i18n "pages.xray.outbound.bridge" }}', portal:'{{ i18n "pages.xray.outbound.portal" }}'},
},
methods: {
}
});
</script>
{{end}}

View file

@ -0,0 +1,277 @@
{{define "ruleModal"}}
<a-modal id="rule-modal" v-model="ruleModal.visible" :title="ruleModal.title" @ok="ruleModal.ok"
:confirm-loading="ruleModal.confirmLoading" :closable="true" :mask-closable="false"
:ok-text="ruleModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
<a-form layout="inline">
<table width="100%" class="ant-table-tbody">
<tr>
<td style="width: 30%;">Domain Matcher</td>
<td>
<a-form-item>
<a-select v-model="ruleModal.rule.domainMatcher" style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="dm in ['','hybrid','linear']" :value="dm">[[ dm ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>Source IPs
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
</template>
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input v-model.trim="ruleModal.rule.source" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>Source Port
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
</template>
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input v-model.trim="ruleModal.rule.sourcePort" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>Network</td>
<td>
<a-form-item>
<a-select v-model="ruleModal.rule.network" style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="x in ['','tcp','tdp','tcp,udp']" :value="x">[[ x ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>Protocol</td>
<td>
<a-form-item>
<a-select v-model="ruleModal.rule.protocol" style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="x in ['','http','tls','bittorrent']" :value="x">[[ x ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td colspan="2">
<a-form-item>
<span>Attributes</span>
<a-button size="small" style="margin-left: 10px" @click="ruleModal.rule.attrs.push(['', ''])">+</a-button>
<a-input-group compact v-for="(attr,index) in ruleModal.rule.attrs">
<a-input style="width: 50%" v-model="attr[0]" placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
</a-input>
<a-input style="width: 50%" v-model="attr[1]" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<a-button slot="addonAfter" size="small" @click="ruleModal.rule.attrs.splice(index,1)">-</a-button>
</a-input>
</a-input-group>
</a-form-item>
</td>
</tr>
<tr>
<td>IP
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
</template>
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input v-model.trim="ruleModal.rule.ip" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>Domain
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
</template>
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input v-model.trim="ruleModal.rule.domain" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>Port
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
</template>
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input v-model.trim="ruleModal.rule.port" style="width: 250px"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>Inbound Tags</td>
<td>
<a-form-item>
<a-select v-model="ruleModal.rule.inboundTag" mode="multiple" style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="tag in ruleModal.inboundTags" :value="tag">[[ tag ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr>
<td>Outbound Tag</td>
<td>
<a-form-item>
<a-select v-model="ruleModal.rule.outboundTag" style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="tag in ruleModal.outboundTags" :value="tag">[[ tag ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
</table>
</a-form>
</a-modal>
<script>
const ruleModal = {
title: '',
visible: false,
confirmLoading: false,
okText: '{{ i18n "sure" }}',
isEdit: false,
confirm: null,
rule: {
type: "field",
domainMatcher: "",
domain: "",
ip: "",
port: "",
sourcePort: "",
network: "",
source: "",
user: "",
inboundTag: [],
protocol: [],
attrs: [],
outboundTag: "",
},
inboundTags: [],
outboundTags: [],
users: [],
balancerTag: [],
ok() {
newRule = ruleModal.getResult();
ObjectUtil.execute(ruleModal.confirm, newRule);
},
show({ title='', okText='{{ i18n "sure" }}', rule, confirm=(rule)=>{}, isEdit=false }) {
this.title = title;
this.okText = okText;
this.confirm = confirm;
this.visible = true;
if(isEdit) {
this.rule.domainMatcher = rule.domainMatcher;
this.rule.domain = rule.domain ? rule.domain.join(',') : [];
this.rule.ip = rule.ip ? rule.ip.join(',') : [];
this.rule.port = rule.port;
this.rule.sourcePort = rule.sourcePort;
this.rule.network = rule.network;
this.rule.source = rule.source ? rule.source.join(',') : [];
this.rule.user = rule.user ? rule.user.join(',') : [];
this.rule.inboundTag = rule.inboundTag;
this.rule.protocol = rule.protocol;
this.rule.attrs = rule.attrs ? Object.entries(rule.attrs) : [];
this.rule.outboundTag = rule.outboundTag;
} else {
this.rule = {
domainMatcher: "",
domain: "",
ip: "",
port: "",
sourcePort: "",
network: "",
source: "",
user: "",
inboundTag: [],
protocol: [],
attrs: [],
outboundTag: "",
}
}
this.isEdit = isEdit;
this.inboundTags = app.templateSettings.inbounds.filter((i) => !ObjectUtil.isEmpty(i.tag)).map(obj => obj.tag);
this.inboundTags.push(...app.inboundTags);
this.outboundTags = app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag);
if(app.templateSettings.reverse){
if(app.templateSettings.reverse.bridges) {
this.inboundTags.push(...app.templateSettings.reverse.bridges.map(b => b.tag));
}
if(app.templateSettings.reverse.portals) this.outboundTags.push(...app.templateSettings.reverse.portals.map(b => b.tag));
}
},
close() {
ruleModal.visible = false;
ruleModal.loading(false);
},
loading(loading) {
ruleModal.confirmLoading = loading;
},
getResult() {
value = ruleModal.rule;
rule = {};
newRule = {};
rule.type = "field";
rule.domainMatcher = value.domainMatcher;
rule.domain = value.domain.length>0 ? value.domain.split(',') : [];
rule.ip = value.ip.length>0 ? value.ip.split(',') : [];
rule.port = value.port;
rule.sourcePort = value.sourcePort;
rule.network = value.network;
rule.source = value.source.length>0 ? value.source.split(',') : [];
rule.user = value.user.length>0 ? value.user.split(',') : [];
rule.inboundTag = value.inboundTag;
rule.protocol = value.protocol;
rule.attrs = Object.fromEntries(value.attrs);
rule.outboundTag = value.outboundTag;
for (const [key, value] of Object.entries(rule)) {
if (
value !== null &&
value !== undefined &&
!(Array.isArray(value) && value.length === 0) &&
!(typeof value === 'object' && Object.keys(value).length === 0) &&
value !== ''
) {
newRule[key] = value;
}
}
return newRule;
}
};
new Vue({
delimiters: ['[[', ']]'],
el: '#rule-modal',
data: {
ruleModal: ruleModal,
}
});
</script>
{{end}}

View file

@ -168,9 +168,13 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
err = tx.Save(inbound).Error
if err == nil {
for _, client := range clients {
s.AddClientStat(tx, inbound.Id, &client)
if len(inbound.ClientStats) == 0 {
for _, client := range clients {
s.AddClientStat(tx, inbound.Id, &client)
}
}
} else {
return inbound, false, err
}
needRestart := false
@ -263,7 +267,18 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
tag := oldInbound.Tag
err = s.updateClientTraffics(oldInbound, inbound)
db := database.GetDB()
tx := db.Begin()
defer func() {
if err != nil {
tx.Rollback()
} else {
tx.Commit()
}
}()
err = s.updateClientTraffics(tx, oldInbound, inbound)
if err != nil {
return inbound, false, err
}
@ -304,11 +319,10 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
}
s.xrayApi.Close()
db := database.GetDB()
return inbound, needRestart, db.Save(oldInbound).Error
return inbound, needRestart, tx.Save(oldInbound).Error
}
func (s *InboundService) updateClientTraffics(oldInbound *model.Inbound, newInbound *model.Inbound) error {
func (s *InboundService) updateClientTraffics(tx *gorm.DB, oldInbound *model.Inbound, newInbound *model.Inbound) error {
oldClients, err := s.GetClients(oldInbound)
if err != nil {
return err
@ -318,17 +332,6 @@ func (s *InboundService) updateClientTraffics(oldInbound *model.Inbound, newInbo
return err
}
db := database.GetDB()
tx := db.Begin()
defer func() {
if err != nil {
tx.Rollback()
} else {
tx.Commit()
}
}()
var emailExists bool
for _, oldClient := range oldClients {
@ -601,7 +604,7 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
if len(clients[0].Email) > 0 {
if len(oldEmail) > 0 {
err = s.UpdateClientStat(oldEmail, &clients[0])
err = s.UpdateClientStat(tx, oldEmail, &clients[0])
if err != nil {
return false, err
}
@ -676,6 +679,13 @@ func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraff
return err, false
}
needRestart0, count, err := s.autoRenewClients(tx)
if err != nil {
logger.Warning("Error in renew clients:", err)
} else if count > 0 {
logger.Debugf("%v clients renewed", count)
}
needRestart1, count, err := s.disableInvalidClients(tx)
if err != nil {
logger.Warning("Error in disabling invalid clients:", err)
@ -689,7 +699,7 @@ func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraff
} else if count > 0 {
logger.Debugf("%v inbounds disabled", count)
}
return nil, (needRestart1 || needRestart2)
return nil, (needRestart0 || needRestart1 || needRestart2)
}
func (s *InboundService) addInboundTraffic(tx *gorm.DB, traffics []*xray.Traffic) error {
@ -716,9 +726,15 @@ func (s *InboundService) addInboundTraffic(tx *gorm.DB, traffics []*xray.Traffic
func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTraffic) (err error) {
if len(traffics) == 0 {
// Empty onlineUsers
if p != nil {
p.SetOnlineClients(nil)
}
return nil
}
var onlineClients []string
emails := make([]string, 0, len(traffics))
for _, traffic := range traffics {
emails = append(emails, traffic.Email)
@ -744,11 +760,19 @@ func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTr
if dbClientTraffics[dbTraffic_index].Email == traffics[traffic_index].Email {
dbClientTraffics[dbTraffic_index].Up += traffics[traffic_index].Up
dbClientTraffics[dbTraffic_index].Down += traffics[traffic_index].Down
// Add user in onlineUsers array on traffic
if traffics[traffic_index].Up+traffics[traffic_index].Down > 0 {
onlineClients = append(onlineClients, traffics[traffic_index].Email)
}
break
}
}
}
// Set onlineUsers
p.SetOnlineClients(onlineClients)
err = tx.Save(dbClientTraffics).Error
if err != nil {
logger.Warning("AddClientTraffic update data ", err)
@ -809,6 +833,102 @@ func (s *InboundService) adjustTraffics(tx *gorm.DB, dbClientTraffics []*xray.Cl
return dbClientTraffics, nil
}
func (s *InboundService) autoRenewClients(tx *gorm.DB) (bool, int64, error) {
// check for time expired
var traffics []*xray.ClientTraffic
now := time.Now().Unix() * 1000
var err, err1 error
err = tx.Model(xray.ClientTraffic{}).Where("reset > 0 and expiry_time > 0 and expiry_time <= ?", now).Find(&traffics).Error
if err != nil {
return false, 0, err
}
// return if there is no client to renew
if len(traffics) == 0 {
return false, 0, nil
}
var inbound_ids []int
var inbounds []*model.Inbound
needRestart := false
var clientsToAdd []struct {
protocol string
tag string
client map[string]interface{}
}
for _, traffic := range traffics {
inbound_ids = append(inbound_ids, traffic.InboundId)
}
err = tx.Model(model.Inbound{}).Where("id IN ?", inbound_ids).Find(&inbounds).Error
if err != nil {
return false, 0, err
}
for inbound_index := range inbounds {
settings := map[string]interface{}{}
json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)
clients := settings["clients"].([]interface{})
for client_index := range clients {
c := clients[client_index].(map[string]interface{})
for traffic_index, traffic := range traffics {
if traffic.Email == c["email"].(string) {
newExpiryTime := traffic.ExpiryTime
for newExpiryTime < now {
newExpiryTime += (int64(traffic.Reset) * 86400000)
}
c["expiryTime"] = newExpiryTime
traffics[traffic_index].ExpiryTime = newExpiryTime
traffics[traffic_index].Down = 0
traffics[traffic_index].Up = 0
if !traffic.Enable {
traffics[traffic_index].Enable = true
clientsToAdd = append(clientsToAdd,
struct {
protocol string
tag string
client map[string]interface{}
}{
protocol: string(inbounds[inbound_index].Protocol),
tag: inbounds[inbound_index].Tag,
client: c,
})
}
clients[client_index] = interface{}(c)
break
}
}
}
settings["clients"] = clients
newSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return false, 0, err
}
inbounds[inbound_index].Settings = string(newSettings)
}
err = tx.Save(inbounds).Error
if err != nil {
return false, 0, err
}
err = tx.Save(traffics).Error
if err != nil {
return false, 0, err
}
if p != nil {
err1 = s.xrayApi.Init(p.GetAPIPort())
if err1 != nil {
return true, int64(len(traffics)), nil
}
for _, clientToAdd := range clientsToAdd {
err1 = s.xrayApi.AddUser(clientToAdd.protocol, clientToAdd.tag, clientToAdd.client)
if err1 != nil {
needRestart = true
}
}
s.xrayApi.Close()
}
return needRestart, int64(len(traffics)), nil
}
func (s *InboundService) disableInvalidInbounds(tx *gorm.DB) (bool, int64, error) {
now := time.Now().Unix() * 1000
needRestart := false
@ -881,6 +1001,17 @@ func (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, error)
return needRestart, count, err
}
func (s *InboundService) GetInboundTags() (string, error) {
db := database.GetDB()
var inboundTags []string
err := db.Model(model.Inbound{}).Select("tag").Find(&inboundTags).Error
if err != nil && err != gorm.ErrRecordNotFound {
return "", err
}
tags, _ := json.Marshal(inboundTags)
return string(tags), nil
}
func (s *InboundService) MigrationRemoveOrphanedTraffics() {
db := database.GetDB()
db.Exec(`
@ -902,6 +1033,7 @@ func (s *InboundService) AddClientStat(tx *gorm.DB, inboundId int, client *model
clientTraffic.Enable = true
clientTraffic.Up = 0
clientTraffic.Down = 0
clientTraffic.Reset = client.Reset
result := tx.Create(&clientTraffic)
err := result.Error
if err != nil {
@ -910,16 +1042,15 @@ func (s *InboundService) AddClientStat(tx *gorm.DB, inboundId int, client *model
return nil
}
func (s *InboundService) UpdateClientStat(email string, client *model.Client) error {
db := database.GetDB()
result := db.Model(xray.ClientTraffic{}).
func (s *InboundService) UpdateClientStat(tx *gorm.DB, email string, client *model.Client) error {
result := tx.Model(xray.ClientTraffic{}).
Where("email = ?", email).
Updates(map[string]interface{}{
"enable": true,
"email": client.Email,
"total": client.TotalGB,
"expiry_time": client.ExpiryTime})
"expiry_time": client.ExpiryTime,
"reset": client.Reset})
err := result.Error
if err != nil {
return err
@ -1415,7 +1546,7 @@ func (s *InboundService) DelDepletedClients(id int) (err error) {
}
}()
whereText := "inbound_id "
whereText := "reset = 0 and inbound_id "
if id < 0 {
whereText += "> ?"
} else {
@ -1669,9 +1800,53 @@ func (s *InboundService) MigrationRequirements() {
// Remove orphaned traffics
tx.Where("inbound_id = 0").Delete(xray.ClientTraffic{})
// Migrate old MultiDomain to External Proxy
var externalProxy []struct {
Id int
Port int
StreamSettings []byte
}
err = tx.Raw(`select id, port, stream_settings
from inbounds
WHERE protocol in ('vmess','vless','trojan')
AND json_extract(stream_settings, '$.security') = 'tls'
AND json_extract(stream_settings, '$.tlsSettings.settings.domains') IS NOT NULL`).Scan(&externalProxy).Error
if err != nil || len(externalProxy) == 0 {
return
}
for _, ep := range externalProxy {
var reverses interface{}
var stream map[string]interface{}
json.Unmarshal(ep.StreamSettings, &stream)
if tlsSettings, ok := stream["tlsSettings"].(map[string]interface{}); ok {
if settings, ok := tlsSettings["settings"].(map[string]interface{}); ok {
if domains, ok := settings["domains"].([]interface{}); ok {
for _, domain := range domains {
if domainMap, ok := domain.(map[string]interface{}); ok {
domainMap["forceTls"] = "same"
domainMap["port"] = ep.Port
domainMap["dest"] = domainMap["domain"].(string)
delete(domainMap, "domain")
}
}
}
reverses = settings["domains"]
delete(settings, "domains")
}
}
stream["externalProxy"] = reverses
newStream, _ := json.MarshalIndent(stream, " ", " ")
tx.Model(model.Inbound{}).Where("id = ?", ep.Id).Update("stream_settings", newStream)
}
}
func (s *InboundService) MigrateDB() {
s.MigrationRequirements()
s.MigrationRemoveOrphanedTraffics()
}
func (s *InboundService) GetOnlineClinets() []string {
return p.GetOnlineClients()
}

View file

@ -230,7 +230,7 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
status.AppStats.Mem = rtm.Sys
status.AppStats.Threads = uint32(runtime.NumGoroutine())
if p.IsRunning() {
if p != nil && p.IsRunning() {
status.AppStats.Uptime = p.GetUptime()
} else {
status.AppStats.Uptime = 0
@ -380,14 +380,6 @@ func (s *ServerService) UpdateXray(version string) error {
if err != nil {
return err
}
err = copyZipFile("geosite.dat", xray.GetGeositePath())
if err != nil {
return err
}
err = copyZipFile("geoip.dat", xray.GetGeoipPath())
if err != nil {
return err
}
return nil
@ -435,6 +427,11 @@ func (s *ServerService) GetConfigJson() (interface{}, error) {
}
func (s *ServerService) GetDb() ([]byte, error) {
// Update by manually trigger a checkpoint operation
err := database.Checkpoint()
if err != nil {
return nil, err
}
// Open the file for reading
file, err := os.Open(config.GetDBPath())
if err != nil {

View file

@ -31,8 +31,10 @@ var defaultValueMap = map[string]string{
"secret": random.Seq(32),
"webBasePath": "/",
"sessionMaxAge": "0",
"pageSize": "0",
"expireDiff": "0",
"trafficDiff": "0",
"remarkModel": "-ieo",
"timeLocation": "Asia/Tehran",
"tgBotEnable": "false",
"tgBotToken": "",
@ -53,6 +55,7 @@ var defaultValueMap = map[string]string{
"subUpdates": "12",
"subEncrypt": "true",
"subShowInfo": "true",
"subURI": "",
}
type SettingService struct {
@ -70,7 +73,7 @@ func (s *SettingService) GetDefaultJsonConfig() (interface{}, error) {
func (s *SettingService) GetAllSetting() (*entity.AllSetting, error) {
db := database.GetDB()
settings := make([]*model.Setting, 0)
err := db.Model(model.Setting{}).Find(&settings).Error
err := db.Model(model.Setting{}).Not("key = ?", "xrayTemplateConfig").Find(&settings).Error
if err != nil {
return nil, err
}
@ -309,6 +312,10 @@ func (s *SettingService) GetSessionMaxAge() (int, error) {
return s.getInt("sessionMaxAge")
}
func (s *SettingService) GetRemarkModel() (string, error) {
return s.getString("remarkModel")
}
func (s *SettingService) GetSecretStatus() (bool, error) {
return s.getBool("secretEnable")
}
@ -406,6 +413,14 @@ func (s *SettingService) GetSubShowInfo() (bool, error) {
return s.getBool("subShowInfo")
}
func (s *SettingService) GetSubURI() (string, error) {
return s.getString("subURI")
}
func (s *SettingService) GetPageSize() (int, error) {
return s.getInt("pageSize")
}
func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
if err := allSetting.CheckValid(); err != nil {
return err
@ -426,3 +441,71 @@ func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
}
return common.Combine(errs...)
}
func (s *SettingService) GetDefaultXrayConfig() (interface{}, error) {
var jsonData interface{}
err := json.Unmarshal([]byte(xrayTemplateConfig), &jsonData)
if err != nil {
return nil, err
}
return jsonData, nil
}
func (s *SettingService) GetDefaultSettings(host string) (interface{}, error) {
type settingFunc func() (interface{}, error)
settings := map[string]settingFunc{
"expireDiff": func() (interface{}, error) { return s.GetExpireDiff() },
"trafficDiff": func() (interface{}, error) { return s.GetTrafficDiff() },
"pageSize": func() (interface{}, error) { return s.GetPageSize() },
"defaultCert": func() (interface{}, error) { return s.GetCertFile() },
"defaultKey": func() (interface{}, error) { return s.GetKeyFile() },
"tgBotEnable": func() (interface{}, error) { return s.GetTgbotenabled() },
"subEnable": func() (interface{}, error) { return s.GetSubEnable() },
"subURI": func() (interface{}, error) { return s.GetSubURI() },
"remarkModel": func() (interface{}, error) { return s.GetRemarkModel() },
}
result := make(map[string]interface{})
for key, fn := range settings {
value, err := fn()
if err != nil {
return "", err
}
result[key] = value
}
if result["subEnable"].(bool) && result["subURI"].(string) == "" {
subURI := ""
subPort, _ := s.GetSubPort()
subPath, _ := s.GetSubPath()
subDomain, _ := s.GetSubDomain()
subKeyFile, _ := s.GetSubKeyFile()
subCertFile, _ := s.GetSubCertFile()
subTLS := false
if subKeyFile != "" && subCertFile != "" {
subTLS = true
}
if subDomain == "" {
subDomain = strings.Split(host, ":")[0]
}
if subTLS {
subURI = "https://"
} else {
subURI = "http://"
}
if (subPort == 443 && subTLS) || (subPort == 80 && !subTLS) {
subURI += subDomain
} else {
subURI += fmt.Sprintf("%s:%d", subDomain, subPort)
}
if subPath[0] == byte('/') {
subURI += subPath
} else {
subURI += "/" + subPath
}
result["subURI"] = subURI
}
return result, nil
}

View file

@ -9,6 +9,7 @@ import (
"strings"
"time"
"x-ui/config"
"x-ui/database"
"x-ui/database/model"
"x-ui/logger"
"x-ui/util/common"
@ -1417,6 +1418,12 @@ func (t *Tgbot) sendBackup(chatId int64) {
output := t.I18nBot("tgbot.messages.backupTime", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
t.SendMsgToTgbot(chatId, output)
// Update by manually trigger a checkpoint operation
err := database.Checkpoint()
if err != nil {
logger.Warning("Error in trigger a checkpoint operation: ", err)
}
file, err := os.Open(config.GetDBPath())
if err != nil {
logger.Warning("Error in opening db file for backup: ", err)

View file

@ -134,6 +134,32 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
inbound.Settings = string(modifiedSettings)
}
if len(inbound.StreamSettings) > 0 {
// Unmarshal stream JSON
var stream map[string]interface{}
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
// Remove the "settings" field under "tlsSettings" and "realitySettings"
tlsSettings, ok1 := stream["tlsSettings"].(map[string]interface{})
realitySettings, ok2 := stream["realitySettings"].(map[string]interface{})
if ok1 || ok2 {
if ok1 {
delete(tlsSettings, "settings")
} else if ok2 {
delete(realitySettings, "settings")
}
}
delete(stream, "externalProxy")
newStream, err := json.MarshalIndent(stream, "", " ")
if err != nil {
return nil, err
}
inbound.StreamSettings = string(newStream)
}
inboundConfig := inbound.GenXrayInboundConfig()
xrayConfig.InboundConfigs = append(xrayConfig.InboundConfigs, *inboundConfig)
}

View file

@ -0,0 +1,28 @@
package service
import (
_ "embed"
"encoding/json"
"x-ui/util/common"
"x-ui/xray"
)
type XraySettingService struct {
SettingService
}
func (s *XraySettingService) SaveXraySetting(newXraySettings string) error {
if err := s.CheckXrayConfig(newXraySettings); err != nil {
return err
}
return s.SettingService.saveSetting("xrayTemplateConfig", newXraySettings)
}
func (s *XraySettingService) CheckXrayConfig(XrayTemplateConfig string) error {
xrayConfig := &xray.Config{}
err := json.Unmarshal([]byte(XrayTemplateConfig), xrayConfig)
if err != nil {
return common.NewError("xray template config invalid:", err)
}
return nil
}

View file

@ -12,7 +12,7 @@
"protocol" = "Protocol"
"search" = "Search"
"filter" = "Filter"
"loading" = "Loading"
"loading" = "Loading..."
"second" = "Second"
"minute" = "Minute"
"hour" = "Hour"
@ -37,7 +37,9 @@
"enabled" = "Enabled"
"disabled" = "Disabled"
"depleted" = "Depleted"
"depletingSoon" = "Depleting soon"
"depletingSoon" = "Depleting"
"offline" = "Offline"
"online" = "Online"
"domainName" = "Domain name"
"monitor" = "Listening IP"
"certificate" = "Certificate"
@ -49,11 +51,13 @@
"usage" = "Usage"
"secretToken" = "Secret Token"
"remained" = "Remained"
"security" = "Security"
[menu]
"dashboard" = "System Status"
"inbounds" = "Inbounds"
"settings" = "Panel Settings"
"xray" = "Xray Settings"
"logout" = "Logout"
"link" = "Other"
@ -121,6 +125,8 @@
"modifyInbound" = "Modify Inbound"
"deleteInbound" = "Delete Inbound"
"deleteInboundContent" = "Confirm deletion of inbound?"
"deleteClient" = "Delete Client"
"deleteClientContent" = "Are you sure you want to delete client?"
"resetTrafficContent" = "Confirm traffic reset?"
"copyLink" = "Copy Link"
"address" = "Address"
@ -132,8 +138,8 @@
"totalFlow" = "Total Flow"
"leaveBlankToNeverExpire" = "Leave Blank to Never Expire"
"noRecommendKeepDefault" = "No special requirements to maintain default settings"
"certificatePath" = "Certificate File Path"
"certificateContent" = "Certificate File Content"
"certificatePath" = "File Path"
"certificateContent" = "File Content"
"publicKeyPath" = "Public Key Path"
"publicKeyContent" = "Public Key Content"
"keyPath" = "Private Key Path"
@ -169,6 +175,12 @@
"realityDesc" = "Xray core needs to be 1.8.0 or higher."
"telegramDesc" = "use Telegram ID without @ or chat IDs ( you can get it here @userinfobot or use '/id' command in bot )"
"subscriptionDesc" = "you can find your sub link on Details, also you can use the same name for several configurations"
"info" = "Info"
"same" = "Same"
"inboundData" = "Inbound's data"
"copyToClipboard" = "Copy to clipboard"
"import" = "Import"
"importInbound" = "Import an inbound"
[pages.client]
"add" = "Add Client"
@ -185,6 +197,8 @@
"delayedStart" = "Start after first use"
"expireDays" = "Expire days"
"days" = "day(s)"
"renew" = "Auto renew"
"renewDesc" = "Auto renew days after expiration. 0 = disable"
[pages.inbounds.toasts]
"obtain" = "Obtain"
@ -216,7 +230,6 @@
"resetDefaultConfig" = "Reset to Default Configuration"
"panelSettings" = "Panel Settings"
"securitySettings" = "Security Settings"
"xrayConfiguration" = "Xray Configuration"
"TGBotSettings" = "Telegram Bot Settings"
"panelListeningIP" = "Panel Listening IP"
"panelListeningIPDesc" = "Leave blank by default to monitor all IPs."
@ -230,6 +243,10 @@
"privateKeyPathDesc" = "Fill in an absolute path starting with."
"panelUrlPath" = "Panel URL Root Path"
"panelUrlPathDesc" = "Must start with '/' and end with."
"pageSize" = "Pagination size"
"pageSizeDesc" = "Define page size for inbounds table. Set 0 to disable"
"remarkModel" = "Remark Model and Seperation charachter"
"sampleRemark" = "Sample remark"
"oldUsername" = "Current Username"
"currentPassword" = "Current Password"
"newUsername" = "New Username"
@ -276,13 +293,16 @@
"subEncrypt" = "Encrypt configs"
"subEncryptDesc" = "Encrypt the returned configs in subscription"
"subShowInfo" = "Show usage info"
"subShowInfoDesc" = "Show remianed traffic and date after config name"
"subShowInfoDesc" = "Show remained traffic and date after config name"
"subURI" = "Reverse Proxy URI"
"subURIDesc" = "Change base URI of subscription URL for using on behind of proxies"
[pages.settings.templates]
"title" = "Templates"
[pages.xray]
"title" = "Xray Settings"
"save" = "Save Settings"
"restart" = "Restart Xray"
"basicTemplate" = "Basic Template"
"advancedTemplate" = "Advanced Template"
"completeTemplate" = "Complete Template"
"generalConfigs" = "General Configs"
"generalConfigsDesc" = "These options will provide general adjustments."
"blockConfigs" = "Blocking Configs"
@ -355,14 +375,40 @@
"xrayConfigOutboundsDesc" = "Change the configuration template to define outgoing ways for this server."
"xrayConfigRoutings" = "Configuration of routing rules."
"xrayConfigRoutingsDesc" = "Change the configuration template to define routing rules for this server."
"manualLists" = "Manual Lists"
"manualListsDesc" = "Please use the JSON array format."
"manualBlockedIPs" = "List of Blocked IPs"
"manualBlockedDomains" = "List of Blocked Domains"
"manualDirectIPs" = "List of Direct IPs"
"manualDirectDomains" = "List of Direct Domains"
"manualIPv4Domains" = "List of IPv4 Domains"
"manualWARPDomains" = "List of WARP Domains"
"completeTemplate" = "All"
"Inbounds" = "Inbounds"
"Outbounds" = "Outbounds"
"Routings" = "Routing rules"
"RoutingsDesc" = "The priority of each rule is important!"
[pages.xray.rules]
"first" = "First"
"last" = "Last"
"up" = "Up"
"down" = "Down"
"source" = "Source"
"dest" = "Destination"
"inbound" = "Inbound"
"outbound" = "Outbound"
"info" = "Info"
"add" = "Add Rule"
"edit" = "Edit Rule"
"useComma" = "Comma separated items"
[pages.xray.outbound]
"addOutbound" = "Add outbound"
"addReverse" = "Add reverse"
"editOutbound" = "Edit outbound"
"editReverse" = "Edit reverse"
"tag" = "Tag"
"tagDesc" = "Unique tag"
"address" = "Address"
"reverse" = "Reverse"
"domain" = "Domain"
"type" = "Type"
"bridge" = "Bridge"
"portal" = "Portal"
"intercon" = "Interconnection"
[pages.settings.security]
"admin" = "Admin"
@ -406,7 +452,7 @@
"usage" = "❗ Please provide a text to search!"
"getID" = "🆔 Your ID: <code>{{ .ID }}</code>"
"helpAdminCommands" = "Search for a client email:\r\n<code>/usage [Email]</code>\r\n \r\nSearch for inbounds (with client stats):\r\n<code>/inbound [Remark]</code>"
"helpClientCommands" = "To search for statistics, just use folowing command:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\nUse UUID for vmess/vless and Password for Trojan."
"helpClientCommands" = "To search for statistics, just use the following command:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\nUse UUID for vmess/vless and Password for Trojan."
[tgbot.messages]
"cpuThreshold" = "🔴 The CPU usage {{ .Percent }}% is more than threshold {{ .Threshold }}%"

View file

@ -12,7 +12,7 @@
"protocol" = "Protocolo"
"search" = "Buscar"
"filter" = "Filtrar"
"loading" = "Cargando"
"loading" = "Cargando..."
"second" = "Segundo"
"minute" = "Minuto"
"hour" = "Hora"
@ -37,7 +37,9 @@
"enabled" = "Habilitado"
"disabled" = "Deshabilitado"
"depleted" = "Agotado"
"depletingSoon" = "Agotándose pronto"
"depletingSoon" = "Agotándose"
"offline" = "fuera de línea"
"online" = "en línea"
"domainName" = "Nombre de dominio"
"monitor" = "Listening IP"
"certificate" = "Certificado"
@ -49,11 +51,13 @@
"usage" = "Uso"
"secretToken" = "Token Secreto"
"remained" = "Restante"
"security" = "Seguridad"
[menu]
"dashboard" = "Estado del Sistema"
"inbounds" = "Entradas"
"settings" = "Configuraciones"
"xray" = "Configuración Xray"
"logout" = "Cerrar Sesión"
"link" = "Otro"
@ -121,6 +125,8 @@
"modifyInbound" = "Modificar Entrada"
"deleteInbound" = "Eliminar Entrada"
"deleteInboundContent" = "¿Confirmar eliminación de entrada?"
"deleteClient" = "Eliminar cliente"
"deleteClientContent" = "¿Está seguro de que desea eliminar el cliente?"
"resetTrafficContent" = "¿Confirmar restablecimiento de tráfico?"
"copyLink" = "Copiar Enlace"
"address" = "Dirección"
@ -132,8 +138,8 @@
"totalFlow" = "Flujo Total"
"leaveBlankToNeverExpire" = "Dejar en Blanco para Nunca Expirar"
"noRecommendKeepDefault" = "No hay requisitos especiales para mantener la configuración predeterminada"
"certificatePath" = "Ruta del Archivo de Certificado"
"certificateContent" = "Contenido del Archivo de Certificado"
"certificatePath" = "Ruta del Archivo"
"certificateContent" = "Contenido del Archivo"
"publicKeyPath" = "Ruta de la Clave Pública"
"publicKeyContent" = "Contenido de la Clave Pública"
"keyPath" = "Ruta de la Clave Privada"
@ -169,6 +175,12 @@
"realityDesc" = "La versión del núcleo de Xray debe ser 1.8.0 o superior."
"telegramDesc" = "Utiliza el ID de Telegram sin @ o los IDs de chat (puedes obtenerlo aquí @userinfobot o usando el comando '/id' en el bot)."
"subscriptionDesc" = "Puedes encontrar tu enlace de suscripción en Detalles, también puedes usar el mismo nombre para varias configuraciones."
"info" = "Info"
"same" = "misma"
"inboundData" = "Datos de entrada"
"copyToClipboard" = "Copiar al portapapeles"
"import" = "Importar"
"importInbound" = "Importar un entrante"
[pages.client]
"add" = "Agregar Cliente"
@ -185,6 +197,8 @@
"delayedStart" = "Iniciar después del primer uso"
"expireDays" = "Días de Expiración"
"days" = "día(s)"
"renew" = "Renovación automática"
"renewDesc" = "Renovación automática días después del vencimiento. 0 = deshabilitar"
[pages.inbounds.toasts]
"obtain" = "Recibir"
@ -216,7 +230,6 @@
"resetDefaultConfig" = "Restablecer a Configuración Predeterminada"
"panelSettings" = "Configuraciones del Panel"
"securitySettings" = "Configuraciones de Seguridad"
"xrayConfiguration" = "Configuración de Xray"
"TGBotSettings" = "Configuraciones de Bot de Telegram"
"panelListeningIP" = "IP de Escucha del Panel"
"panelListeningIPDesc" = "Dejar en blanco por defecto para monitorear todas las IPs."
@ -230,6 +243,10 @@
"privateKeyPathDesc" = "Complete con una ruta absoluta que comience con."
"panelUrlPath" = "Ruta Raíz de la URL del Panel"
"panelUrlPathDesc" = "Debe empezar con '/' y terminar con."
"pageSize" = "Tamaño de paginación"
"pageSizeDesc" = "Defina el tamaño de página para la tabla de entradas. Establezca 0 para desactivar"
"remarkModel" = "Modelo de observación y carácter de separación"
"sampleRemark" = "Observación de muestra"
"oldUsername" = "Nombre de Usuario Actual"
"currentPassword" = "Contraseña Actual"
"newUsername" = "Nuevo Nombre de Usuario"
@ -277,12 +294,15 @@
"subEncryptDesc" = "Encriptar las configuraciones devueltas en la suscripción."
"subShowInfo" = "Mostrar información de uso"
"subShowInfoDesc" = "Mostrar tráfico restante y fecha después del nombre de configuración."
"subURI" = "URI de proxy inverso"
"subURIDesc" = "Cambiar el URI base de la URL de suscripción para usar detrás de los servidores proxy"
[pages.settings.templates]
"title" = "Plantillas"
[pages.xray]
"title" = "Xray Configuración"
"save" = "Guardar configuración"
"restart" = "Reiniciar Xray"
"basicTemplate" = "Plantilla Básica"
"advancedTemplate" = "Plantilla Avanzada"
"completeTemplate" = "Plantilla Completa"
"generalConfigs" = "Configuraciones Generales"
"generalConfigsDesc" = "Estas opciones proporcionarán ajustes generales."
"blockConfigs" = "Configuraciones de Bloqueo"
@ -355,14 +375,40 @@
"xrayConfigOutboundsDesc" = "Cambia la plantilla de configuración para definir formas de salida para este servidor."
"xrayConfigRoutings" = "Configuración de Reglas de Enrutamiento"
"xrayConfigRoutingsDesc" = "Cambia la plantilla de configuración para definir reglas de enrutamiento para este servidor."
"manualLists" = "Listas Manuales"
"manualListsDesc" = "Por favor, utilice el formato de matriz JSON."
"manualBlockedIPs" = "Lista de IPs Bloqueadas"
"manualBlockedDomains" = "Lista de Dominios Bloqueados"
"manualDirectIPs" = "Lista de IPs Directas"
"manualDirectDomains" = "Lista de Dominios Directos"
"manualIPv4Domains" = "Lista de Dominios IPv4"
"manualWARPDomains" = "Lista de Dominios de WARP"
"completeTemplate" = "Todos"
"Inbounds" = "Entrante"
"Outbounds" = "Salidas"
"Routings" = "Reglas de enrutamiento"
"RoutingsDesc" = "¡La prioridad de cada regla es importante!"
[pages.xray.rules]
"first" = "Primero"
"last" = "Último"
"up" = "arriba"
"down" = "abajo"
"source" = "Fuente"
"dest" = "Destino"
"inbound" = "Entrante"
"outbound" = "saliente"
"info" = "Información"
"add" = "Agregar regla"
"edit" = "Editar regla"
"useComma" = "Elementos separados por comas"
[pages.xray.outbound]
"addOutbound" = "Agregar salida"
"addReverse" = "Agregar reverso"
"editOutbound" = "Editar salida"
"editReverse" = "Editar reverso"
"tag" = "Etiqueta"
"tagDesc" = "etiqueta única"
"address" = "Dirección"
"reverse" = "Reverso"
"domain" = "Dominio"
"type" = "Tipo"
"bridge" = "puente"
"portal" = "portal"
"intercon" = "Interconexión"
[pages.settings.security]
"admin" = "Administrador"

View file

@ -38,6 +38,8 @@
"disabled" = "غیرفعال"
"depleted" = "منقضی"
"depletingSoon" = "در حال انقضا"
"offline" = "آفلاین"
"online" = "آنلاین"
"domainName" = "آدرس دامنه"
"monitor" = "آی پی اتصال"
"certificate" = "گواهی دیجیتال"
@ -49,11 +51,13 @@
"usage" = "استفاده"
"secretToken" = "توکن امنیتی"
"remained" = "باقیمانده"
"security" = "امنیت"
[menu]
"dashboard" = "وضعیت سیستم"
"inbounds" = "سرویس ها"
"settings" = "تنظیمات پنل"
"xray" = "الگوی ایکس‌ری"
"logout" = "خروج"
"link" = "دیگر"
@ -121,6 +125,8 @@
"modifyInbound" = "ویرایش سرویس"
"deleteInbound" = "حذف سرویس"
"deleteInboundContent" = "آیا مطمئن به حذف سرویس هستید ؟"
"deleteClient" = "حذف کاربر"
"deleteClientContent" = "آیا مطمئن به حذف کاربر هستید ؟"
"resetTrafficContent" = "آیا مطمئن به ریست ترافیک هستید ؟"
"copyLink" = "کپی لینک"
"address" = "آدرس"
@ -169,6 +175,12 @@
"realityDesc" = "هسته Xray باید 1.8.0 و بالاتر باشد"
"telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot یا در ربات دستور '/id' را وارد کنید)"
"subscriptionDesc" = "می توانید ساب لینک خود را در جزئیات پیدا کنید، همچنین می توانید از همین نام برای چندین کانفیگ استفاده کنید"
"info" = "اطلاعات"
"same" = "همسان"
"inboundData" = "داده‌های سرویس"
"copyToClipboard" = "کپی در حافظه"
"import" = "وارد کردن"
"importInbound" = "وارد کردن یک سرویس"
[pages.client]
"add" = "کاربر جدید"
@ -185,6 +197,8 @@
"delayedStart" = "شروع بعد از اولین استفاده"
"expireDays" = "روزهای اعتبار"
"days" = "(روز)"
"renew" = "تمدید خودکار"
"renewDesc" = "روزهای تمدید خودکار پس از انقضا. 0 = غیرفعال"
[pages.inbounds.toasts]
"obtain" = "Obtain"
@ -216,7 +230,6 @@
"resetDefaultConfig" = "برگشت به تنظیمات پیشفرض"
"panelSettings" = "تنظیمات پنل"
"securitySettings" = "تنظیمات امنیتی"
"xrayConfiguration" = "تنظیمات Xray"
"TGBotSettings" = "تنظیمات ربات تلگرام"
"panelListeningIP" = "محدودیت آی پی پنل"
"panelListeningIPDesc" = "برای استفاده از تمام آی‌پیها به طور پیش فرض خالی بگذارید"
@ -230,6 +243,10 @@
"privateKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود "
"panelUrlPath" = "آدرس روت پنل"
"panelUrlPathDesc" = "باید با '/' شروع شود و با '/' تمام شود"
"pageSize" = "اندازه صفحه بندی جدول"
"pageSizeDesc" = "اندازه صفحه را برای جدول سرویس ها تعریف کنید. 0: غیرفعال"
"remarkModel" = "نام کانفیگ و جداکننده"
"sampleRemark" = "نمونه نام"
"oldUsername" = "نام کاربری فعلی"
"currentPassword" = "رمز عبور فعلی"
"newUsername" = "نام کاربری جدید"
@ -277,12 +294,15 @@
"subEncryptDesc" = "رمزگذاری کانفیگ های بازگشتی سابسکریپشن"
"subShowInfo" = "نمایش اطلاعات مصرف"
"subShowInfoDesc" = "ترافیک و زمان باقیمانده را در هر کانفیگ نمایش میدهد"
"subURI" = "آدرس پایه پروکسی معکوس"
"subURIDesc" = "آدرس پایه سابسکریپشن را برای استفاده در پشت پراکسی ها تغییر میدهد"
[pages.settings.templates]
[pages.xray]
"title" = "الگوها"
"save" = "ذخیره تنظیمات"
"restart" = "ریستارت ایکس‌ری"
"basicTemplate" = "بخش الگو پایه"
"advancedTemplate" = "بخش الگو پیشرفته"
"completeTemplate" = "بخش الگو کامل"
"generalConfigs" = "تنظیمات عمومی"
"generalConfigsDesc" = "این تنظیمات میتواند ترافیک کلی سرویس را متاثر کند"
"blockConfigs" = "مسدود سازی"
@ -355,14 +375,40 @@
"xrayConfigOutboundsDesc" = "میتوانید الگوی تنظیمات را برای خروجی اینترنت تنظیم نمایید"
"xrayConfigRoutings" = "تنظیمات قوانین مسیریابی"
"xrayConfigRoutingsDesc" = "میتوانید الگوی تنظیمات را برای مسیریابی تنظیم نمایید"
"manualLists" = "لیست های دستی"
"manualListsDesc" = "فرمت: JSON Array"
"manualBlockedIPs" = "لیست آی‌پی های مسدود شده"
"manualBlockedDomains" = "لیست دامنه های مسدود شده"
"manualDirectIPs" = "لیست آی‌پی های مستقیم"
"manualDirectDomains" = "لیست دامنه های مستقیم"
"manualIPv4Domains" = "لیست دامنه‌های IPv4"
"manualWARPDomains" = "لیست دامنه های WARP"
"completeTemplate" = "کامل"
"Inbounds" = "ورودی‌ها"
"Outbounds" = "خروجی‌ها"
"Routings" = "قوانین مسیریابی"
"RoutingsDesc" = "اولویت هر قانون مهم است!"
[pages.xray.rules]
"first" = "اولین"
"last" = "آخرین"
"up" = "بالا"
"down" = "پایین"
"source" = "مبدا"
"dest" = "مقصد"
"inbound" = "ورودی"
"outbound" = "خروجی"
"info" = "اطلاعات"
"add" = "افزودن قانون"
"edit" = "ویرایش قانون"
"useComma" = "موارد جدا شده با کاما"
[pages.xray.outbound]
"addOutbound" = "افزودن خروجی"
"addReverse" = "افزودن معکوس"
"editOutbound" = "ویرایش خروجی"
"editReverse" = "ویرایش معکوس"
"tag" = "برچسب"
"tagDesc" = "برچسب یگانه"
"address" = "آدرس"
"reverse" = "معکوس"
"domain" = "دامنه"
"type" = "نوع"
"bridge" = "پل"
"portal" = "پرتال"
"intercon" = "اتصال میانی"
[pages.settings.security]
"admin" = "مدیر"
@ -370,7 +416,7 @@
"loginSecurity" = "لاگین ایمن"
"loginSecurityDesc" = "افزودن یک مرحله دیگر به فرآیند لاگین"
"secretToken" = "توکن امنیتی"
"secretTokenDesc" = "این کد امنیتی را نزد خود در این جای امن نگه داری، بدون این کد امکان ورود به پنل را نخواهید داشت. امکان بازیابی آن وجود ندارد!"
"secretTokenDesc" = "این کد امنیتی را نزد خود در این جای امن نگه دارید، بدون این کد امکان ورود به پنل را نخواهید داشت. امکان بازیابی آن وجود ندارد!"
[pages.settings.toasts]
"modifySettings" = "ویرایش تنظیمات"

View file

@ -12,7 +12,7 @@
"protocol" = "Протокол"
"search" = "Поиск"
"filter" = "Фильтр"
"loading" = "Загрузка"
"loading" = "Загрузка..."
"second" = "Секунда"
"minute" = "Минута"
"hour" = "Час"
@ -38,6 +38,8 @@
"disabled" = "Отключено"
"depleted" = "Исчерпано"
"depletingSoon" = "Почти исчерпано"
"offline" = "Офлайн"
"online" = "Онлайн"
"domainName" = "Домен"
"monitor" = "Порт IP"
"certificate" = "Сертификат"
@ -49,11 +51,13 @@
"usage" = "Использование"
"secretToken" = "Секретный токен"
"remained" = "остались"
"security" = "Безопасность"
[menu]
"dashboard" = "Статус системы"
"inbounds" = "Подключения"
"settings" = "Настройки панели"
"xray" = "Xray Настройки"
"logout" = "Выход"
"link" = "Прочее"
@ -121,6 +125,8 @@
"modifyInbound" = "Изменить подключение"
"deleteInbound" = "Удалить подключение"
"deleteInboundContent" = "Подтвердите удаление подключения?"
"deleteClient" = "Удалить клиента"
"deleteClientContent" = "Вы уверены, что хотите удалить клиента?"
"resetTrafficContent" = "Подтвердите сброс трафика?"
"copyLink" = "Копировать ключ"
"address" = "Адрес"
@ -132,8 +138,8 @@
"totalFlow" = "Общий расход"
"leaveBlankToNeverExpire" = "Оставьте пустым, чтобы не истекало"
"noRecommendKeepDefault" = "Нет требований для сохранения настроек по умолчанию"
"certificatePath" = "Путь файла сертификата"
"certificateContent" = "Содержимое файла сертификата"
"certificatePath" = "Путь файла"
"certificateContent" = "Содержимое файла"
"publicKeyPath" = "Путь к публичному ключу"
"publicKeyContent" = "Содержимое публичного ключа"
"keyPath" = "Путь к приватному ключу"
@ -169,6 +175,12 @@
"realityDesc" = "Версия Xray должна быть не ниже 1.8.0"
"telegramDesc" = "Используйте идентификатор Telegram без символа @ или идентификатора чата (можно получить его здесь @userinfobot или использовать команду '/id' в боте)"
"subscriptionDesc" = "Вы можете найти свою ссылку подписки в разделе 'Подробнее', также вы можете использовать одно и то же имя для нескольких конфигураций"
"info" = "Информация"
"same" = "Тот же"
"inboundData" = "Входящие данные"
"copyToClipboard" = "Копировать в буфер обмена"
"import" = "Импортировать"
"importInbound" = "Импортировать входящее сообщение"
[pages.client]
"add" = "Добавить пользователя"
@ -185,6 +197,8 @@
"delayedStart" = "Начать с момента первого подключения"
"expireDays" = "Срок действия"
"days" = "дней"
"renew" = "Автопродление"
"renewDesc" = "Автоматическое продление через несколько дней после истечения срока действия. 0 = отключить"
[pages.inbounds.toasts]
"obtain" = "Получить"
@ -216,7 +230,6 @@
"resetDefaultConfig" = "Сбросить на конфигурацию по умолчанию"
"panelSettings" = "Настройки панели"
"securitySettings" = "Настройки безопасности"
"xrayConfiguration" = "Конфигурация Xray"
"TGBotSettings" = "Настройки Telegram бота"
"panelListeningIP" = "IP-адрес панели"
"panelListeningIPDesc" = "Оставьте пустым для подключения с любого IP"
@ -225,11 +238,15 @@
"panelPort" = "Порт панели"
"panelPortDesc" = "Порт, используемый для отображения этой панели"
"publicKeyPath" = "Путь к файлу публичного ключа сертификата панели"
"publicKeyPathDesc" = "Введите полный путь, начинающийся с "
"publicKeyPathDesc" = "Введите полный путь, начинающийся с"
"privateKeyPath" = "Путь к файлу приватного ключа сертификата панели"
"privateKeyPathDesc" = "Введите полный путь, начинающийся с "
"privateKeyPathDesc" = "Введите полный путь, начинающийся с"
"panelUrlPath" = "Корневой путь URL адреса панели"
"panelUrlPathDesc" = "Должен начинаться с '/' и заканчиваться на "
"panelUrlPathDesc" = "Должен начинаться с '/' и заканчиваться на"
"pageSize" = "Размер нумерации страниц"
"pageSizeDesc" = "Определить размер страницы для входящей таблицы. Установите 0, чтобы отключить"
"remarkModel" = "Модель примечания и символ разделения"
"sampleRemark" = "Пример замечания"
"oldUsername" = "Текущее имя пользователя"
"currentPassword" = "Текущий пароль"
"newUsername" = "Новое имя пользователя"
@ -277,12 +294,15 @@
"subEncryptDesc" = "Шифровать возвращенные конфиги в подписке"
"subShowInfo" = "Показать информацию об использовании"
"subShowInfoDesc" = "Показывать восстановленный трафик и дату после имени конфигурации"
"subURI" = "URI обратного прокси"
"subURIDesc" = "Изменить базовый URI URL-адреса подписки для использования за прокси-серверами"
[pages.settings.templates]
"title" = "Шаблоны"
[pages.xray]
"title" = "Xray Настройки"
"save" = "Сохранить настройки"
"restart" = "Перезапустить рентген"
"basicTemplate" = "Базовый шаблон"
"advancedTemplate" = "Расширенный шаблон"
"completeTemplate" = "Полный шаблон"
"generalConfigs" = "Основные настройки"
"generalConfigsDesc" = "Эти параметры описывают общие настройки"
"blockConfigs" = "Блокировка конфигураций"
@ -355,14 +375,40 @@
"xrayConfigOutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие пути для этого сервера"
"xrayConfigRoutings" = "Настройка правил маршрутизации"
"xrayConfigRoutingsDesc" = "Изменение шаблона конфигурации для определения правил маршрутизации для этого сервера"
"manualLists" = "Ручные списки"
"manualListsDesc" = "Пожалуйста, используйте формат массива JSON"
"manualBlockedIPs" = "Список заблокированных IP-адресов"
"manualBlockedDomains" = "Список заблокированных доменов"
"manualDirectIPs" = "Список прямых IP-адресов"
"manualDirectDomains" = "Список прямых доменов"
"manualIPv4Domains" = "Список доменов IPv4"
"manualWARPDomains" = "Список доменов WARP"
"completeTemplate" = "Все"
"Inbounds" = "Входящие"
"Outbounds" = "Исходящие"
"Routings" = "Правила маршрутизации"
"RoutingsDesc" = "Важен приоритет каждого правила!"
[pages.xray.rules]
"first" = "Первый"
"last" = "Последний"
"up" = "Вверх"
"down" = "Вниз"
"source" = "Источник"
"dest" = "Пункт назначения"
"inbound" = "Входящий"
"outboun" = "Исходящий"
"info" = "Информация"
"add" = "Добавить правило"
"edit" = "Редактировать правило"
"useComma" = "Элементы, разделенные запятыми"
[pages.xray.outbound]
"addOutbound" = "Добавить исходящий"
"addReverse" = "Добавить реверс"
"editOutbound" = "Изменить исходящий"
"editReverse" = "Редактировать реверс"
"tag" = "Тег"
"tagDesc" = "уникальный тег"
"address" = "Адрес"
"reverse" = "Обратный"
"domain" = "Домен"
"type" = "Тип"
"bridge" = "Мост"
"portal" = "Портал"
"intercon" = "Соединение"
[pages.settings.security]
"admin" = "Админ"

View file

@ -1,6 +1,6 @@
"username" = "Tên người dùng"
"password" = "Mật khẩu"
"login" = "Đăng nhập"
"login" = "Đăng nhập..."
"confirm" = "Xác nhận"
"cancel" = "Hủy bỏ"
"close" = "Đóng"
@ -37,7 +37,9 @@
"enabled" = "Đã kích hoạt"
"disabled" = "Đã tắt"
"depleted" = "Đã cạn kiệt"
"depletingSoon" = "Sắp cạn kiệt"
"depletingSoon" = "Đang cạn kiệt"
"offline" = "Ngoại tuyến"
"online" = "Ngoại tuyến"
"domainName" = "Tên miền"
"monitor" = "Listening IP"
"certificate" = "Chứng chỉ"
@ -49,12 +51,14 @@
"usage" = "Sử dụng"
"secretToken" = "secretToken"
"remained" = "Còn lại"
"security" = "Bảo vệ"
[menu]
"dashboard" = "Trạng thái hệ thống"
"inbounds" = "Inbounds"
"settings" = "Cài đặt bảng điều khiển"
"logout" = "Đăng xuất"
"xray" = "Xray Cài đặt"
"link" = "Khác"
[pages.login]
@ -121,6 +125,8 @@
"modifyInbound" = "Chỉnh sửa điểm vào (Inbound)"
"deleteInbound" = "Xóa điểm vào (Inbound)"
"deleteInboundContent" = "Xác nhận xóa điểm vào? (Inbound)"
"deleteClient" = "Xóa khách hàng"
"deleteClientContent" = "Bạn có chắc chắn muốn xóa ứng dụng khách không?"
"resetTrafficContent" = "Xác nhận đặt lại lưu lượng?"
"copyLink" = "Sao chép liên kết"
"address" = "Địa chỉ"
@ -132,8 +138,8 @@
"totalFlow" = "Tổng lưu lượng"
"leaveBlankToNeverExpire" = "Để trống để không bao giờ hết hạn"
"noRecommendKeepDefault" = "Không yêu cầu đặc biệt để giữ nguyên cài đặt mặc định"
"certificatePath" = "Đường dẫn tập tin chứng chỉ"
"certificateContent" = "Nội dung tập tin chứng chỉ"
"certificatePath" = "Đường dẫn tập"
"certificateContent" = "Nội dung tập"
"publicKeyPath" = "Đường dẫn khóa công khai"
"publicKeyContent" = "Nội dung khóa công khai"
"keyPath" = "Đường dẫn khóa riêng tư"
@ -169,6 +175,12 @@
"realityDesc" = "Xray core cần phiên bản 1.8.0 hoặc cao hơn."
"telegramDesc" = "Sử dụng Telegram ID mà không cần ký hiệu @ hoặc chat IDs (bạn có thể nhận được nó ở đây @userinfobot hoặc sử dụng lệnh '/id' trong bot)"
"subscriptionDesc" = "Bạn có thể tìm liên kết đăng ký của mình trong Chi tiết, cũng như bạn có thể sử dụng cùng tên cho nhiều cấu hình khác nhau"
"info" = "Thông tin"
"same" = "Giống nhau"
"inboundData" = "Dữ liệu gửi đến"
"copyToClipboard" = "Sao chép vào bảng nhớ tạm"
"import" = "Nhập"
"importInbound" = "Nhập hàng gửi về"
[pages.client]
"add" = "Thêm Client"
@ -185,6 +197,8 @@
"delayedStart" = "Bắt đầu sau khi sử dụng lần đầu"
"expireDays" = "Số ngày hết hạn"
"days" = "ngày"
"renew" = "Tự động gia hạn"
"renewDesc" = "Tự động gia hạn những ngày sau khi hết hạn. 0 = tắt"
[pages.inbounds.toasts]
"obtain" = "Nhận"
@ -216,7 +230,6 @@
"resetDefaultConfig" = "Đặt lại Cấu hình Mặc định"
"panelSettings" = "Cài đặt Bảng điều khiển"
"securitySettings" = "Cài đặt Bảo mật"
"xrayConfiguration" = "Cấu hình Xray"
"TGBotSettings" = "Cài đặt Bot Telegram"
"panelListeningIP" = "IP Nghe của Bảng điều khiển"
"panelListeningIPDesc" = "Mặc định để trống để nghe tất cả các IP."
@ -230,6 +243,10 @@
"privateKeyPathDesc" = "Điền vào đường dẫn tuyệt đối bắt đầu với."
"panelUrlPath" = "Đường dẫn gốc URL Bảng điều khiển"
"panelUrlPathDesc" = "Phải bắt đầu bằng '/' và kết thúc bằng."
"pageSize" = "Kích thước phân trang"
"pageSizeDesc" = "Xác định kích thước trang cho bảng gửi đến. Đặt 0 để tắt"
"remarkModel" = "Ghi chú mô hình và ký tự phân tách"
"sampleRemark" = "Nhận xét mẫu"
"oldUsername" = "Tên người dùng hiện tại"
"currentPassword" = "Mật khẩu hiện tại"
"newUsername" = "Tên người dùng mới"
@ -277,12 +294,15 @@
"subEncryptDesc" = "Mã hóa các cấu hình được trả về trong đăng ký"
"subShowInfo" = "Hiển thị thông tin sử dụng"
"subShowInfoDesc" = "Hiển thị lưu lượng truy cập còn lại và ngày sau tên cấu hình"
"subURI" = "URI proxy ngược"
"subURIDesc" = "Thay đổi URI cơ sở của URL đăng ký để sử dụng ở phía sau proxy"
[pages.settings.templates]
"title" = "Mẫu"
[pages.xray]
"title" = "Xray Cài đặt"
"save" = "Lưu cài đặt"
"restart" = "Khởi động lại Xray"
"basicTemplate" = "Mẫu Cơ bản"
"advancedTemplate" = "Mẫu Nâng cao"
"completeTemplate" = "Mẫu Đầy đủ"
"generalConfigs" = "Cấu hình Chung"
"generalConfigsDesc" = "Những tùy chọn này sẽ cung cấp điều chỉnh tổng quát."
"blockConfigs" = "Cấu hình Chặn"
@ -355,14 +375,40 @@
"xrayConfigOutboundsDesc" = "Thay đổi mẫu cấu hình để xác định các cách ra đi cho máy chủ này."
"xrayConfigRoutings" = "Cấu hình của Luật Định tuyến."
"xrayConfigRoutingsDesc" = "Thay đổi mẫu cấu hình để xác định luật định tuyến cho máy chủ này."
"manualLists" = "Danh sách Thủ công"
"manualListsDesc" = "Vui lòng sử dụng định dạng mảng JSON."
"manualBlockedIPs" = "Danh sách IP bị Chặn"
"manualBlockedDomains" = "Danh sách Tên miền bị Chặn"
"manualDirectIPs" = "Danh sách IP Trực tiếp"
"manualDirectDomains" = "Danh sách Tên miền Trực tiếp"
"manualIPv4Domains" = "Danh sách Tên miền IPv4"
"manualWARPDomains" = "Danh sách Tên miền WARP"
"completeTemplate" = "All"
"Inbounds" = "Vào"
"Outbounds" = "Outbounds"
"Routings" = "Quy tắc định tuyến"
"RoutingsDesc" = "Mức độ ưu tiên của mỗi quy tắc đều quan trọng!"
[pages.xray.rules]
"first" = "Đầu tiên"
"last" = "Cuối cùng"
"up" = "Lên"
"down" = "Xuống"
"source" = "Nguồn"
"dest" = "Đích"
"inbound" = "Vào"
"outbound" = "Ra ngoài"
"info" = "Thông tin"
"add" = "Thêm quy tắc"
"edit" = "Chỉnh sửa quy tắc"
"useComma" = "Các mục được phân tách bằng dấu phẩy"
[pages.xray.outbound]
"addOutbound" = "Thêm thư đi"
"addReverse" = "Thêm đảo ngược"
"editOutbound" = "Chỉnh sửa gửi đi"
"editReverse" = "Chỉnh sửa ngược lại"
"tag" = "Thẻ"
"tagDesc" = "thẻ duy nhất"
"address" = "Địa chỉ"
"reverse" = "Đảo ngược"
"domain" = "Miền"
"type" = "Loại"
"bridge" = "Cầu"
"portal" = "Cổng thông tin"
"intercon" = "Kết nối"
[pages.settings.security]
"admin" = "Quản trị viên"

View file

@ -12,7 +12,7 @@
"protocol" = "协议"
"search" = "搜尋"
"filter" = "过滤器"
"loading" = "加载中"
"loading" = "加载中..."
"second" = "秒"
"minute" = "分钟"
"hour" = "小时"
@ -38,6 +38,8 @@
"disabled" = "关闭"
"depleted" = "耗尽"
"depletingSoon" = "即将耗尽"
"offline" = "离线"
"online" = "在线"
"domainName" = "域名"
"monitor" = "监听"
"certificate" = "证书"
@ -49,11 +51,13 @@
"usage" = "用法"
"secretToken" = "秘密令牌"
"remained" = "仍然存在"
"security" = "安全"
[menu]
"dashboard" = "系统状态"
"inbounds" = "入站列表"
"settings" = "面板设置"
"xray" = "Xray 设置"
"logout" = "退出登录"
"link" = "其他"
@ -121,6 +125,8 @@
"modifyInbound" = "修改入站"
"deleteInbound" = "删除入站"
"deleteInboundContent" = "确定要删除入站吗?"
"deleteClient" = "删除客户端"
"deleteClientContent" = "您确定要删除客户端吗?"
"resetTrafficContent" = "确定要重置流量吗?"
"copyLink" = "复制链接"
"address" = "地址"
@ -132,8 +138,8 @@
"totalFlow" = "总流量"
"leaveBlankToNeverExpire" = "留空则永不到期"
"noRecommendKeepDefault" = "没有特殊需求保持默认即可"
"certificatePath" = "证书文件路径"
"certificateContent" = "证书文件内容"
"certificatePath" = "文件路径"
"certificateContent" = "文件内容"
"publicKeyPath" = "公钥文件路径"
"publicKeyContent" = "公钥内容"
"keyPath" = "密钥文件路径"
@ -169,6 +175,12 @@
"realityDesc" = "Xray核心需要1.8.0及以上版本"
"telegramDesc" = "使用 Telegram ID不包含 @ 符号或聊天 ID可以在 @userinfobot 处获取,或在机器人中使用'/id'命令)"
"subscriptionDesc" = "您可以在详细信息上找到您的子链接,也可以对多个配置使用相同的名称"
"info" = "信息"
"same" = "相同"
"inboundData" = "入站数据"
"copyToClipboard" = "复制到剪贴板"
"import"="导入"
"importInbound" = "导入入站"
[pages.client]
"add" = "添加客户端"
@ -185,6 +197,8 @@
"delayedStart" = "首次使用后开始"
"expireDays" = "过期天数"
"days" = "天"
"renew" = "自动续订"
"renewDesc" = "过期后自动续订。0 = 禁用"
[pages.inbounds.toasts]
"obtain" = "获取"
@ -216,7 +230,6 @@
"resetDefaultConfig" = "重置为默认配置"
"panelSettings" = "面板配置"
"securitySettings" = "安全设定"
"xrayConfiguration" = "xray 相关设置"
"TGBotSettings" = "TG提醒相关设置"
"panelListeningIP" = "面板监听 IP"
"panelListeningIPDesc" = "默认留空监听所有 IP"
@ -230,6 +243,10 @@
"privateKeyPathDesc" = "填写一个 '/' 开头的绝对路径"
"panelUrlPath" = "面板 url 根路径"
"panelUrlPathDesc" = "必须以 '/' 开头,以 '/' 结尾"
"pageSize" = "分页大小"
"pageSizeDesc" = "定义入站表的页面大小。设置 0 表示禁用"
"remarkModel" = "备注模型和分隔符"
"sampleRemark" = "备注示例"
"oldUsername" = "原用户名"
"currentPassword" = "原密码"
"newUsername" = "新用户名"
@ -277,12 +294,15 @@
"subEncryptDesc" = "在订阅中加密返回的配置"
"subShowInfo" = "显示使用信息"
"subShowInfoDesc" = "在配置名称后显示剩余流量和日期"
"subURI" = "反向代理 URI"
"subURIDesc" = "更改订阅 URL 的基本 URI 以在代理后面使用"
[pages.settings.templates]
"title" = "模板"
[pages.xray]
"title" = "Xray 设置"
"save" = "保存设置"
"restart" = "重新启动 Xray"
"basicTemplate" = "基本模板"
"advancedTemplate" = "高级模板部件"
"completeTemplate" = "Xray 配置的完整模板"
"generalConfigs" = "通用配置"
"generalConfigsDesc" = "这些选项将提供一般调整"
"blockConfigs" = "阻塞配置"
@ -355,14 +375,40 @@
"xrayConfigOutboundsDesc" = "更改配置模板定义此服务器的传出方式"
"xrayConfigRoutings" = "路由规则配置"
"xrayConfigRoutingsDesc" = "更改配置模板为该服务器定义路由规则"
"manualLists" = "手动列表"
"manualListsDesc" = "请使用 JSON 数组格式"
"manualBlockedIPs" = "被阻止的 IP 列表"
"manualBlockedDomains" = "被阻止的域列表"
"manualDirectIPs" = "直接 IP 列表"
"manualDirectDomains" = "直接域列表"
"manualIPv4Domains" = "IPv4 域名列表"
"manualWARPDomains" = "WARP域名列表"
"completeTemplate" = "全部"
"Inbounds" = "界内"
"Outbounds" = "出站"
"Routings" = "路由规则"
"RoutingsDesc" = "每条规则的优先级都很重要"
[pages.xray.rules]
"firsto" = "第一个"
"last" = "最后"
"up" = "向上"
"down" = "向下"
"source" = "来源"
"dest" = "目的地"
"inbound" = "入站"
"outbound" = "出站"
"info" = "信息"
"add" = "添加规则"
"edit" = "编辑规则"
"useComma" = "逗号分隔的项目"
[pages.xray.outbound]
"addOutbound" = "添加出站"
"addReverse" = "添加反向"
"editOutbound" = "编辑出站"
"editReverse" = "编辑反向"
"tag" = "标签"
"tagDesc" = "独特的标签"
"address" = "地址"
"rreverse" = "反转"
"domain" = "域名"
"type" = "类型"
"bridge" = "桥"
"portal" = "门户"
"intercon" = "互连"
[pages.settings.security]
"admin" = "管理员"

View file

@ -25,6 +25,7 @@ import (
sessions "github.com/Calidity/gin-sessions"
"github.com/Calidity/gin-sessions/cookie"
"github.com/gin-contrib/gzip"
"github.com/gin-gonic/gin"
"github.com/robfig/cron/v3"
)
@ -174,6 +175,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
if err != nil {
return nil, err
}
engine.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedPaths([]string{basePath + "panel/API/"})))
assetsBasePath := basePath + "assets/"
store := cookie.NewStore(secret)

Some files were not shown because too many files have changed in this diff Show more