Explorar o código

更新大量功能

sprov %!s(int64=4) %!d(string=hai) anos
pai
achega
a66dff6959
Modificáronse 41 ficheiros con 1227 adicións e 1005 borrados
  1. 0 0
      a.s
  2. 9 2
      go.mod
  3. 4 261
      go.sum
  4. 50 4
      main.go
  5. 0 0
      web/assets/[email protected]/antd-with-locales.min.js
  6. 0 0
      web/assets/[email protected]/antd.min.js
  7. 47 2
      web/assets/js/model/models.js
  8. 184 51
      web/assets/js/model/xray.js
  9. 7 2
      web/controller/base.go
  10. 5 1
      web/controller/index.go
  11. 19 41
      web/controller/server.go
  12. 52 2
      web/controller/xui.go
  13. 23 0
      web/global/global.go
  14. 0 0
      web/html/login.html
  15. 0 535
      web/html/x-ui/inbound_modal.html
  16. 0 0
      web/html/xui/common_sider.html
  17. 81 0
      web/html/xui/form/inbound.html
  18. 17 0
      web/html/xui/form/protocol/dokodemo.html
  19. 10 0
      web/html/xui/form/protocol/http.html
  20. 19 0
      web/html/xui/form/protocol/shadowsocks.html
  21. 23 0
      web/html/xui/form/protocol/socks.html
  22. 7 0
      web/html/xui/form/protocol/trojan.html
  23. 49 0
      web/html/xui/form/protocol/vless.html
  24. 13 0
      web/html/xui/form/protocol/vmess.html
  25. 16 0
      web/html/xui/form/sniffing.html
  26. 7 0
      web/html/xui/form/stream/stream_grpc.html
  27. 12 0
      web/html/xui/form/stream/stream_http.html
  28. 38 0
      web/html/xui/form/stream/stream_kcp.html
  29. 24 0
      web/html/xui/form/stream/stream_quic.html
  30. 45 0
      web/html/xui/form/stream/stream_settings.html
  31. 83 0
      web/html/xui/form/stream/stream_tcp.html
  32. 28 0
      web/html/xui/form/stream/stream_ws.html
  33. 45 0
      web/html/xui/form/tls_settings.html
  34. 79 0
      web/html/xui/inbound_modal.html
  35. 56 54
      web/html/xui/inbounds.html
  36. 0 0
      web/html/xui/index.html
  37. 29 0
      web/service/inbound.go
  38. 6 1
      web/service/server.go
  39. 15 0
      web/service/setting.go
  40. 5 0
      web/session/session.go
  41. 120 49
      web/web.go

+ 0 - 0
a.s


+ 9 - 2
go.mod

@@ -9,15 +9,22 @@ require (
 	github.com/gin-contrib/sessions v0.0.3
 	github.com/gin-gonic/gin v1.7.1
 	github.com/go-ole/go-ole v1.2.5 // indirect
+	github.com/golang/protobuf v1.5.2 // indirect
+	github.com/kr/pretty v0.1.0 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.1 // indirect
 	github.com/nicksnyder/go-i18n/v2 v2.1.2
 	github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
-	github.com/pelletier/go-toml v1.9.1 // indirect
+	github.com/robfig/cron/v3 v3.0.1
 	github.com/shirou/gopsutil v3.21.3+incompatible
 	github.com/tklauser/go-sysconf v0.3.5 // indirect
-	github.com/xtls/xray-core v1.4.2
+	go.uber.org/atomic v1.7.0
+	golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect
 	golang.org/x/sys v0.0.0-20210511113859-b0526f3d8744 // indirect
 	golang.org/x/text v0.3.6
+	gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
+	gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c // indirect
 	gorm.io/driver/sqlite v1.1.4
 	gorm.io/gorm v1.21.9
 )

+ 4 - 261
go.sum

@@ -1,51 +1,15 @@
-cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
-dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
-dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
-dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
-dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
-git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46 h1:5sXbqlSomvdjlRbWyNqkPsJ3Fg+tQZCbgeX1VGljbQY=
 github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
 github.com/Workiva/go-datastructures v1.0.53 h1:J6Y/52yX10Xc5JjXmGtWoSSxs3mZnGSaq37xZZh7Yig=
 github.com/Workiva/go-datastructures v1.0.53/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A=
-github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
-github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
-github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
 github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
 github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
-github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
-github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
-github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
-github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
-github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
-github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
-github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
-github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
-github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165 h1:BS21ZUJ/B5X2UVUbczfmdWH7GapPWAhxcMsDnjJTU1E=
-github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
-github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
-github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
-github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
-github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
-github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 h1:Mn26/9ZMNWSw9C9ERFA1PUxfmGpolnw2v0bKOREu5ew=
-github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
 github.com/gin-contrib/sessions v0.0.3 h1:PoBXki+44XdJdlgDqDrY5nDVe3Wk7wDV/UCOuLP6fBI=
 github.com/gin-contrib/sessions v0.0.3/go.mod h1:8C/J6cad3Il1mWYYgtw0w+hqasmpvy25mPkXdOgeB9I=
 github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
@@ -53,9 +17,7 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm
 github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
 github.com/gin-gonic/gin v1.7.1 h1:qC89GU3p8TvKWMAVhEpmpB2CIb1hnqt2UdKZaP93mS8=
 github.com/gin-gonic/gin v1.7.1/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
-github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
 github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
-github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
 github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY=
 github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
 github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
@@ -68,47 +30,15 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87
 github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
 github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
 github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
-github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
-github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
-github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
-github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g=
-github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
-github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
-github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
-github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
-github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
-github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
-github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
-github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
-github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
-github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
-github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
-github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
-github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
-github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
-github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
 github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
 github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
@@ -116,128 +46,52 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+
 github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
 github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU=
 github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
-github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
-github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
-github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
-github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364/go.mod h1:eDJQioIyy4Yn3MVivT7rv/39gAJTrA7lgmYr8EW950c=
-github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
 github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
 github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
 github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
 github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI=
 github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
-github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
 github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
-github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
 github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
 github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
-github.com/lucas-clemente/quic-go v0.20.0 h1:FSU3YN5VnLafHR27Ejs1r1CYMS7XMyIVDzRewkDLNBw=
-github.com/lucas-clemente/quic-go v0.20.0/go.mod h1:fZq/HUDIM+mW6X6wtzORjC0E/WDBMKe5Hf9bgjISwLk=
-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/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc=
-github.com/marten-seemann/qtls-go1-15 v0.1.4 h1:RehYMOyRW8hPVEja1KBVsFVNSm35Jj9Mvs5yNoZZ28A=
-github.com/marten-seemann/qtls-go1-15 v0.1.4/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I=
-github.com/marten-seemann/qtls-go1-16 v0.1.3 h1:XEZ1xGorVy9u+lJq+WXNE+hiqRYLNvJGYmwfwKQN2gU=
-github.com/marten-seemann/qtls-go1-16 v0.1.3/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk=
 github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
 github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
 github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
 github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ=
 github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
-github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
-github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
-github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
-github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
 github.com/nicksnyder/go-i18n/v2 v2.1.2 h1:QHYxcUJnGHBaq7XbvgunmZ2Pn0focXFqTD61CkH146c=
 github.com/nicksnyder/go-i18n/v2 v2.1.2/go.mod h1:d++QJC9ZVf7pa48qrsRWhMJ5pSHIPmS3OLqK1niyLxs=
-github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
-github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
-github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
-github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
-github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
-github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
-github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
-github.com/pelletier/go-toml v1.9.1 h1:a6qW1EVNZWH9WGI6CsYdD8WAylkoXBS5yv0XHlh17Tc=
-github.com/pelletier/go-toml v1.9.1/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
-github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
 github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
-github.com/pires/go-proxyproto v0.5.0 h1:A4Jv4ZCaV3AFJeGh5mGwkz4iuWUYMlQ7IoO/GTuSuLo=
-github.com/pires/go-proxyproto v0.5.0/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
-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=
-github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
-github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
-github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
-github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
-github.com/refraction-networking/utls v0.0.0-20201210053706-2179f286686b h1:lzo71oHzQEz0fKMSjR0BpVzuh2hOHvJTxnN3Rnikmtg=
-github.com/refraction-networking/utls v0.0.0-20201210053706-2179f286686b/go.mod h1:tz9gX959MEFfFN5whTIocCLUG57WiILqtdVxI8c6Wj0=
-github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
-github.com/seiflotfy/cuckoofilter v0.0.0-20201222105146-bc6005554a0c h1:pqy40B3MQWYrza7YZXOXgl0Nf0QGFqrOC0BKae1UNAA=
-github.com/seiflotfy/cuckoofilter v0.0.0-20201222105146-bc6005554a0c/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
-github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
+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/shirou/gopsutil v3.21.3+incompatible h1:uenXGGa8ESCQq+dbgtl916dmg6PSAz2cXov0uORQ9v8=
 github.com/shirou/gopsutil v3.21.3+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
-github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
-github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
-github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
-github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
-github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
-github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
-github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
-github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
-github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
-github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
-github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
-github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
-github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
-github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
-github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
-github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
-github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
-github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
-github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
-github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
-github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
-github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
-github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
-github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-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.5 h1:uu3Xl4nkLzQfXNsWn15rPc/HQCJKObbt1dKJeWp3vU4=
 github.com/tklauser/go-sysconf v0.3.5/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI=
@@ -248,103 +102,38 @@ github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
 github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
 github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
 github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
-github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
-github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
-github.com/xtls/go v0.0.0-20201118062508-3632bf3b7499 h1:QHESTXtfgc1ABV+ArlbPVqUx9Ht5I0dDkYhxYoXFxNo=
-github.com/xtls/go v0.0.0-20201118062508-3632bf3b7499/go.mod h1:5TB2+k58gx4A4g2Nf5miSHNDF6CuAzHKpWBooLAshTs=
-github.com/xtls/xray-core v1.4.2 h1:D0Le+Qy9L/eY5LbUQfrk7WJ8wbODpQSW/ZRCg+BRe7c=
-github.com/xtls/xray-core v1.4.2/go.mod h1:DmL/9rOCliev/a6HciWEvSJVEhUF6C0EpD3clW8v0pc=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
-go.starlark.net v0.0.0-20210312235212-74c10e2c17dc h1:pVkptfeOTFfx+zXZo7HEHN3d5LmhatBFvHdm/f2QnpY=
-go.starlark.net v0.0.0-20210312235212-74c10e2c17dc/go.mod h1:t3mmBBPzAVvK0L0n1drDmrQsJ8FoIx4INCqVMTr/Zo0=
-go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
-golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
-golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
+go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 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-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
 golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
-golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-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/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-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-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210330230544-e57232859fb2 h1:nGCZOty+lVDsc4H2qPFksI5Se296+V+GhMiL/TzmYNk=
-golang.org/x/net v0.0.0-20210330230544-e57232859fb2/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-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-20190911185100-cd5d95a43a6e/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.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-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-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201231184435-2d18734c6014/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210511113859-b0526f3d8744 h1:yhBbb4IRs2HS9PPlAg6DMC6mUOKexJBNsLf4Z+6En1Q=
 golang.org/x/sys v0.0.0-20210511113859-b0526f3d8744/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-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-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -352,53 +141,15 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
-google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
-google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
-google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
-google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
-google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
-google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
-google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
-google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
-google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
-google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
-google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.36.1 h1:cmUfbeGKnz9+2DD/UYsMQXeqbHZqZDs4eQwW0sFOpBY=
-google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
-google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
-google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
-google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
-google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
-google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
 gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
-gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
-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=
-gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
@@ -411,11 +162,3 @@ gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9D
 gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
 gorm.io/gorm v1.21.9 h1:INieZtn4P2Pw6xPJ8MzT0G4WUOsHq3RhfuDF1M6GW0E=
 gorm.io/gorm v1.21.9/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
-grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
-h12.io/socks v1.0.2/go.mod h1:AIhxy1jOId/XCz9BO+EIgNL2rQiPTBNnOfnVnQ+3Eck=
-honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
-sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=

+ 50 - 4
main.go

@@ -1,18 +1,25 @@
 package main
 
 import (
+	"flag"
+	"fmt"
 	"github.com/op/go-logging"
 	"log"
 	"os"
 	"os/signal"
 	"syscall"
+	_ "unsafe"
 	"x-ui/config"
 	"x-ui/database"
 	"x-ui/logger"
 	"x-ui/web"
+	"x-ui/web/global"
 )
 
-func main() {
+// this function call global.setWebServer
+func setWebServer(server global.WebServer)
+
+func runWebServer() {
 	log.Printf("%v %v", config.GetName(), config.GetVersion())
 
 	switch config.GetLogLevel() {
@@ -36,7 +43,11 @@ func main() {
 	var server *web.Server
 
 	server = web.NewServer()
-	go server.Run()
+	setWebServer(server)
+	err = server.Start()
+	if err != nil {
+		panic(err)
+	}
 
 	sigCh := make(chan os.Signal, 1)
 	signal.Notify(sigCh, syscall.SIGHUP)
@@ -46,10 +57,45 @@ func main() {
 		if sig == syscall.SIGHUP {
 			server.Stop()
 			server = web.NewServer()
-			go server.Run()
+			setWebServer(server)
+			err = server.Start()
+			if err != nil {
+				panic(err)
+			}
 		} else {
-			return
+			continue
 		}
 	}
+}
+
+func v2ui(dbPath string) {
+	// migrate from v2-ui
+}
+
+func main() {
+	if len(os.Args) < 2 {
+		runWebServer()
+		return
+	}
+
+	runCmd := flag.NewFlagSet("run", flag.ExitOnError)
 
+	v2uiCmd := flag.NewFlagSet("v2-ui", flag.ExitOnError)
+	var dbPath string
+	v2uiCmd.StringVar(&dbPath, "db", "/etc/v2-ui/v2-ui.db", "set v2-ui db file path")
+
+	switch flag.Arg(0) {
+	case "run":
+		runCmd.Parse(os.Args[2:])
+		runWebServer()
+	case "v2-ui":
+		v2uiCmd.Parse(os.Args[2:])
+		v2ui(dbPath)
+	default:
+		fmt.Println("excepted 'run' or 'v2-ui' subcommands")
+		fmt.Println()
+		runCmd.Usage()
+		fmt.Println()
+		v2uiCmd.Usage()
+	}
 }

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
web/assets/[email protected]/antd-with-locales.min.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
web/assets/[email protected]/antd.min.js


+ 47 - 2
web/assets/js/model/models.js

@@ -26,9 +26,10 @@ class DBInbound {
     userId = 0;
     up = 0;
     down = 0;
-    remark = 0;
-    enable = false;
+    remark = "";
+    enable = true;
     expiryTime = 0;
+
     listen = "";
     port = 0;
     protocol = "";
@@ -43,4 +44,48 @@ class DBInbound {
         }
         ObjectUtil.cloneProps(this, data);
     }
+
+    toInbound() {
+        let settings = {};
+        if (!ObjectUtil.isEmpty(this.settings)) {
+            settings = JSON.parse(this.settings);
+        }
+
+        let streamSettings = {};
+        if (!ObjectUtil.isEmpty(this.streamSettings)) {
+            streamSettings = JSON.parse(this.streamSettings);
+        }
+
+        let sniffing = {};
+        if (!ObjectUtil.isEmpty(this.sniffing)) {
+            sniffing = JSON.parse(this.sniffing);
+        }
+        const config = {
+            port: this.port,
+            listen: this.listen,
+            protocol: this.protocol,
+            settings: settings,
+            streamSettings: streamSettings,
+            tag: this.tag,
+            sniffing: sniffing,
+        };
+        return Inbound.fromJson(config);
+    }
+
+    hasLink() {
+        switch (this.protocol) {
+            case Protocols.VMESS:
+            case Protocols.VLESS:
+            case Protocols.TROJAN:
+            case Protocols.SHADOWSOCKS:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    genLink(address="") {
+        const inbound = this.toInbound();
+        return inbound.genLink(address, this.remark);
+    }
 }

+ 184 - 51
web/assets/js/model/xray.js

@@ -374,6 +374,23 @@ class QuicStreamSettings extends XrayCommonClass {
     }
 }
 
+class GrpcStreamSettings extends XrayCommonClass {
+    constructor(serviceName="") {
+        super();
+        this.serviceName = serviceName;
+    }
+
+    static fromJson(json={}) {
+        return new GrpcStreamSettings(json.serviceName);
+    }
+
+    toJson() {
+        return {
+            serviceName: this.serviceName,
+        }
+    }
+}
+
 class TlsStreamSettings extends XrayCommonClass {
     constructor(serverName='',
                 certificates=[new TlsStreamSettings.Cert()]) {
@@ -459,30 +476,42 @@ class StreamSettings extends XrayCommonClass {
                 wsSettings=new WsStreamSettings(),
                 httpSettings=new HttpStreamSettings(),
                 quicSettings=new QuicStreamSettings(),
+                grpcSettings=new GrpcStreamSettings(),
                 ) {
         super();
         this.network = network;
-        if (security === "xtls") {
-            this.security = "tls";
-            this._is_xtls = true;
-        } else {
-            this.security = security;
-            this._is_xtls = false;
-        }
+        this.security = security;
         this.tls = tlsSettings;
         this.tcp = tcpSettings;
         this.kcp = kcpSettings;
         this.ws = wsSettings;
         this.http = httpSettings;
         this.quic = quicSettings;
+        this.grpc = grpcSettings;
     }
 
-    get is_xtls() {
-        return this.security === "tls" && this.network === "tcp" && this._is_xtls;
+    get isTls() {
+        return this.security === 'tls';
     }
 
-    set is_xtls(is_xtls) {
-        this._is_xtls = is_xtls;
+    set isTls(isTls) {
+        if (isTls) {
+            this.security = 'tls';
+        } else {
+            this.security = 'none';
+        }
+    }
+
+    get isXTls() {
+        return this.security === "xtls";
+    }
+
+    set isXTls(isXTls) {
+        if (isXTls) {
+            this.security = 'xtls';
+        } else {
+            this.security = 'none';
+        }
     }
 
     static fromJson(json={}) {
@@ -501,25 +530,23 @@ class StreamSettings extends XrayCommonClass {
             WsStreamSettings.fromJson(json.wsSettings),
             HttpStreamSettings.fromJson(json.httpSettings),
             QuicStreamSettings.fromJson(json.quicSettings),
+            GrpcStreamSettings.fromJson(json.grpcSettings),
         );
     }
 
     toJson() {
-        let network = this.network;
-        let security = this.security;
-        if (this.is_xtls) {
-            security = "xtls";
-        }
+        const network = this.network;
         return {
             network: network,
-            security: security,
-            tlsSettings: this.security === 'tls' && ['tcp', 'ws', 'http', 'quic'].indexOf(network) >= 0 && !this.is_xtls ? this.tls.toJson() : undefined,
-            xtlsSettings: this.is_xtls ? this.tls.toJson() : undefined,
+            security: this.security,
+            tlsSettings: this.isTls ? this.tls.toJson() : undefined,
+            xtlsSettings: this.isXTls ? this.tls.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,
         };
     }
 }
@@ -547,40 +574,147 @@ class Sniffing extends XrayCommonClass {
 
 class Inbound extends XrayCommonClass {
     constructor(port=RandomUtil.randomIntRange(10000, 60000),
-                listen='0.0.0.0',
+                listen='',
                 protocol=Protocols.VMESS,
                 settings=null,
                 streamSettings=new StreamSettings(),
                 tag='',
                 sniffing=new Sniffing(),
-                remark='',
-                enable=true,
                 ) {
         super();
         this.port = port;
         this.listen = listen;
-        this.protocol = protocol;
+        this._protocol = protocol;
         this.settings = ObjectUtil.isEmpty(settings) ? Inbound.Settings.getSettings(protocol) : settings;
         this.stream = streamSettings;
         this.tag = tag;
         this.sniffing = sniffing;
-        this.remark = remark;
-        this.enable = enable;
+    }
+
+    get protocol() {
+        return this._protocol;
+    }
+
+    set protocol(protocol) {
+        this._protocol = protocol;
+        this.settings = Inbound.Settings.getSettings(protocol);
+        if (protocol === Protocols.TROJAN) {
+            this.tls = true;
+        }
+    }
+
+    get tls() {
+        return this.stream.security === 'tls';
+    }
+
+    set tls(isTls) {
+        if (isTls) {
+            this.stream.security = 'tls';
+        } else {
+            if (this.protocol === Protocols.TROJAN) {
+                this.xtls = true;
+            } else {
+                this.stream.security = 'none';
+            }
+        }
+    }
+
+    get xtls() {
+        return this.stream.security === 'xtls';
+    }
+
+    set xtls(isXTls) {
+        if (isXTls) {
+            this.stream.security = 'xtls';
+        } else {
+            if (this.protocol === Protocols.TROJAN) {
+                this.tls = true;
+            } else {
+                this.stream.security = 'none';
+            }
+        }
+    }
+
+    get network() {
+        return this.stream.network;
+    }
+
+    set network(network) {
+        this.stream.network = network;
+    }
+
+    canEnableTls() {
+        switch (this.protocol) {
+            case Protocols.VMESS:
+            case Protocols.VLESS:
+            case Protocols.TROJAN:
+            case Protocols.SHADOWSOCKS:
+                break;
+            default:
+                return false;
+        }
+
+        switch (this.network) {
+            case "tcp":
+            case "ws":
+            case "http":
+            case "quic":
+            case "grpc":
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    canSetTls() {
+        return this.canEnableTls();
+    }
+
+    canEnableXTls() {
+        switch (this.protocol) {
+            case Protocols.VLESS:
+            case Protocols.TROJAN:
+                break;
+            default:
+                return false;
+        }
+        return this.network === "tcp";
+    }
+
+    canEnableStream() {
+        switch (this.protocol) {
+            case Protocols.VMESS:
+            case Protocols.VLESS:
+            case Protocols.SHADOWSOCKS:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    canSniffing() {
+        switch (this.protocol) {
+            case Protocols.VMESS:
+            case Protocols.VLESS:
+            case Protocols.TROJAN:
+            case Protocols.SHADOWSOCKS:
+                return true;
+            default:
+                return false;
+        }
     }
 
     reset() {
         this.port = RandomUtil.randomIntRange(10000, 60000);
-        this.listen = '0.0.0.0';
+        this.listen = '';
         this.protocol = Protocols.VMESS;
         this.settings = Inbound.Settings.getSettings(Protocols.VMESS);
         this.stream = new StreamSettings();
         this.tag = '';
         this.sniffing = new Sniffing();
-        this.remark = '';
-        this.enable = true;
     }
 
-    genVmessLink(address='') {
+    genVmessLink(address='', remark='') {
         if (this.protocol !== Protocols.VMESS) {
             return '';
         }
@@ -618,6 +752,8 @@ class Inbound extends XrayCommonClass {
             type = this.stream.quic.type;
             host = this.stream.quic.security;
             path = this.stream.quic.key;
+        } else if (network === 'grpc') {
+            path = this.stream.grpc.serviceName;
         }
 
         if (this.stream.security === 'tls') {
@@ -628,7 +764,7 @@ class Inbound extends XrayCommonClass {
 
         let obj = {
             v: '2',
-            ps: this.remark,
+            ps: remark,
             add: address,
             port: this.port,
             id: this.settings.vmesses[0].id,
@@ -642,14 +778,14 @@ class Inbound extends XrayCommonClass {
         return 'vmess://' + base64(JSON.stringify(obj, null, 2));
     }
 
-    genVLESSLink(address = '') {
+    genVLESSLink(address = '', remark='') {
         const settings = this.settings;
         const uuid = settings.vlesses[0].id;
         const port = this.port;
         const type = this.stream.network;
         const params = new Map();
         params.set("type", this.stream.network);
-        if (this.stream.is_xtls) {
+        if (this.isXTls) {
             params.set("security", "xtls");
         } else {
             params.set("security", this.stream.security);
@@ -692,6 +828,10 @@ class Inbound extends XrayCommonClass {
                 params.set("key", quic.key);
                 params.set("headerType", quic.type);
                 break;
+            case "grpc":
+                const grpc = this.stream.grpc;
+                params.set("serviceName", grpc.serviceName);
+                break;
         }
 
         if (this.stream.security === 'tls') {
@@ -701,7 +841,7 @@ class Inbound extends XrayCommonClass {
             }
         }
 
-        if (this.stream.is_xtls) {
+        if (this.isXTls) {
             params.set("flow", this.settings.vlesses[0].flow);
         }
 
@@ -722,31 +862,31 @@ class Inbound extends XrayCommonClass {
         for (const [key, value] of params) {
             url.searchParams.set(key, value)
         }
-        url.hash = encodeURIComponent(this.remark);
+        url.hash = encodeURIComponent(remark);
         return url.toString();
     }
 
-    genSSLink(address='') {
+    genSSLink(address='', remark='') {
         let settings = this.settings;
         const server = this.stream.tls.server;
         if (!ObjectUtil.isEmpty(server)) {
             address = server;
         }
         return 'ss://' + safeBase64(settings.method + ':' + settings.password + '@' + address + ':' + this.port)
-            + '#' + encodeURIComponent(this.remark);
+            + '#' + encodeURIComponent(remark);
     }
 
-    genTrojanLink(address='') {
+    genTrojanLink(address='', remark='') {
         let settings = this.settings;
-        return `trojan://${settings.clients[0].password}@${address}:${this.port}#${encodeURIComponent(this.remark)}`;
+        return `trojan://${settings.clients[0].password}@${address}:${this.port}#${encodeURIComponent(remark)}`;
     }
 
-    genLink(address='') {
+    genLink(address='', remark='') {
         switch (this.protocol) {
-            case Protocols.VMESS: return this.genVmessLink(address);
-            case Protocols.VLESS: return this.genVLESSLink(address);
-            case Protocols.SHADOWSOCKS: return this.genSSLink(address);
-            case Protocols.TROJAN: return this.genTrojanLink(address);
+            case Protocols.VMESS: return this.genVmessLink(address, remark);
+            case Protocols.VLESS: return this.genVLESSLink(address, remark);
+            case Protocols.SHADOWSOCKS: return this.genSSLink(address, remark);
+            case Protocols.TROJAN: return this.genTrojanLink(address, remark);
             default: return '';
         }
     }
@@ -760,17 +900,12 @@ class Inbound extends XrayCommonClass {
             StreamSettings.fromJson(json.streamSettings),
             json.tag,
             Sniffing.fromJson(json.sniffing),
-            json.remark,
-            json.enable,
         )
     }
 
     toJson() {
         let streamSettings;
-        if (this.protocol === Protocols.VMESS
-            || this.protocol === Protocols.VLESS
-            || this.protocol === Protocols.TROJAN
-            || this.protocol === Protocols.SHADOWSOCKS) {
+        if (this.canEnableStream() || this.protocol === Protocols.TROJAN) {
             streamSettings = this.stream.toJson();
         }
         return {
@@ -781,8 +916,6 @@ class Inbound extends XrayCommonClass {
             streamSettings: streamSettings,
             tag: this.tag,
             sniffing: this.sniffing.toJson(),
-            remark: this.remark,
-            enable: this.enable,
         };
     }
 }

+ 7 - 2
web/controller/base.go

@@ -2,15 +2,20 @@ package controller
 
 import (
 	"github.com/gin-gonic/gin"
+	"net/http"
 	"x-ui/web/session"
 )
 
 type BaseController struct {
 }
 
-func (a *BaseController) before(c *gin.Context) {
+func (a *BaseController) checkLogin(c *gin.Context) {
 	if !session.IsLogin(c) {
-		pureJsonMsg(c, false, "登录时效已过,请重新登录")
+		if isAjax(c) {
+			pureJsonMsg(c, false, "登录时效已过,请重新登录")
+		} else {
+			c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
+		}
 		c.Abort()
 	} else {
 		c.Next()

+ 5 - 1
web/controller/index.go

@@ -32,6 +32,10 @@ func (a *IndexController) initRouter(g *gin.RouterGroup) {
 }
 
 func (a *IndexController) index(c *gin.Context) {
+	if session.IsLogin(c) {
+		c.Redirect(http.StatusTemporaryRedirect, "xui/")
+		return
+	}
 	html(c, "login.html", "登录", nil)
 }
 
@@ -68,5 +72,5 @@ func (a *IndexController) logout(c *gin.Context) {
 		logger.Info("user", user.Id, "logout")
 	}
 	session.ClearSession(c)
-	c.Redirect(http.StatusTemporaryRedirect, "")
+	c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
 }

+ 19 - 41
web/controller/server.go

@@ -1,37 +1,17 @@
 package controller
 
 import (
-	"context"
 	"github.com/gin-gonic/gin"
-	"runtime"
 	"time"
+	"x-ui/web/global"
 	"x-ui/web/service"
 )
 
-func stopServerController(a *ServerController) {
-	a.stopTask()
-}
-
 type ServerController struct {
-	*serverController
-}
-
-func NewServerController(g *gin.RouterGroup) *ServerController {
-	a := &ServerController{
-		serverController: newServerController(g),
-	}
-	runtime.SetFinalizer(a, stopServerController)
-	return a
-}
-
-type serverController struct {
 	BaseController
 
 	serverService service.ServerService
 
-	ctx    context.Context
-	cancel context.CancelFunc
-
 	lastStatus        *service.Status
 	lastGetStatusTime time.Time
 
@@ -39,11 +19,8 @@ type serverController struct {
 	lastGetVersionsTime time.Time
 }
 
-func newServerController(g *gin.RouterGroup) *serverController {
-	ctx, cancel := context.WithCancel(context.Background())
-	a := &serverController{
-		ctx:               ctx,
-		cancel:            cancel,
+func NewServerController(g *gin.RouterGroup) *ServerController {
+	a := &ServerController{
 		lastGetStatusTime: time.Now(),
 	}
 	a.initRouter(g)
@@ -51,23 +28,28 @@ func newServerController(g *gin.RouterGroup) *serverController {
 	return a
 }
 
-func (a *serverController) initRouter(g *gin.RouterGroup) {
-	g.POST("/server/status", a.status)
-	g.POST("/server/getXrayVersion", a.getXrayVersion)
-	g.POST("/server/installXray/:version", a.installXray)
+func (a *ServerController) initRouter(g *gin.RouterGroup) {
+	g = g.Group("/server")
+
+	g.Use(a.checkLogin)
+	g.POST("/status", a.status)
+	g.POST("/getXrayVersion", a.getXrayVersion)
+	g.POST("/installXray/:version", a.installXray)
 }
 
-func (a *serverController) refreshStatus() {
+func (a *ServerController) refreshStatus() {
 	status := a.serverService.GetStatus(a.lastStatus)
 	a.lastStatus = status
 }
 
-func (a *serverController) startTask() {
+func (a *ServerController) startTask() {
+	webServer := global.GetWebServer()
+	ctx := webServer.GetCtx()
 	go func() {
 		for {
 			select {
-			case <-a.ctx.Done():
-				break
+			case <-ctx.Done():
+				return
 			default:
 			}
 			now := time.Now()
@@ -80,17 +62,13 @@ func (a *serverController) startTask() {
 	}()
 }
 
-func (a *serverController) stopTask() {
-	a.cancel()
-}
-
-func (a *serverController) status(c *gin.Context) {
+func (a *ServerController) status(c *gin.Context) {
 	a.lastGetStatusTime = time.Now()
 
 	jsonObj(c, a.lastStatus, nil)
 }
 
-func (a *serverController) getXrayVersion(c *gin.Context) {
+func (a *ServerController) getXrayVersion(c *gin.Context) {
 	now := time.Now()
 	if now.Sub(a.lastGetVersionsTime) <= time.Minute {
 		jsonObj(c, a.lastVersions, nil)
@@ -109,7 +87,7 @@ func (a *serverController) getXrayVersion(c *gin.Context) {
 	jsonObj(c, versions, nil)
 }
 
-func (a *serverController) installXray(c *gin.Context) {
+func (a *ServerController) installXray(c *gin.Context) {
 	version := c.Param("version")
 	err := a.serverService.UpdateXray(version)
 	jsonMsg(c, "安装 xray", err)

+ 52 - 2
web/controller/xui.go

@@ -2,9 +2,12 @@ package controller
 
 import (
 	"github.com/gin-gonic/gin"
+	"go.uber.org/atomic"
 	"log"
 	"strconv"
 	"x-ui/database/model"
+	"x-ui/logger"
+	"x-ui/web/global"
 	"x-ui/web/service"
 	"x-ui/web/session"
 )
@@ -13,25 +16,45 @@ type XUIController struct {
 	BaseController
 
 	inboundService service.InboundService
+	xrayService    service.XrayService
+
+	isNeedXrayRestart atomic.Bool
 }
 
 func NewXUIController(g *gin.RouterGroup) *XUIController {
 	a := &XUIController{}
 	a.initRouter(g)
+	a.startTask()
 	return a
 }
 
 func (a *XUIController) initRouter(g *gin.RouterGroup) {
 	g = g.Group("/xui")
+	g.Use(a.checkLogin)
 
 	g.GET("/", a.index)
 	g.GET("/inbounds", a.inbounds)
-	g.POST("/inbounds", a.postInbounds)
+	g.POST("/inbounds", a.getInbounds)
 	g.POST("/inbound/add", a.addInbound)
 	g.POST("/inbound/del/:id", a.delInbound)
+	g.POST("/inbound/update/:id", a.updateInbound)
 	g.GET("/setting", a.setting)
 }
 
+func (a *XUIController) startTask() {
+	webServer := global.GetWebServer()
+	c := webServer.GetCron()
+	c.AddFunc("@every 10s", func() {
+		if a.isNeedXrayRestart.Load() {
+			err := a.xrayService.RestartXray()
+			if err != nil {
+				logger.Error("restart xray failed:", err)
+			}
+			a.isNeedXrayRestart.Store(false)
+		}
+	})
+}
+
 func (a *XUIController) index(c *gin.Context) {
 	html(c, "index.html", "系统状态", nil)
 }
@@ -44,7 +67,7 @@ func (a *XUIController) setting(c *gin.Context) {
 	html(c, "setting.html", "设置", nil)
 }
 
-func (a *XUIController) postInbounds(c *gin.Context) {
+func (a *XUIController) getInbounds(c *gin.Context) {
 	user := session.GetLoginUser(c)
 	inbounds, err := a.inboundService.GetInbounds(user.Id)
 	if err != nil {
@@ -67,6 +90,9 @@ func (a *XUIController) addInbound(c *gin.Context) {
 	log.Println(inbound)
 	err = a.inboundService.AddInbound(inbound)
 	jsonMsg(c, "添加", err)
+	if err == nil {
+		a.isNeedXrayRestart.Store(true)
+	}
 }
 
 func (a *XUIController) delInbound(c *gin.Context) {
@@ -77,4 +103,28 @@ func (a *XUIController) delInbound(c *gin.Context) {
 	}
 	err = a.inboundService.DelInbound(id)
 	jsonMsg(c, "删除", err)
+	if err == nil {
+		a.isNeedXrayRestart.Store(true)
+	}
+}
+
+func (a *XUIController) updateInbound(c *gin.Context) {
+	id, err := strconv.Atoi(c.Param("id"))
+	if err != nil {
+		jsonMsg(c, "修改", err)
+		return
+	}
+	inbound := &model.Inbound{
+		Id: id,
+	}
+	err = c.ShouldBind(inbound)
+	if err != nil {
+		jsonMsg(c, "修改", err)
+		return
+	}
+	err = a.inboundService.UpdateInbound(inbound)
+	jsonMsg(c, "修改", err)
+	if err == nil {
+		a.isNeedXrayRestart.Store(true)
+	}
 }

+ 23 - 0
web/global/global.go

@@ -0,0 +1,23 @@
+package global
+
+import (
+	"context"
+	"github.com/robfig/cron/v3"
+	_ "unsafe"
+)
+
+var webServer WebServer
+
+type WebServer interface {
+	GetCron() *cron.Cron
+	GetCtx() context.Context
+}
+
+//go:linkname setWebServer main.setWebServer
+func setWebServer(s WebServer) {
+	webServer = s
+}
+
+func GetWebServer() WebServer {
+	return webServer
+}

+ 0 - 0
web/html/login/login.html → web/html/login.html


+ 0 - 535
web/html/x-ui/inbound_modal.html

@@ -1,535 +0,0 @@
-{{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"
-         :ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
-
-    <!-- base -->
-    <a-form layout="inline">
-        <a-form-item label='{{ i18n "remark" }}'>
-            <a-input v-model.trim="inModal.inbound.remark"></a-input>
-        </a-form-item>
-        <a-form-item label='{{ i18n "enable" }}'>
-            <a-switch v-model="inModal.inbound.enable"></a-switch>
-        </a-form-item>
-        <a-form-item label='{{ i18n "protocol" }}'>
-            <a-select v-model="inModal.inbound.protocol" style="width: 160px;"
-                      @change="protocolChange">
-                <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">
-                监听 IP
-                <a-tooltip>
-                    <template slot="title">
-                        不懂请保持默认
-                    </template>
-                    <a-icon type="question-circle" theme="filled"></a-icon>
-                </a-tooltip>
-            </span>
-            <a-input v-model.trim="inModal.inbound.listen"></a-input>
-        </a-form-item>
-        <a-form-item label="端口">
-            <a-input type="number" v-model.number="inModal.inbound.port"></a-input>
-        </a-form-item>
-    </a-form>
-
-    <!-- vmess settings -->
-    <a-form v-if="inModal.inbound.protocol === Protocols.VMESS"
-            layout="inline">
-        <a-form-item label="id">
-            <a-input v-model.trim="inModal.inbound.settings.vmesses[0].id"></a-input>
-        </a-form-item>
-        <a-form-item label="额外 ID">
-            <a-input type="number" v-model.number="inModal.inbound.settings.vmesses[0].alterId"></a-input>
-        </a-form-item>
-        <a-form-item label="禁用不安全加密">
-            <a-switch v-model.number="inModal.inbound.settings.disableInsecure"></a-switch>
-        </a-form-item>
-    </a-form>
-
-    <!-- vless settings -->
-    <a-form v-if="inModal.inbound.protocol === Protocols.VLESS"
-            layout="inline">
-        <a-form-item label="id">
-            <a-input v-model.trim="inModal.inbound.settings.vlesses[0].id"></a-input>
-        </a-form-item>
-        <a-form-item label="flow">
-            <a-select v-model="inModal.inbound.settings.vlesses[0].flow" style="width: 150px">
-                <a-select-option value="">无</a-select-option>
-                <a-select-option v-for="key in VLESS_FLOW" :value="key">[[ key ]]</a-select-option>
-            </a-select>
-        </a-form-item>
-    </a-form>
-
-    <a-form v-if="inModal.inbound.protocol === Protocols.VLESS"
-            layout="inline">
-        <a-form-item label="fallbacks">
-            <a-row>
-                <a-button type="primary" size="small"
-                          @click="inModal.inbound.settings.addFallback()">
-                    +
-                </a-button>
-            </a-row>
-        </a-form-item>
-    </a-form>
-
-    <!-- vless fallbacks -->
-    <a-form v-for="(fallback, index) in inModal.inbound.settings.fallbacks" layout="inline">
-        <a-form-item>
-            <a-divider>
-                fallback[[ index + 1 ]]
-                <a-icon type="delete" @click="() => inModal.inbound.settings.delFallback(index)" style="color: rgb(255, 77, 79);cursor: pointer;"/>
-            </a-divider>
-        </a-form-item>
-        <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 type="number" v-model.number="fallback.xver"></a-input>
-        </a-form-item>
-    </a-form>
-
-    <!-- trojan settings -->
-    <a-form v-if="inModal.inbound.protocol === Protocols.TROJAN"
-            layout="inline">
-        <a-form-item label="密码">
-            <a-input v-model.trim="inModal.inbound.settings.clients[0].password"></a-input>
-        </a-form-item>
-    </a-form>
-
-    <!-- shadowsocks -->
-    <a-form v-if="inModal.inbound.protocol === Protocols.SHADOWSOCKS"
-            layout="inline">
-        <a-form-item label="加密">
-            <a-select v-model="inModal.inbound.settings.method" style="width: 165px;">
-                <a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
-            </a-select>
-        </a-form-item>
-        <a-form-item label="密码">
-            <a-input v-model.trim="inModal.inbound.settings.password"></a-input>
-        </a-form-item>
-        <a-form-item label="网络">
-            <a-select v-model="inModal.inbound.settings.network" style="width: 100px;">
-                <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>
-    </a-form>
-
-    <!-- stream settings -->
-    <template v-if="inModal.inbound.protocol === Protocols.VMESS
-                    || inModal.inbound.protocol === Protocols.VLESS
-                    || inModal.inbound.protocol === Protocols.SHADOWSOCKS">
-
-        <!-- select stream network -->
-        <a-form layout="inline">
-            <a-form-item label="传输">
-                <a-select v-model="inModal.inbound.stream.network" @change="streamNetworkChange">
-                    <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>
-                    <a-select-option value="http">http</a-select-option>
-                    <a-select-option value="quic">quic</a-select-option>
-                </a-select>
-            </a-form-item>
-        </a-form>
-
-        <!-- vmess tcp -->
-        <template v-if="inModal.inbound.stream.network === 'tcp'">
-            <!-- vmess tcp type -->
-            <a-form layout="inline">
-                <a-form-item label="http 伪装">
-                    <a-switch
-                            :checked="inModal.inbound.stream.tcp.type === 'http'"
-                            @change="checked => inModal.inbound.stream.tcp.type = checked ? 'http' : 'none'">
-                    </a-switch>
-                </a-form-item>
-            </a-form>
-
-            <!-- vmess tcp request -->
-            <a-form v-if="inModal.inbound.stream.tcp.type === 'http'"
-                    layout="inline">
-                <a-form-item label="请求版本">
-                    <a-input v-model.trim="inModal.inbound.stream.tcp.request.version"></a-input>
-                </a-form-item>
-                <a-form-item label="请求方法">
-                    <a-input v-model.trim="inModal.inbound.stream.tcp.request.method"></a-input>
-                </a-form-item>
-                <a-form-item label="请求路径">
-                    <a-row v-for="(path, index) in inModal.inbound.stream.tcp.request.path">
-                        <a-input v-model.trim="inModal.inbound.stream.tcp.request.path[index]"></a-input>
-                    </a-row>
-                </a-form-item>
-                <a-form-item label="请求头">
-                    <a-row>
-                        <a-button size="small"
-                                  @click="inModal.inbound.stream.tcp.request.addHeader('Host', 'xxx.com')">
-                            +
-                        </a-button>
-                    </a-row>
-                    <a-input-group v-for="(header, index) in inModal.inbound.stream.tcp.request.headers">
-                        <a-input style="width: 50%" v-model.trim="header.name"
-                                 addon-before="名称"></a-input>
-                        <a-input style="width: 50%" v-model.trim="header.value"
-                                 addon-before="值">
-                            <template slot="addonAfter">
-                                <a-button size="small"
-                                          @click="inModal.inbound.stream.tcp.request.removeHeader(index)">
-                                    -
-                                </a-button>
-                            </template>
-                        </a-input>
-                    </a-input-group>
-                </a-form-item>
-            </a-form>
-
-            <!-- vmess tcp response -->
-            <a-form v-if="inModal.inbound.stream.tcp.type === 'http'"
-                    layout="inline">
-                <a-form-item label="响应版本">
-                    <a-input v-model.trim="inModal.inbound.stream.tcp.response.version"></a-input>
-                </a-form-item>
-                <a-form-item label="响应状态">
-                    <a-input v-model.trim="inModal.inbound.stream.tcp.response.status"></a-input>
-                </a-form-item>
-                <a-form-item label="响应状态说明">
-                    <a-input v-model.trim="inModal.inbound.stream.tcp.response.reason"></a-input>
-                </a-form-item>
-                <a-form-item label="响应头">
-                    <a-row>
-                        <a-button size="small"
-                                  @click="inModal.inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">
-                            +
-                        </a-button>
-                    </a-row>
-                    <a-input-group v-for="(header, index) in inModal.inbound.stream.tcp.response.headers">
-                        <a-input style="width: 50%" v-model.trim="header.name"
-                                 addon-before="名称"></a-input>
-                        <a-input style="width: 50%" v-model.trim="header.value"
-                                 addon-before="值">
-                            <template slot="addonAfter">
-                                <a-button size="small"
-                                          @click="inModal.inbound.stream.tcp.response.removeHeader(index)">
-                                    -
-                                </a-button>
-                            </template>
-                        </a-input>
-                    </a-input-group>
-                </a-form-item>
-            </a-form>
-        </template>
-
-        <!-- vmess kcp -->
-        <a-form v-if="inModal.inbound.stream.network === 'kcp'"
-                layout="inline">
-            <a-form-item label="伪装">
-                <a-select v-model="inModal.inbound.stream.kcp.type" style="width: 280px;">
-                    <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>
-            <a-form-item label="密码">
-                <a-input v-model.number="inModal.inbound.stream.kcp.seed"></a-input>
-            </a-form-item>
-            <a-form-item label="mtu">
-                <a-input type="number" v-model.number="inModal.inbound.stream.kcp.mtu"></a-input>
-            </a-form-item>
-            <a-form-item label="tti (ms)">
-                <a-input type="number" v-model.number="inModal.inbound.stream.kcp.tti"></a-input>
-            </a-form-item>
-            <a-form-item label="uplink capacity (MB/S)">
-                <a-input type="number" v-model.number="inModal.inbound.stream.kcp.upCap"></a-input>
-            </a-form-item>
-            <a-form-item label="downlink capacity (MB/S)">
-                <a-input type="number" v-model.number="inModal.inbound.stream.kcp.downCap"></a-input>
-            </a-form-item>
-            <a-form-item label="congestion">
-                <a-switch v-model="inModal.inbound.stream.kcp.congestion"></a-switch>
-            </a-form-item>
-            <a-form-item label="read buffer size (MB)">
-                <a-input type="number" v-model.number="inModal.inbound.stream.kcp.readBuffer"></a-input>
-            </a-form-item>
-            <a-form-item label="write buffer size (MB)">
-                <a-input type="number" v-model.number="inModal.inbound.stream.kcp.writeBuffer"></a-input>
-            </a-form-item>
-        </a-form>
-
-        <!-- vmess ws -->
-        <a-form v-if="inModal.inbound.stream.network === 'ws'"
-                layout="inline">
-            <a-form-item label="路径">
-                <a-input v-model.trim="inModal.inbound.stream.ws.path"></a-input>
-            </a-form-item>
-            <a-form-item label="请求头">
-                <a-row>
-                    <a-button size="small"
-                              @click="inModal.inbound.stream.ws.addHeader('Host', '')">
-                        +
-                    </a-button>
-                </a-row>
-                <a-input-group v-for="(header, index) in inModal.inbound.stream.ws.headers">
-                    <a-input style="width: 50%" v-model.trim="header.name"
-                             addon-before="名称"></a-input>
-                    <a-input style="width: 50%" v-model.trim="header.value"
-                             addon-before="值">
-                        <template slot="addonAfter">
-                            <a-button size="small"
-                                      @click="inModal.inbound.stream.ws.removeHeader(index)">
-                                -
-                            </a-button>
-                        </template>
-                    </a-input>
-                </a-input-group>
-            </a-form-item>
-        </a-form>
-
-        <!-- vmess http -->
-        <a-form v-if="inModal.inbound.stream.network === 'http'"
-                layout="inline">
-            <a-form-item label="路径">
-                <a-input v-model.trim="inModal.inbound.stream.http.path"></a-input>
-            </a-form-item>
-            <a-form-item label="host">
-                <a-row v-for="(host, index) in inModal.inbound.stream.http.host">
-                    <a-input v-model.trim="inModal.inbound.stream.http.host[index]"></a-input>
-                </a-row>
-            </a-form-item>
-        </a-form>
-
-        <!-- vmess quic -->
-        <a-form v-if="inModal.inbound.stream.network === 'quic'"
-                layout="inline">
-            <a-form-item label="加密">
-                <a-select v-model="inModal.inbound.stream.quic.security" style="width: 165px;">
-                    <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="密码">
-                <a-input v-model.trim="inModal.inbound.stream.quic.key"></a-input>
-            </a-form-item>
-            <a-form-item label="伪装">
-                <a-select v-model="inModal.inbound.stream.quic.type" style="width: 280px;">
-                    <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>
-        </a-form>
-    </template>
-
-    <!-- dokodemo-door -->
-    <a-form v-if="inModal.inbound.protocol === Protocols.DOKODEMO"
-            layout="inline">
-        <a-form-item label="目标地址">
-            <a-input v-model.trim="inModal.inbound.settings.address"></a-input>
-        </a-form-item>
-        <a-form-item label="目标端口">
-            <a-input type="number" v-model.number="inModal.inbound.settings.port"></a-input>
-        </a-form-item>
-        <a-form-item label="网络">
-            <a-select v-model="inModal.inbound.settings.network" style="width: 100px;">
-                <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>
-    </a-form>
-
-    <!-- socks -->
-    <a-form v-if="inModal.inbound.protocol === Protocols.SOCKS"
-            layout="inline">
-        <a-form-item label="密码认证">
-            <a-switch :checked="inModal.inbound.settings.auth === 'password'"
-                      @change="checked => inModal.inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
-        </a-form-item>
-        <template v-if="inModal.inbound.settings.auth === 'password'">
-            <a-form-item label="用户名">
-                <a-input v-model.trim="inModal.inbound.settings.accounts[0].user"></a-input>
-            </a-form-item>
-            <a-form-item label="密码">
-                <a-input v-model.trim="inModal.inbound.settings.accounts[0].pass"></a-input>
-            </a-form-item>
-        </template>
-        <a-form-item label="启用 udp">
-            <a-switch v-model="inModal.inbound.settings.udp"></a-switch>
-        </a-form-item>
-        <a-form-item v-if="inModal.inbound.settings.udp"
-                     label="IP">
-            <a-input v-model.trim="inModal.inbound.settings.ip"></a-input>
-        </a-form-item>
-    </a-form>
-
-    <!-- http -->
-    <a-form v-if="inModal.inbound.protocol === Protocols.HTTP"
-            layout="inline">
-        <a-form-item label="用户名">
-            <a-input v-model.trim="inModal.inbound.settings.accounts[0].user"></a-input>
-        </a-form-item>
-        <a-form-item label="密码">
-            <a-input v-model.trim="inModal.inbound.settings.accounts[0].pass"></a-input>
-        </a-form-item>
-    </a-form>
-
-    <!-- tls settings -->
-    <template v-if="(inModal.inbound.protocol === Protocols.VMESS
-                    || inModal.inbound.protocol === Protocols.VLESS
-                    || inModal.inbound.protocol === Protocols.TROJAN
-                    || inModal.inbound.protocol === Protocols.SHADOWSOCKS)
-                    && ['tcp', 'ws', 'http', 'quic'].indexOf(inModal.inbound.stream.network) >= 0">
-
-        <!-- tls enable -->
-        <a-form layout="inline" v-if="inModal.inbound.protocol !== Protocols.TROJAN">
-            <a-form-item label="tls">
-                <a-switch
-                        :checked="inModal.inbound.stream.security === 'tls'"
-                        @change="checked => inModal.inbound.stream.security = checked ? 'tls' : 'none'">
-                </a-switch>
-            </a-form-item>
-            <a-form-item v-if="inModal.inbound.protocol === Protocols.VLESS && inModal.inbound.stream.security === 'tls' && inModal.inbound.stream.network === 'tcp'" label="xtls">
-                <a-switch v-model="inModal.inbound.stream.is_xtls"></a-switch>
-            </a-form-item>
-        </a-form>
-
-        <!-- tls settings -->
-        <a-form v-if="inModal.inbound.stream.security === 'tls'"
-                layout="inline">
-            <a-form-item label="域名">
-                <a-input v-model.trim="inModal.inbound.stream.tls.server"></a-input>
-            </a-form-item>
-{#            <a-form-item label="允许不安全">#}
-{#                <a-switch v-model="inModal.inbound.stream.tls.allowInsecure"></a-switch>#}
-{#            </a-form-item>#}
-            <a-form-item label="证书">
-                <a-radio-group v-model="inModal.inbound.stream.tls.certs[0].useFile"
-                               button-style="solid">
-                    <a-radio-button :value="true">certificate file path</a-radio-button>
-                    <a-radio-button :value="false">certificate file content</a-radio-button>
-                </a-radio-group>
-            </a-form-item>
-            <template v-if="inModal.inbound.stream.tls.certs[0].useFile">
-                <a-form-item label="公钥文件路径">
-                    <a-input v-model.trim="inModal.inbound.stream.tls.certs[0].certFile"></a-input>
-                </a-form-item>
-                <a-form-item label="密钥文件路径">
-                    <a-input v-model.trim="inModal.inbound.stream.tls.certs[0].keyFile"></a-input>
-                </a-form-item>
-            </template>
-            <template v-else>
-                <a-form-item label="公钥内容">
-                    <a-input type="textarea" :rows="2"
-                             v-model="inModal.inbound.stream.tls.certs[0].cert"></a-input>
-                </a-form-item>
-                <a-form-item label="密钥内容">
-                    <a-input type="textarea" :rows="2"
-                             v-model="inModal.inbound.stream.tls.certs[0].key"></a-input>
-                </a-form-item>
-            </template>
-        </a-form>
-    </template>
-
-    <!-- sniffing -->
-    <a-form layout="inline">
-        <a-form-item>
-            <span slot="label">
-                sniffing
-                <a-tooltip>
-                    <template slot="title">
-                        没有特殊需求保持默认即可
-                    </template>
-                    <a-icon type="question-circle" theme="filled"></a-icon>
-                </a-tooltip>
-            </span>
-            <a-switch v-model="inModal.inbound.sniffing.enabled"></a-switch>
-        </a-form-item>
-    </a-form>
-</a-modal>
-<script>
-
-    const inModal = {
-        title: '',
-        visible: false,
-        confirmLoading: false,
-        okText: '确定',
-        confirm: null,
-        inbound: new Inbound(),
-        ok() {
-            ObjectUtil.execute(inModal.confirm);
-        },
-        show({ title='', okText='确定', inbound=null, confirm=()=>{} }) {
-            this.title = title;
-            this.okText = okText;
-            if (inbound) {
-                this.inbound = Inbound.fromJson(inbound.toJson());
-            } else {
-                this.inbound = new Inbound();
-            }
-            this.confirm = confirm;
-            this.visible = true;
-        },
-        close() {
-            inModal.visible = false;
-            inModal.closeLoading();
-        },
-        loading() {
-            inModal.confirmLoading = true;
-        },
-        closeLoading() {
-            inModal.confirmLoading = false;
-        }
-    };
-
-    const protocols = {
-        VMESS: Protocols.VMESS,
-        VLESS: Protocols.VLESS,
-        TROJAN: Protocols.TROJAN,
-        SHADOWSOCKS: Protocols.SHADOWSOCKS,
-        DOKODEMO: Protocols.DOKODEMO,
-        SOCKS: Protocols.SOCKS,
-        HTTP: Protocols.HTTP,
-    };
-
-    new Vue({
-        delimiters: ['[[', ']]'],
-        el: '#inbound-modal',
-        data: {
-            inModal: inModal,
-            Protocols: protocols,
-            SSMethods: SSMethods,
-        },
-        methods: {
-            streamNetworkChange(oldValue) {
-                if (oldValue === 'kcp') {
-                    this.inModal.inbound.stream.security = 'none';
-                }
-            },
-            protocolChange(value) {
-                this.inModal.inbound.settings = Inbound.Settings.getSettings(value);
-                if (value === Protocols.TROJAN) {
-                    this.inModal.inbound.stream.security = 'tls';
-                }
-            }
-        }
-    });
-
-</script>
-{{end}}

+ 0 - 0
web/html/x-ui/common_sider.html → web/html/xui/common_sider.html


+ 81 - 0
web/html/xui/form/inbound.html

@@ -0,0 +1,81 @@
+{{define "form/inbound"}}
+<!-- base -->
+<a-form layout="inline">
+    <a-form-item label='{{ i18n "remark" }}'>
+        <a-input v-model.trim="dbInbound.remark"></a-input>
+    </a-form-item>
+    <a-form-item label='{{ i18n "enable" }}'>
+        <a-switch v-model="dbInbound.enable"></a-switch>
+    </a-form-item>
+    <a-form-item label='{{ i18n "protocol" }}'>
+        <a-select v-model="inbound.protocol" style="width: 160px;">
+            <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">
+            监听 IP
+            <a-tooltip>
+                <template slot="title">
+                    默认留空即可
+                </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>
+    <a-form-item label="端口">
+        <a-input type="number" v-model.number="inbound.port"></a-input>
+    </a-form-item>
+</a-form>
+
+<!-- vmess settings -->
+<template v-if="inbound.protocol === Protocols.VMESS">
+    {{template "form/vmess"}}
+</template>
+
+<!-- vless settings -->
+<template v-if="inbound.protocol === Protocols.VLESS">
+    {{template "form/vless"}}
+</template>
+
+<!-- trojan settings -->
+<template v-if="inbound.protocol === Protocols.TROJAN">
+    {{template "form/trojan"}}
+</template>
+
+<!-- shadowsocks -->
+<template v-if="inbound.protocol === Protocols.SHADOWSOCKS">
+    {{template "form/shadowsocks"}}
+</template>
+
+<!-- dokodemo-door -->
+<template v-if="inbound.protocol === Protocols.DOKODEMO">
+    {{template "form/dokodemo"}}
+</template>
+
+<!-- socks -->
+<template v-if="inbound.protocol === Protocols.SOCKS">
+    {{template "form/socks"}}
+</template>
+
+<!-- http -->
+<template v-if="inbound.protocol === Protocols.HTTP">
+    {{template "form/http"}}
+</template>
+
+<!-- stream settings -->
+<template v-if="inbound.canEnableStream()">
+    {{template "form/streamSettings"}}
+</template>
+
+<!-- tls settings -->
+<template v-if="inbound.canEnableTls()">
+    {{template "form/tlsSettings"}}
+</template>
+
+<!-- sniffing -->
+<template v-if="inbound.canSniffing()">
+    {{template "form/sniffing"}}
+</template>
+{{end}}

+ 17 - 0
web/html/xui/form/protocol/dokodemo.html

@@ -0,0 +1,17 @@
+{{define "form/dokodemo"}}
+<a-form layout="inline">
+    <a-form-item label="目标地址">
+        <a-input v-model.trim="inbound.settings.address"></a-input>
+    </a-form-item>
+    <a-form-item label="目标端口">
+        <a-input type="number" v-model.number="inbound.settings.port"></a-input>
+    </a-form-item>
+    <a-form-item label="网络">
+        <a-select v-model="inbound.settings.network" style="width: 100px;">
+            <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>
+</a-form>
+{{end}}

+ 10 - 0
web/html/xui/form/protocol/http.html

@@ -0,0 +1,10 @@
+{{define "form/http"}}
+<a-form layout="inline">
+    <a-form-item label="用户名">
+        <a-input v-model.trim="inbound.settings.accounts[0].user"></a-input>
+    </a-form-item>
+    <a-form-item label="密码">
+        <a-input v-model.trim="inbound.settings.accounts[0].pass"></a-input>
+    </a-form-item>
+</a-form>
+{{end}}

+ 19 - 0
web/html/xui/form/protocol/shadowsocks.html

@@ -0,0 +1,19 @@
+{{define "form/shadowsocks"}}
+<a-form layout="inline">
+    <a-form-item label="加密">
+        <a-select v-model="inbound.settings.method" style="width: 165px;">
+            <a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
+        </a-select>
+    </a-form-item>
+    <a-form-item label="密码">
+        <a-input v-model.trim="inbound.settings.password"></a-input>
+    </a-form-item>
+    <a-form-item label="网络">
+        <a-select v-model="inbound.settings.network" style="width: 100px;">
+            <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>
+</a-form>
+{{end}}

+ 23 - 0
web/html/xui/form/protocol/socks.html

@@ -0,0 +1,23 @@
+{{define "form/socks"}}
+<a-form layout="inline">
+    <a-form-item label="密码认证">
+        <a-switch :checked="inbound.settings.auth === 'password'"
+                  @change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
+    </a-form-item>
+    <template v-if="inbound.settings.auth === 'password'">
+        <a-form-item label="用户名">
+            <a-input v-model.trim="inbound.settings.accounts[0].user"></a-input>
+        </a-form-item>
+        <a-form-item label="密码">
+            <a-input v-model.trim="inbound.settings.accounts[0].pass"></a-input>
+        </a-form-item>
+    </template>
+    <a-form-item label="启用 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>
+</a-form>
+{{end}}

+ 7 - 0
web/html/xui/form/protocol/trojan.html

@@ -0,0 +1,7 @@
+{{define "form/trojan"}}
+<a-form layout="inline">
+    <a-form-item label="密码">
+        <a-input v-model.trim="inbound.settings.clients[0].password"></a-input>
+    </a-form-item>
+</a-form>
+{{end}}

+ 49 - 0
web/html/xui/form/protocol/vless.html

@@ -0,0 +1,49 @@
+{{define "form/vless"}}
+<a-form layout="inline">
+    <a-form-item label="id">
+        <a-input v-model.trim="inbound.settings.vlesses[0].id"></a-input>
+    </a-form-item>
+    <a-form-item label="flow">
+        <a-select v-model="inbound.settings.vlesses[0].flow" style="width: 150px">
+            <a-select-option value="">无</a-select-option>
+            <a-select-option v-for="key in VLESS_FLOW" :value="key">[[ key ]]</a-select-option>
+        </a-select>
+    </a-form-item>
+</a-form>
+
+<a-form layout="inline">
+    <a-form-item label="fallbacks">
+        <a-row>
+            <a-button type="primary" size="small"
+                      @click="inbound.settings.addFallback()">
+                +
+            </a-button>
+        </a-row>
+    </a-form-item>
+</a-form>
+
+<!-- vless fallbacks -->
+<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
+    <a-divider>
+        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 type="number" v-model.number="fallback.xver"></a-input>
+    </a-form-item>
+    <a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
+</a-form>
+{{end}}

+ 13 - 0
web/html/xui/form/protocol/vmess.html

@@ -0,0 +1,13 @@
+{{define "form/vmess"}}
+<a-form layout="inline">
+    <a-form-item label="id">
+        <a-input v-model.trim="inbound.settings.vmesses[0].id"></a-input>
+    </a-form-item>
+    <a-form-item label="额外 ID">
+        <a-input type="number" v-model.number="inbound.settings.vmesses[0].alterId"></a-input>
+    </a-form-item>
+    <a-form-item label="禁用不安全加密">
+        <a-switch v-model.number="inbound.settings.disableInsecure"></a-switch>
+    </a-form-item>
+</a-form>
+{{end}}

+ 16 - 0
web/html/xui/form/sniffing.html

@@ -0,0 +1,16 @@
+{{define "form/sniffing"}}
+<a-form layout="inline">
+  <a-form-item>
+            <span slot="label">
+                sniffing
+                <a-tooltip>
+                    <template slot="title">
+                        没有特殊需求保持默认即可
+                    </template>
+                    <a-icon type="question-circle" theme="filled"></a-icon>
+                </a-tooltip>
+            </span>
+    <a-switch v-model="inbound.sniffing.enabled"></a-switch>
+  </a-form-item>
+</a-form>
+{{end}}

+ 7 - 0
web/html/xui/form/stream/stream_grpc.html

@@ -0,0 +1,7 @@
+{{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>
+{{end}}

+ 12 - 0
web/html/xui/form/stream/stream_http.html

@@ -0,0 +1,12 @@
+{{define "form/streamHTTP"}}
+<a-form layout="inline">
+    <a-form-item label="路径">
+        <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>
+</a-form>
+{{end}}

+ 38 - 0
web/html/xui/form/stream/stream_kcp.html

@@ -0,0 +1,38 @@
+{{define "form/streamKCP"}}
+<a-form layout="inline">
+    <a-form-item label="伪装">
+        <a-select v-model="inbound.stream.kcp.type" style="width: 280px;">
+            <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>
+    <a-form-item label="密码">
+        <a-input v-model.number="inbound.stream.kcp.seed"></a-input>
+    </a-form-item>
+    <a-form-item label="mtu">
+        <a-input type="number" v-model.number="inbound.stream.kcp.mtu"></a-input>
+    </a-form-item>
+    <a-form-item label="tti (ms)">
+        <a-input type="number" v-model.number="inbound.stream.kcp.tti"></a-input>
+    </a-form-item>
+    <a-form-item label="uplink capacity (MB/S)">
+        <a-input type="number" v-model.number="inbound.stream.kcp.upCap"></a-input>
+    </a-form-item>
+    <a-form-item label="downlink capacity (MB/S)">
+        <a-input type="number" v-model.number="inbound.stream.kcp.downCap"></a-input>
+    </a-form-item>
+    <a-form-item label="congestion">
+        <a-switch v-model="inbound.stream.kcp.congestion"></a-switch>
+    </a-form-item>
+    <a-form-item label="read buffer size (MB)">
+        <a-input type="number" v-model.number="inbound.stream.kcp.readBuffer"></a-input>
+    </a-form-item>
+    <a-form-item label="write buffer size (MB)">
+        <a-input type="number" v-model.number="inbound.stream.kcp.writeBuffer"></a-input>
+    </a-form-item>
+</a-form>
+{{end}}

+ 24 - 0
web/html/xui/form/stream/stream_quic.html

@@ -0,0 +1,24 @@
+{{define "form/streamQUIC"}}
+<a-form layout="inline">
+    <a-form-item label="加密">
+        <a-select v-model="inbound.stream.quic.security" style="width: 165px;">
+            <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="密码">
+        <a-input v-model.trim="inbound.stream.quic.key"></a-input>
+    </a-form-item>
+    <a-form-item label="伪装">
+        <a-select v-model="inbound.stream.quic.type" style="width: 280px;">
+            <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>
+</a-form>
+{{end}}

+ 45 - 0
web/html/xui/form/stream/stream_settings.html

@@ -0,0 +1,45 @@
+{{define "form/streamSettings"}}
+<!-- select stream network -->
+<a-form layout="inline">
+    <a-form-item label="传输">
+        <a-select v-model="inbound.stream.network" @change="streamNetworkChange">
+            <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>
+            <a-select-option value="http">http</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>
+</a-form>
+
+<!-- tcp -->
+<template v-if="inbound.stream.network === 'tcp'">
+    {{template "form/streamTCP"}}
+</template>
+
+<!-- kcp -->
+<template v-if="inbound.stream.network === 'kcp'">
+    {{template "form/streamKCP"}}
+</template>
+
+<!-- ws -->
+<template v-if="inbound.stream.network === 'ws'">
+    {{template "form/streamWS"}}
+</template>
+
+<!-- http -->
+<template v-if="inbound.stream.network === 'http'">
+    {{template "form/streamHTTP"}}
+</template>
+
+<!-- quic -->
+<template v-if="inbound.stream.network === 'quic'">
+    {{template "form/streamQUIC"}}
+</template>
+
+<!-- grpc -->
+<template v-if="inbound.stream.network === 'grpc'">
+    {{template "form/streamGRPC"}}
+</template>
+{{end}}

+ 83 - 0
web/html/xui/form/stream/stream_tcp.html

@@ -0,0 +1,83 @@
+{{define "form/streamTCP"}}
+<!-- tcp type -->
+<a-form layout="inline">
+    <a-form-item label="http 伪装">
+        <a-switch
+                :checked="inbound.stream.tcp.type === 'http'"
+                @change="checked => inbound.stream.tcp.type = checked ? 'http' : 'none'">
+        </a-switch>
+    </a-form-item>
+</a-form>
+
+<!-- tcp request -->
+<a-form v-if="inbound.stream.tcp.type === 'http'"
+        layout="inline">
+    <a-form-item label="请求版本">
+        <a-input v-model.trim="inbound.stream.tcp.request.version"></a-input>
+    </a-form-item>
+    <a-form-item label="请求方法">
+        <a-input v-model.trim="inbound.stream.tcp.request.method"></a-input>
+    </a-form-item>
+    <a-form-item label="请求路径">
+        <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>
+    <a-form-item label="请求头">
+        <a-row>
+            <a-button size="small"
+                      @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="名称"></a-input>
+            <a-input style="width: 50%" v-model.trim="header.value"
+                     addon-before="值">
+                <template slot="addonAfter">
+                    <a-button size="small"
+                              @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="响应版本">
+        <a-input v-model.trim="inbound.stream.tcp.response.version"></a-input>
+    </a-form-item>
+    <a-form-item label="响应状态">
+        <a-input v-model.trim="inbound.stream.tcp.response.status"></a-input>
+    </a-form-item>
+    <a-form-item label="响应状态说明">
+        <a-input v-model.trim="inbound.stream.tcp.response.reason"></a-input>
+    </a-form-item>
+    <a-form-item label="响应头">
+        <a-row>
+            <a-button size="small"
+                      @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="名称"></a-input>
+            <a-input style="width: 50%" v-model.trim="header.value"
+                     addon-before="值">
+                <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>
+</a-form>
+{{end}}

+ 28 - 0
web/html/xui/form/stream/stream_ws.html

@@ -0,0 +1,28 @@
+{{define "form/streamWS"}}
+<a-form layout="inline">
+    <a-form-item label="路径">
+        <a-input v-model.trim="inbound.stream.ws.path"></a-input>
+    </a-form-item>
+    <a-form-item label="请求头">
+        <a-row>
+            <a-button size="small"
+                      @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="名称"></a-input>
+            <a-input style="width: 50%" v-model.trim="header.value"
+                     addon-before="值">
+                <template slot="addonAfter">
+                    <a-button size="small"
+                              @click="inbound.stream.ws.removeHeader(index)">
+                        -
+                    </a-button>
+                </template>
+            </a-input>
+        </a-input-group>
+    </a-form-item>
+</a-form>
+{{end}}

+ 45 - 0
web/html/xui/form/tls_settings.html

@@ -0,0 +1,45 @@
+{{define "form/tlsSettings"}}
+<!-- tls enable -->
+<a-form layout="inline" v-if="inbound.canSetTls()">
+    <a-form-item label="tls">
+        <a-switch v-model="inbound.tls">
+        </a-switch>
+    </a-form-item>
+    <a-form-item v-if="inbound.canEnableXTls()" label="xtls">
+        <a-switch v-model="inbound.xtls"></a-switch>
+    </a-form-item>
+</a-form>
+
+<!-- tls settings -->
+<a-form v-if="inbound.tls || inbound.xtls"
+        layout="inline">
+    <a-form-item label="域名">
+        <a-input v-model.trim="inbound.stream.tls.server"></a-input>
+    </a-form-item>
+    <a-form-item label="证书">
+        <a-radio-group v-model="inbound.stream.tls.certs[0].useFile"
+                       button-style="solid">
+            <a-radio-button :value="true">certificate file path</a-radio-button>
+            <a-radio-button :value="false">certificate file content</a-radio-button>
+        </a-radio-group>
+    </a-form-item>
+    <template v-if="inbound.stream.tls.certs[0].useFile">
+        <a-form-item label="公钥文件路径">
+            <a-input v-model.trim="inbound.stream.tls.certs[0].certFile"></a-input>
+        </a-form-item>
+        <a-form-item label="密钥文件路径">
+            <a-input v-model.trim="inbound.stream.tls.certs[0].keyFile"></a-input>
+        </a-form-item>
+    </template>
+    <template v-else>
+        <a-form-item label="公钥内容">
+            <a-input type="textarea" :rows="2"
+                     v-model="inbound.stream.tls.certs[0].cert"></a-input>
+        </a-form-item>
+        <a-form-item label="密钥内容">
+            <a-input type="textarea" :rows="2"
+                     v-model="inbound.stream.tls.certs[0].key"></a-input>
+        </a-form-item>
+    </template>
+</a-form>
+{{end}}

+ 79 - 0
web/html/xui/inbound_modal.html

@@ -0,0 +1,79 @@
+{{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"
+         :ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
+    {{template "form/inbound"}}
+</a-modal>
+<script>
+
+    const inModal = {
+        title: '',
+        visible: false,
+        confirmLoading: false,
+        okText: '确定',
+        confirm: null,
+        inbound: new Inbound(),
+        dbInbound: new DBInbound(),
+        ok() {
+            ObjectUtil.execute(inModal.confirm, inModal.inbound, inModal.dbInbound);
+        },
+        show({ title='', okText='确定', inbound=null, dbInbound=null, confirm=(inbound, dbInbound)=>{} }) {
+            this.title = title;
+            this.okText = okText;
+            if (inbound) {
+                this.inbound = Inbound.fromJson(inbound.toJson());
+            } else {
+                this.inbound = new Inbound();
+            }
+            if (dbInbound) {
+                this.dbInbound = new DBInbound(dbInbound);
+            } else {
+                this.dbInbound = new DBInbound();
+            }
+            this.confirm = confirm;
+            this.visible = true;
+        },
+        close() {
+            inModal.visible = false;
+            inModal.loading(false);
+        },
+        loading(loading) {
+            inModal.confirmLoading = loading;
+        },
+    };
+
+    const protocols = {
+        VMESS: Protocols.VMESS,
+        VLESS: Protocols.VLESS,
+        TROJAN: Protocols.TROJAN,
+        SHADOWSOCKS: Protocols.SHADOWSOCKS,
+        DOKODEMO: Protocols.DOKODEMO,
+        SOCKS: Protocols.SOCKS,
+        HTTP: Protocols.HTTP,
+    };
+
+    new Vue({
+        delimiters: ['[[', ']]'],
+        el: '#inbound-modal',
+        data: {
+            inModal: inModal,
+            Protocols: protocols,
+            SSMethods: SSMethods,
+            get inbound() {
+                return inModal.inbound;
+            },
+            get dbInbound() {
+                return inModal.dbInbound;
+            }
+        },
+        methods: {
+            streamNetworkChange(oldValue) {
+                if (oldValue === 'kcp') {
+                    this.inModal.inbound.tls = false;
+                }
+            }
+        }
+    });
+
+</script>
+{{end}}

+ 56 - 54
web/html/x-ui/inbounds.html → web/html/xui/inbounds.html

@@ -23,12 +23,6 @@
                 </transition>
                 <transition name="list" appear>
                     <a-card hoverable style="margin-bottom: 20px;">
-                        <div slot="title">
-                            <a-button type="primary" icon="plus" @click="openAddInbound"></a-button>
-                        </div>
-                        <a-row>
-                            <a-input v-model="searchKey" placeholder="search" autofocus></a-input>
-                        </a-row>
                         <a-row>
                             <a-col :xs="24" :sm="24" :lg="12">
                                 upload / download:
@@ -51,36 +45,37 @@
                 </transition>
                 <transition name="list" appear>
                     <a-card hoverable>
-                        <a-table :columns="columns" :row-key="inbound => inbound.id"
+                        <div slot="title">
+                            <a-button type="primary" icon="plus" @click="openAddInbound"></a-button>
+                        </div>
+                        <a-input v-model="searchKey" placeholder="search" autofocus style="max-width: 300px"></a-input>
+                        <a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
                                  :data-source="dbInbounds"
                                  :loading="spinning" :scroll="{ x: 1500 }"
                                  :pagination="false"
+                                 style="margin-top: 20px"
                                  @change="() => getDBInbounds()">
-                            <template slot="protocol" slot-scope="text, inbound">
-                                <a-tag color="blue">[[ inbound.protocol ]]</a-tag>
+                            <template slot="protocol" slot-scope="text, dbInbound">
+                                <a-tag color="blue">[[ dbInbound.protocol ]]</a-tag>
                             </template>
-                            <template slot="settings" slot-scope="text, inbound">
+                            <template slot="settings" slot-scope="text, dbInbound">
                                 <a-button type="link">查看</a-button>
                             </template>
-                            <template slot="streamSettings" slot-scope="text, inbound">
+                            <template slot="streamSettings" slot-scope="text, dbInbound">
                                 <a-button type="link">查看</a-button>
                             </template>
-                            <template slot="enable" slot-scope="text, inbound">
-                                <a-tag v-if="inbound.enable" color="green">启用</a-tag>
+                            <template slot="enable" slot-scope="text, dbInbound">
+                                <a-tag v-if="dbInbound.enable" color="green">启用</a-tag>
                                 <a-tag v-else color="red">禁用</a-tag>
                             </template>
-                            <template slot="expiryTime" slot-scope="text, inbound">
-                                <span v-if="inbound.expiryTime > 0" color="red">[[ DateUtil.formatMillis(inbound.expiryTime) ]]</span>
+                            <template slot="expiryTime" slot-scope="text, dbInbound">
+                                <span v-if="dbInbound.expiryTime > 0" color="red">[[ DateUtil.formatMillis(dbInbound.expiryTime) ]]</span>
                                 <span v-else>无限期</span>
                             </template>
-                            <template slot="action" slot-scope="text, inbound">
-                                <a-button type="primary" icon="qrcode"></a-button>
-                                <a-button type="primary" icon="edit"></a-button>
-                                <a-button type="danger" icon="delete" @click="delInbound(inbound)"></a-button>
-                            </template>
-
-                            <template slot="expandedRowRender" slot-scope="inbound" style="margin: 0">
-                                [[ inbound.id ]]
+                            <template slot="action" slot-scope="text, dbInbound">
+                                <a-button v-if="dbInbound.hasLink()" type="primary" icon="qrcode" @click="showQrcode(dbInbound)"></a-button>
+                                <a-button type="primary" icon="edit" @click="openEditInbound(dbInbound)"></a-button>
+                                <a-button type="danger" icon="delete" @click="delInbound(dbInbound)"></a-button>
                             </template>
                         </a-table>
                     </a-card>
@@ -147,7 +142,6 @@
             loading(spinning=true) {
                 this.spinning = spinning;
             },
-            empDefault(o, defaultValue='') {return ObjectUtil.isEmpty(o) ? defaultValue : o},
             async getDBInbounds() {
                 this.loading();
                 const msg = await HttpUtil.post('/xui/inbounds');
@@ -179,60 +173,72 @@
                 inModal.show({
                     title: 'add account',
                     okText: 'add',
-                    confirm: async () => {
+                    confirm: async (inbound, dbInbound) => {
                         inModal.loading();
-                        await this.addInbound(inModal.inbound);
-                        inModal.closeLoading();
+                        await this.addInbound(inbound, dbInbound);
+                        inModal.close();
                     }
                 });
             },
-            openEditInbound(inbound) {
+            openEditInbound(dbInbound) {
+                const inbound = dbInbound.toInbound();
                 inModal.show({
                     title: 'update account',
                     okText: 'update',
                     inbound: inbound,
-                    confirm: async () => {
+                    dbInbound: dbInbound,
+                    confirm: async (inbound, dbInbound) => {
                         inModal.loading();
-                        inModal.inbound.id = inbound.id;
-                        await this.updateInbound(inModal.inbound);
-                        inModal.closeLoading();
+                        await this.updateInbound(inbound, dbInbound);
+                        inModal.close();
                     }
                 });
             },
-            async addInbound(inbound) {
-                let data = {
-                    port: inbound.port,
+            async addInbound(inbound, dbInbound) {
+                const data = {
+                    remark: dbInbound.remark,
+                    enable: dbInbound.enable,
+
                     listen: inbound.listen,
+                    port: inbound.port,
                     protocol: inbound.protocol,
-                    settings: inbound.settings.toString(false),
-                    stream_settings: inbound.stream.toString(false),
-                    sniffing: [Protocols.VMESS, Protocols.VLESS, Protocols.SHADOWSOCKS].indexOf(inbound.protocol) >= 0 ? inbound.sniffing.toString(false) : '{}',
-                    remark: inbound.remark,
+                    settings: inbound.settings.toString(),
+                    stream_settings: inbound.stream.toString(),
+                    sniffing: inbound.canSniffing() ? inbound.sniffing.toString() : '{}',
                 };
                 await this.submit('/xui/inbound/add', data, inModal);
             },
-            async updateInbound(inbound) {
+            async updateInbound(inbound, dbInbound) {
                 const data = {
-                    port: inbound.port,
+                    remark: dbInbound.remark,
+                    enable: dbInbound.enable,
+
                     listen: inbound.listen,
+                    port: inbound.port,
                     protocol: inbound.protocol,
-                    settings: inbound.settings.toString(false),
-                    stream_settings: inbound.stream.toString(false),
-                    sniffing: [Protocols.VMESS, Protocols.VLESS, Protocols.SHADOWSOCKS].indexOf(inbound.protocol) >= 0 ? inbound.sniffing.toString(false) : '{}',
-                    remark: inbound.remark,
-                    enable: inbound.enable,
+                    settings: inbound.settings.toString(),
+                    stream_settings: inbound.stream.toString(),
+                    sniffing: inbound.canSniffing() ? inbound.sniffing.toString() : '{}',
                 };
-                await this.submit(`/xui/inbound/update/${inbound.id}`, data, inModal);
+                await this.submit(`/xui/inbound/update/${dbInbound.id}`, data, inModal);
             },
-            delInbound(inbound) {
+            delInbound(dbInbound) {
                 this.$confirm({
                     title: 'delete account',
                     content: 'Cannot be restored after deletion, confirm deletion?',
                     okText: 'delete',
                     cancelText: 'cancel',
-                    onOk: () => this.submit('/xui/inbound/del/' + inbound.id),
+                    onOk: () => this.submit('/xui/inbound/del/' + dbInbound.id),
                 });
             },
+            showQrcode(dbInbound) {
+                let address = location.hostname;
+                if (!ObjectUtil.isEmpty(dbInbound.listen) || dbInbound.listen !== "0.0.0.0") {
+                    address = dbInbound.listen;
+                }
+                const link = dbInbound.genLink(address);
+                qrModal.show('二维码', link);
+            },
             resetTraffic(inbound) {
                 this.submit(`/xui/reset_traffic/${inbound.id}`);
             },
@@ -244,14 +250,10 @@
                 this.submit(`/xui/inbound/update/${inbound.id}`, data);
             },
             async submit(url, data, modal) {
-                const msg = await HttpUtil.post(url, data);
+                const msg = await HttpUtil.postWithModal(url, data, modal);
                 if (msg.success) {
                     this.getDBInbounds();
-                    if (modal != null) {
-                        modal.close();
-                    }
                 }
-                return msg;
             },
         },
         watch: {

+ 0 - 0
web/html/x-ui/index.html → web/html/xui/index.html


+ 29 - 0
web/service/inbound.go

@@ -38,3 +38,32 @@ func (s *InboundService) DelInbound(id int) error {
 	db := database.GetDB()
 	return db.Delete(model.Inbound{}, id).Error
 }
+
+func (s *InboundService) GetInbound(id int) (*model.Inbound, error) {
+	db := database.GetDB()
+	inbound := &model.Inbound{}
+	err := db.Model(model.Inbound{}).First(inbound, id).Error
+	if err != nil {
+		return nil, err
+	}
+	return inbound, nil
+}
+
+func (s *InboundService) UpdateInbound(inbound *model.Inbound) error {
+	oldInbound, err := s.GetInbound(inbound.Id)
+	if err != nil {
+		return err
+	}
+	oldInbound.Remark = inbound.Remark
+	oldInbound.Enable = inbound.Enable
+	oldInbound.ExpiryTime = inbound.ExpiryTime
+	oldInbound.Listen = inbound.Listen
+	oldInbound.Port = inbound.Port
+	oldInbound.Protocol = inbound.Protocol
+	oldInbound.Settings = inbound.Settings
+	oldInbound.StreamSettings = inbound.StreamSettings
+	oldInbound.Sniffing = inbound.Sniffing
+
+	db := database.GetDB()
+	return db.Save(oldInbound).Error
+}

+ 6 - 1
web/service/server.go

@@ -264,7 +264,12 @@ func (s *ServerService) UpdateXray(version string) error {
 	}
 
 	s.xrayService.StopXray()
-	defer s.xrayService.StartXray()
+	defer func() {
+		err := s.xrayService.StartXray()
+		if err != nil {
+			logger.Error("start xray failed:", err)
+		}
+	}()
 
 	copyZipFile := func(zipName string, fileName string) error {
 		zipFile, err := reader.Open(zipName)

+ 15 - 0
web/service/setting.go

@@ -4,6 +4,7 @@ import (
 	_ "embed"
 	"strconv"
 	"strings"
+	"time"
 	"x-ui/database"
 	"x-ui/database/model"
 	"x-ui/logger"
@@ -110,3 +111,17 @@ func (s *SettingService) GetBasePath() (string, error) {
 	}
 	return basePath, nil
 }
+
+func (s *SettingService) GetTimeLocation() (*time.Location, error) {
+	defaultLocation := "Asia/Shanghai"
+	l, err := s.getString("time_location", defaultLocation)
+	if err != nil {
+		return nil, err
+	}
+	location, err := time.LoadLocation(l)
+	if err != nil {
+		logger.Errorf("location <%v> not exist, using default location: %v", l, defaultLocation)
+		return time.LoadLocation(defaultLocation)
+	}
+	return location, nil
+}

+ 5 - 0
web/session/session.go

@@ -38,4 +38,9 @@ func IsLogin(c *gin.Context) bool {
 func ClearSession(c *gin.Context) {
 	s := sessions.Default(c)
 	s.Clear()
+	s.Options(sessions.Options{
+		Path:   "/",
+		MaxAge: -1,
+	})
+	s.Save()
 }

+ 120 - 49
web/web.go

@@ -2,12 +2,14 @@ package web
 
 import (
 	"context"
+	"crypto/tls"
 	"embed"
 	"github.com/BurntSushi/toml"
 	"github.com/gin-contrib/sessions"
 	"github.com/gin-contrib/sessions/cookie"
 	"github.com/gin-gonic/gin"
 	"github.com/nicksnyder/go-i18n/v2/i18n"
+	"github.com/robfig/cron/v3"
 	"golang.org/x/text/language"
 	"html/template"
 	"io"
@@ -15,9 +17,7 @@ import (
 	"net"
 	"net/http"
 	"os"
-	"runtime"
 	"strconv"
-	"time"
 	"x-ui/config"
 	"x-ui/logger"
 	"x-ui/util/common"
@@ -42,21 +42,7 @@ func (f *wrapAssetsFS) Open(name string) (fs.File, error) {
 	return f.FS.Open("assets/" + name)
 }
 
-func stopServer(s *Server) {
-	s.Stop()
-}
-
 type Server struct {
-	*server
-}
-
-func NewServer() *Server {
-	s := &Server{newServer()}
-	runtime.SetFinalizer(s, stopServer)
-	return s
-}
-
-type server struct {
 	listener net.Listener
 
 	index  *controller.IndexController
@@ -66,19 +52,63 @@ type server struct {
 	xrayService    service.XrayService
 	settingService service.SettingService
 
+	cron *cron.Cron
+
 	ctx    context.Context
 	cancel context.CancelFunc
 }
 
-func newServer() *server {
+func NewServer() *Server {
 	ctx, cancel := context.WithCancel(context.Background())
-	return &server{
+	return &Server{
 		ctx:    ctx,
 		cancel: cancel,
 	}
 }
 
-func (s *server) initRouter() (*gin.Engine, error) {
+func (s *Server) getHtmlFiles() ([]string, error) {
+	files := make([]string, 0)
+	dir, _ := os.Getwd()
+	err := fs.WalkDir(os.DirFS(dir), "web/html", func(path string, d fs.DirEntry, err error) error {
+		if err != nil {
+			return err
+		}
+		if d.IsDir() {
+			return nil
+		}
+		files = append(files, path)
+		return nil
+	})
+	if err != nil {
+		return nil, err
+	}
+	return files, nil
+}
+
+func (s *Server) getHtmlTemplate(funcMap template.FuncMap) (*template.Template, error) {
+	t := template.New("").Funcs(funcMap)
+	err := fs.WalkDir(htmlFS, "html", func(path string, d fs.DirEntry, err error) error {
+		if err != nil {
+			return err
+		}
+
+		if d.IsDir() {
+			newT, err := t.ParseFS(htmlFS, path+"/*.html")
+			if err != nil {
+				// ignore
+				return nil
+			}
+			t = newT
+		}
+		return nil
+	})
+	if err != nil {
+		return nil, err
+	}
+	return t, nil
+}
+
+func (s *Server) initRouter() (*gin.Engine, error) {
 	if config.IsDebug() {
 		gin.SetMode(gin.DebugMode)
 	} else {
@@ -111,11 +141,15 @@ func (s *server) initRouter() (*gin.Engine, error) {
 
 	if config.IsDebug() {
 		// for develop
-		engine.LoadHTMLGlob("web/html/**/*.html")
+		files, err := s.getHtmlFiles()
+		if err != nil {
+			return nil, err
+		}
+		engine.LoadHTMLFiles(files...)
 		engine.StaticFS(basePath+"assets", http.FS(os.DirFS("web/assets")))
 	} else {
-		t := template.New("")
-		t, err = t.ParseFS(htmlFS, "html/**/*.html")
+		// for prod
+		t, err := s.getHtmlTemplate(engine.FuncMap)
 		if err != nil {
 			return nil, err
 		}
@@ -132,7 +166,7 @@ func (s *server) initRouter() (*gin.Engine, error) {
 	return engine, nil
 }
 
-func (s *server) initI18n(engine *gin.Engine) error {
+func (s *Server) initI18n(engine *gin.Engine) error {
 	bundle := i18n.NewBundle(language.SimplifiedChinese)
 	bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
 	err := fs.WalkDir(i18nFS, "translation", func(path string, d fs.DirEntry, err error) error {
@@ -201,38 +235,40 @@ func (s *server) initI18n(engine *gin.Engine) error {
 	return nil
 }
 
-func (s *server) startTask() {
-	go func() {
+func (s *Server) startTask() {
+	err := s.xrayService.StartXray()
+	if err != nil {
+		logger.Warning("start xray failed:", err)
+	}
+	s.cron.AddFunc("@every 30s", func() {
+		if s.xrayService.IsXrayRunning() {
+			return
+		}
 		err := s.xrayService.StartXray()
 		if err != nil {
 			logger.Warning("start xray failed:", err)
 		}
-		ticker := time.NewTicker(time.Second * 30)
-		defer ticker.Stop()
-		for {
-			select {
-			case <-s.ctx.Done():
-				return
-			case <-ticker.C:
-			}
-			if s.xrayService.IsXrayRunning() {
-				continue
-			}
-			err := s.xrayService.StartXray()
-			if err != nil {
-				logger.Warning("start xray failed:", err)
-			}
+	})
+}
+
+func (s *Server) Start() (err error) {
+	defer func() {
+		if err != nil {
+			s.Stop()
 		}
 	}()
-}
 
-func (s *server) Run() error {
-	engine, err := s.initRouter()
+	loc, err := s.settingService.GetTimeLocation()
 	if err != nil {
 		return err
 	}
+	s.cron = cron.New(cron.WithLocation(loc), cron.WithSeconds())
+	s.cron.Start()
 
-	s.startTask()
+	engine, err := s.initRouter()
+	if err != nil {
+		return err
+	}
 
 	certFile, err := s.settingService.GetCertFile()
 	if err != nil {
@@ -251,16 +287,51 @@ func (s *server) Run() error {
 		return err
 	}
 	listenAddr := net.JoinHostPort(listen, strconv.Itoa(port))
+	var listener net.Listener
 	if certFile != "" || keyFile != "" {
-		logger.Info("web server run https on", listenAddr)
-		return engine.RunTLS(listenAddr, certFile, keyFile)
+		cert, err := tls.LoadX509KeyPair(certFile, keyFile)
+		if err != nil {
+			return err
+		}
+		c := &tls.Config{
+			Certificates: []tls.Certificate{cert},
+		}
+		listener, err = tls.Listen("tcp", listenAddr, c)
+	} else {
+		listener, err = net.Listen("tcp", listenAddr)
+	}
+	if err != nil {
+		return err
+	}
+	if certFile != "" || keyFile != "" {
+		logger.Info("web server run https on", listener.Addr())
 	} else {
-		logger.Info("web server run http on", listenAddr)
-		return engine.Run(listenAddr)
+		logger.Info("web server run http on", listener.Addr())
 	}
+	s.listener = listener
+
+	s.startTask()
+
+	go engine.RunListener(listener)
+
+	return nil
 }
 
 func (s *Server) Stop() error {
 	s.cancel()
-	return s.listener.Close()
+	if s.cron != nil {
+		s.cron.Stop()
+	}
+	if s.listener != nil {
+		return s.listener.Close()
+	}
+	return nil
+}
+
+func (s *Server) GetCtx() context.Context {
+	return s.ctx
+}
+
+func (s *Server) GetCron() *cron.Cron {
+	return s.cron
 }

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio